目录
1.类的加载以及加载过程
1.1类加载的过程
1.2类加载器的分类
1.3 启动类加载器BootstrapClassLoader
1.4 扩展类加载器(ExtensionClassLoader)
1.5应用程序类加载器(AppClassLoader)
1.6用户自定义类加载器
1.7双亲委派机制
1.8 补充
2.运行时数据区和jvm的基本结构
2.1 jvm结构
2.2 HotSpotVm 的运行时数据区
2.3 jvm的程序计数器(pc)
2.4虚拟机的栈
2.4.1 操作数栈
3.JVM的堆
3.1堆的内存细分
3.2 新生区老年区
3.3对象分配的过程
3.4堆的垃圾回收分类(GC)
3.5内存的分配策略
3.6 TLAB分配
3.7 一些分配堆空间的设置参数
4.方法区
4.1 概念
4.2方法区的参数设置
4.3方法区(元空间)的内部结构
4.4运行时常量和常量池
4.5方法区到元数据区(元空间)的演变
4.6方法区的垃圾回收
5.对象的实例化和执行引擎
5.1.对象的内存布局
5.2对象的访问定位
5.3执行引擎的概念
5.4解释器和JIT编译器
5.4.1解释器
5.4.2 JIT编译器
5.5热点代码及其探索方式。
6. 垃圾回收
6.1 垃圾回收概述
6.2java垃圾回收的机制
6.3 垃圾回收算法
6.3.1引用记数算法(垃圾的标记)
6.3.2 可达性分析算法(根搜索、追踪性)(垃圾标记)
6.4.对象的Finalization机制。
建议不主动调用finaliz()方法,应该交给垃圾回收机制调用
6.5 垃圾清除算法
6.5.1标记清除算法
6.5.2 复制算法
6.5.3 标记压缩(整理)算法
6.5.4对比
6.6分代收集算法
6.7 增量收集算法
6.8分区算法
简单介绍一下java程序的运行流程
下面就是jvm虚拟机
正是有了jvm虚拟机才体现了java语言的跨平台性
所以说jvm的作用如下:
java虚拟机是二进制的字节码运行环境,可以运行.class文件。并且解释编译为对应平台的机器指令进行执行。
特点:
目前市面上最常见运用最广泛的是HotSpotVm虚拟机
在虚拟机发展的历程中还有如下的JVM:sun ClassicVM、ExactVm、IBMJ9、AzulVM等等感兴趣的可以去搜索一下,我们主要根据HotSpotVm讲解JVM。
通过javac的编译会生成.class
我们的类加载器就会加载我们电脑磁盘上的class文件,加载到了JVM虚拟机中被称之为DNA原数据模板,存放在方法区中。
就是这个class文件到达jvm成为原数据模板的过程之中需要通过类加载器实现(ClassLoader)。我们的类加载器相当于运输设备。
对于加载的过程细分如下
我们重点解读一下链接 的过程
目的在于确保class文件的字节流中的信息符合虚拟机的要求,保证正确性不会危害jvm虚拟机的运行。
4种验证方式:文件格式验证,原数据验证,字节码原则,符号应用验证
为类的变量分配内存并且设置默认值
这里不包含final修饰的static,因为final在编译的时候就会分配内存
这里不会为实例变量分配初始化,类的变量会分配在方法区中,而实例化的变量会随着对象一起分配到java的堆中
将常量池中的符号引用转换为直接引用过程。
事实上,解析操作往往会伴随着jvm在执行完成初始化之后进行
解析的动作主要是针对类或者接口、字段、类方法、接口方法、方法类型等。
初始化阶段就是执行类构造器方法的过程
此方法不需要被定义,是javac编译器字段收集类中的所有类变量的赋值动作和静态代码中的语句进行合并。
JVM虚拟机支持2中类加载器如下:
自定义类加载 器一般指的是由ClassLoader派生出来的类加载器称之为自定义。
启动类的加载器Jvm虚拟机自带BootstrapClassLoader
这个类加载器是用c语言实现的,嵌套在jvm的内部
他用来加载java的核心类库。
并没有继承java.lang.classLoader没有父类加载器
出于安全考虑Bootstarap启动类加载器只会加载包名为java javax sun等开头的类
java语言编写的加载器
派生于Classloader
父类是启动类加载器
从java.ext.dirs系统属性所指向的目录加载类库,或者从jdk的安装目录的jre/lib/ext子目录下加载类库。如果用户自己创建的jar放在此目录下,也会自动由扩展类加载器进行加载。
由java语言编写
派生于ClassLoader
负责加载变量classpath或者系统属性java.class.path指定路径下的类库
该类加载器是陈故乡默认的类加载器,一般的java应用类都是又他完成加载的
开发人员可以通过继承ClassLoader类的方式实现
在jdk1.2之前要求开发人员继承了ClassLoader的时候要重写loaderClass()方法,但是在jdk1.2之后不建议覆盖loaderClass()方法,而是讲加载的逻辑方法写在FindClass()方法之中。
java虚拟机对class文件采用按需加载的方式,也就是说当需要用到这个类的时候才会将他的class文件加载到内存生成class对象。而且在加载这个对象的class文件的时候,jvm采用的是双亲委派的模式,就是把请求交由父类处理。
工作原理:
Jvm中判断2个class对象是否为同一个类存在以下条件
jvm必须知道一个类型是由启动类加载器加载的还是有用户类型加载器加载的。
1. 如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存着方法区。
2.当解析一个类型得到另一个类型的引用的时候,jvm需要保证这两个类型的加载器是相同的。
java如何使用类
内存是非常重要的系统资源,建立cpu和硬盘中间仓库桥梁。jvm内存布局规定了java在运行过程中的内存申请、分配、管理的策略。保证了jvm的高效稳定,不同的jvm对于内存的划分和管理机制不同这样我们主要也是HotSpotVm。
java的虚拟机定义了若干中程序运行时会使用到的运行时数据区,其中有的一线会随着虚拟机的启动二创建,伴随着虚拟机的退出而销毁。另外一些则是与线程一一对应,这些与线程对应的数据区域会随着线程的生命周期而创建消失。
如下图:
灰色表示为单独现场私有,红色表示多个线程的共享
每个线程:独立包括程序计数器,栈、本地栈。
线程空间共享:堆、堆外内存
在hotspot jvm中,每个线程都与操作系统的本地线程直接映射,当java虚拟线程机准备好,此时本地操作系统同时创建一个线程。java虚拟机的线程结束,本地也回收线程。
jvm中的程序计数器(pc)并非我们真正计算机cpu中的pc,jvm中的pc是对物理oc寄存器的一种抽象模拟。cpu只有把数据装载到寄存器才能运行。
学过操作系统的都知道pc是用来存放下一条指令的地址,也就是将要执行的代码。
在jvm中他是很小的一个空间,但是是速度最快的区域,jvm中没个线程拥有自己的pc,是线程私有的,生命周期与线程一致。
它是程序控制的指示器,分支,循环,跳转,异常处理,线程恢复的基础。
字节码解释器工作是就是通过改变这个计数器的值来选取下一个指令
cpu来回去换线程的情况下cpu知道从哪里开始哪里结束
jvm解释器通过pc的值了解下一条命令
由于jvm的跨平台性,java的指令都是更具栈来设计的,因为不同的cpu框架不同,因此不能通过基于寄存器来设计
优点:跨平台,指令集小,编译器容易实现
缺点:性能下降,指令更多
栈是运行时的单位,堆是存储的单位
早期也叫做java栈,每个线程在创建的时候都会创建一个虚拟机的栈,其内部报错一个一个栈帧,对应一次java的方法调用。
生命周期和线程一致
主要管理程序运行,保存方法的局部变量,部分结果,参与方法的调用和返回。
速度快仅次于pc
操作简单----出栈/入栈
不存在垃圾回收的问题
栈可能出现的异常:栈满内存溢出,内存泄漏等等
对于栈内存不足我们可以在idea工具设置内存大小 修改-Xss
但是我们不建议修改过大
每个线程都要自己栈,栈中的数据都是根据栈帧(stackFrame)的格式存储
在线程上每个正在执行的方法都对应于一个栈帧
栈帧是内存的一颗内存区,是一个数据集,维系方法运行的数据信息
jvm对栈的操作只有出入栈
在一条活动线上,一个时间点上,只会有一个活动的栈帧。只有当前正在执行的方法的栈帧在栈顶才是有效的,成为当前栈。
执行引擎运行的所以直接骂指令只针对当前栈帧进行操作
如果这个方法调用其他方法,对应的新的栈帧会被创建出来,这个新的栈帧位于栈顶。
不同线程的栈帧是不允许相互引用的
当前方法调用其他方法返回的时候,当前栈帧回传回此方法的结果给前一个栈帧,接着虚拟机丢弃当前栈帧,使得被调用的方法栈帧位于顶端(即是前一个栈回归顶部)
方法嵌套的次数由栈的大小决定,栈越大嵌套越多。对于一个方法而言参数和局部变量越多,栈帧就越大。
局部变量表中的变量旨在当前方法有效,方法调用后随着方法的栈帧销毁,局部变量表也随之销毁
slot的重复利用
变量的分类
按照数据类型分:① 基本数据类型 ② 引用数据类型
① 成员变量:在使用前,都经历过默认初始化赋值
类变量: linking的prepare阶段:给类变量默认赋值 ---> initial阶段:给类变量显式赋值即静态代码块赋值。(前面讲到类加载的过程) 实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值。
② 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
补充:
在栈帧中,与性能调优最密切的部分就是签名提到的局部变量表。在方法执行是,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表直接或者间接引用的对象都不是回收的对象。
每一个独立的栈帧除了上述提到的局部变量外,还有后进先出的操作数栈。
操作数栈主要用于保存计算机计算过程的中间值,同时作为计算过程变量的中间存储空间。
如果被调用的方法带有返回值的话,其返回值会被压入当期栈帧的操作数栈中,并且更新下一个pc执行的字节码命令。
由于操作数是存在内存中的,因此频繁的执行内存读写是必然会影响执行的速度。为了解决这个问题,HotspotVm 提出了栈顶缓存技术。将栈顶元素全部缓存在无物理cpu的寄存器中,以此达到降低对内存读写的次数。提高执行效率。
每一个栈帧内部都包含了一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用就是为了支持当前方法的代码能够实现动态链接。
当一个方法调用另外的其他方法时,就是通过常量池中指向方法的符号引用表示,动态链接的作业就是为了将这些符号引用转换为调用方法的直接引用。
jvm中,将符号引用转换为调用方法的变得机制有关
静态链接:
动态链接:
总结:简而言之在编译前由程序员显示指明的绑定方法称之为静态,根据运行时才知道的方法绑定称之为动态。
方法结束有2种方式:
无论哪种方式退出,方法在退出后都返回该方法被调用的位置。方法正常退出的时候,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
然后异常退出,返回的地址是要异常处理表来确定的,栈帧一般不报错异常处理表的信息。
结合上文介绍栈的运行原理+方法的调用变量的调用,大概的流程如下
堆在jvm中的位置
概念:
在jdk7前堆内存的逻辑上分为:新生区、养老区、永久区。
在jdk8之后划分方式如下:
因为在jvm启动的时候我们就为jvm开辟了内存空间我们可以通过设置jvm参数进行设置
-Xms 用于表示堆的起始内存,等价于 -XX:InitialHeapSize
-Xmx 用于表示堆区的最大内存,等价于 -XX:MaxHeapSize
/**
* 1. 设置堆空间大小的参数
* -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
* -X 是jvm的运行参数
* ms 是memory start
* -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
*
* 2. 默认堆空间的大小
* 初始内存大小:物理电脑内存大小 / 64
* 最大内存大小:物理电脑内存大小 / 4
* 3. 手动设置:-Xms600m -Xmx600m
* 开发中建议将初始堆内存和最大的堆内存设置成相同的值。
* 4. 查看设置的参数:方式一: jps / jstat -gc 进程id
* 方式二:-XX:+PrintGCDetails
*/
一旦堆区的内存大小超过 -Xmx的上线的时候就会抛出 OutOfMemoryerror异常
我们通常将 Xms和Xmx设置为相同的内存大小,目的为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
存储在jvm中的java对象可以划分为2类:
在这更进一步划分 年轻代(YoungGen)和老年代(OldGen),年轻代又可以划分为 Eden空间和S0空间和S1空间(from区或者to区)。见下2图
我们可以在jvm创建之前为我们分配新生区和老年区的空间比例;
默认下在Hotspot中 Eden空间和另外2个S空间的缺省占比8:1:1
同样的开发人员也是可以进行设置的
-XX:SurvivorRatio=8
几乎所有的对象都是在Eden区被new出来的。
绝大多数java对象的销毁都在新生区。
我们也可以通过 -Xmm设置新生区的内存大小
为新的对象分配内存,要考虑如何分配,在哪里分配等问题,还需要考虑分配完成Gc执行后的内存碎片问题。
-XX:MaxTenuringThreshold=
正常的对象分配流程如上述但是还是会有特殊情况入下图
在按照正常的分配流程下如果当前分配的空间不够的时候可以分配到 s区或者永久区,或者直接报错。
根据回收的区域而言我们把GC分为3类,也分为部分收集和整堆收集
为什么堆要进行分区工作(新老区)
对于java对象而言百分之八十的对象都是临时对象,分区的唯一的好处就是优化GC的性能,可以快速GC临时对象。对于长期有效的对象进行保存,逼不得已再进行清查。
这就是我们上面将的每次幸存的次数都会累加,达到我们规定的次数之后进行转移区域。
为什么用tlab
tlab的作用
tlab的过程
* 测试堆空间常用的jvm参数:
* -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
* -XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
* 具体查看某个参数的指令: jps:查看当前运行中的进程
* jinfo -flag SurvivorRatio 进程id
*
* -Xms:初始堆空间内存 (默认为物理内存的1/64)
* -Xmx:最大堆空间内存(默认为物理内存的1/4)
* -Xmn:设置新生代的大小。(初始值及最大值)
* -XX:NewRatio:配置新生代与老年代在堆结构的占比
* -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
* -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
* -XX:+PrintGCDetails:输出详细的GC处理日志
* 打印gc简要信息:① -XX:+PrintGC ② -verbose:gc
* -XX:HandlePromotionFailure:是否设置空间分配担保
总结
方法区也是jvm内存结构的一部分。
根据我们已经讲解的栈,堆来讲解和方法区的关系
在java虚拟机规范中明确的说明:尽管所有的方法区都属于堆的一部分,但是一些简单的实现可能不会再选择进行垃圾回收或者压缩。对于jvm而言方法区也叫非堆区。所以方法区看作是独立于堆的区域。
jdk7之前:
jdk8之后:
方法区存储的内容主要如下:
它用于存储已被加载的类型信息,常量、静态变量、即时编译器编译后的低吗缓存。
对于每个加载的类、接口、枚举、注解JVM都需要存储他们的
完整有效名称、直接父类名的完整有效名称、类的修饰符,类的直接接口的有序列表。
方法名称、返回类型、方法参数的数量和类型、方法的修饰符、异常表、方法的字节码信息
结构图
在方法区内部包含 了运行时常量池
字节码文件中包含了常量池
一个有效的字节码文件除了包含类的信息、字段、方法、接口描述、还包含意向信息就是常量池表,包含了各种字面量和对类型、域和方法的符号引用。
为什么需要常量池
一个java源文件的类、接口、编译后会产生一个字节码源文件。然而java的字节码需要数据支持。通常这种数据会很大以至于不能直接存到字节码中。换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用,在动态链接的时候会链接运行时常量池(类的加载介绍过)。
所以说常量池可以看做是一张表,虚拟机根据这张表找到要自信的类名、方法名、参数类型、字面量等类型。
对于jdk8而言元空间的数据直接分配带系统的内存中,这样做在某些场景下都给他加载类过多也有足够的空间。不用像使用永久区一样对于庞大的业务来说需要我们对永久区进行调优。
jdk7中将StringTable放到了堆空间中,因为永久代的回收效率很低,在FullGc的时候才会触发,,FuulGC只有在老年代和永久代不足才触发,并且速度慢。这样就到这StringTablle回收效率低下,在我们的开发中大量的字符串被创建,放到堆空间中回收及时不容易引起空间不足。
方法区的垃圾回收主要是2个部分:常量池中飞起的常量和不在使用的类型。
方法区主要存放的2大类型常量:字面量和符号引用。
字面量类似于常量的概念,而符号引用则接近于编译原理的概念。
Hotspot虚拟机对常量池的回收策略是比较明确的,只要在常量池中常量没有被任何地方引用就可以被回收。与堆回收对象类似。
对于判断常量是否销毁相对简单,但是判断一个类型是否销毁的条件相对苛刻。必须满足3个条件
只有满足上面的3个标准的无用类进行回收处理。初次之外这里的回收并不是绝对的,关于这样的回收还收到jvm的控制。
创建对象的方式:
new、Class的newInstance()方法(反射)、反序列化、clone()....等等
创建对象的步骤:
栈帧的对象引用
通过栈帧指向堆区在通过堆区指向方法区(元空间)
执行引擎是java虚拟机核心的组成成分之一。
jvm虚拟机就是相对我们物理机器(操作系统机器)的概念,他们的区别在于物理机的执行引擎直接建立在处理器、缓存、指令集和操作系统上,然而虚拟机的执行引擎是建立在软件自行实现的。因此jvm的执行引擎不受指令集的约束,可以执行一些不能够被硬件直接支持的指令格式。
将字节码指令解释/编译为对应平台的本地机制指令。
原因
因为jvm的主要任务是负责庄重字节码到其内部结构、单字节码不能直接运行在操作系统之上。这个时候的字节码不等价本地机器的指令,他只能被jvm识别,这个时候就需要执行引擎翻译为我们本地机器认识的机器语言。
大部分的程序代码转换成目标代码或者虚拟机执行的指令集之前都需经过上述步骤。
当java虚拟机启动是会根据定义的范围对字节码采用逐行解释的方式执行。将每条字节码文件的内容“翻译”为对应平台的本地机器执行
就是将细腻及的源代码直接编译成和本地机器相关的机器语言。
现在的jvm执行java代码的时候,通常都会解释执行与编译执行二者结合,也就是上述2个方案都保留了。
jvm的设计者初衷只是为了实现java的跨平台性,因此避免通过静态编译的方式直接生成本地机器指令,从而诞生了实现编译器在运行时采用解释字节码的想法。
字节码解释器;执行纯软件代码,效率低
模板解释器:每个字节码指令和一个函数关联,从模板中直接生成字节码对于的机器码
基于解释器的执行是非常低效的。
编译器的机制就是编译执行。这种方式就是将方法编译成机器码后直接执行。
在HotspotJVM中依旧是保留了 解释器与即时编译器并存的架构体系。
因为在程序启动后解释器就可以立马发挥作用,省去编译的时间,立即执行。
JIT编译器想要发挥作用,把代码编译成本地代码是需要时间,但是编译为本地代码后,执行速度很快。简单的理解 一个起步快 加速慢,另一个起步慢加速快。
因此在JVM刚刚启动的时候可以发挥解释器的作业,不必等等编译器启动。随着时间的推移,JIT逐步发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,提高程序效率。
关于是否启动JIT需要根据代码被调用的执行频率而定。一般来说被JIT调用的代码称之为“热点代码”JIT会对热点代码进行深度优化,直接编译成本地机器指令,提升java程序执行的性能。
识别为热点代码的主要是根据热点探测功能。
HotspotJVM采用热点探测的功能是基于计数器的热点探测。主要的计数方式有方法调用计数器和回边计数器。
方法计数器
回边计数器
简单的统计方法体的循环次数触发OSR编译。
在JVM中可以设置执行方式设置成完全解释器或者完全即时编译器的方法
上面我们提到了客户端(Client)和服务端(Server)模式这个是什么呢?
总结来说:
Clenit优化方式简单可靠 耗时短,编译速度快。
Server 优化时间长达到激进优化的目的,但是这个优化后的代码执行更快。
垃圾回收早在Lisp语言诞生了,并不是java专属的概念。
最主要关注的问题:
但是垃圾回收也是java的招牌能力,极大提高了开发的效率。
垃圾回收知道在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的对象。如果不及时的回收这些对象那么垃圾会一直占用空间直到程序结束,甚至导致程序被动结束。
对弈一个程序来说不进行垃圾回收,内存是迟早会被消耗殆尽。
除了释放对象,垃圾回收还可以清除内存的记录碎片。碎片整理将占用的堆内存移动到一边,以便JVM回收将整理出来的对象分配给新的对象。
有了GC的操作才保证了应用程序的正常进行。
自动内存管理,无需开发人员手动暗语内存的分配与回收,这样降低了内存泄漏和内存溢出的风险。
自动内存管理机制,将开发人员从繁重的内存管理中释放专注于业务的开发。
垃圾回收器可以对年轻代进行回收也可以对老年代回收甚至回收方法区的垃圾。
在堆区存放的所有java对象的实例,在GC之前,首先需要区分内存中安歇是存活的对象那些是需要回收的垃圾对象。只有被标记的已经死亡的对象(清除被标记的垃圾)。GC才会在执行垃圾回收的时候,释放占用的内存的空间,因此这个阶段也称之为垃圾标记阶段。
jvm中规定当一个对象已经不再被任何 存活的对象继续引用的时候,就可以宣布此对象是垃圾。
判断是垃圾对象的2种方式,引用计数算法和可达性分析算法。
每个对象报错一个整型的引用计数器,用户记录对象被引用的情况。
当有新的对象指向这个对象计数器加一反之减一,当计数器为0表示该对象为垃圾可以进行回收。
实现简单,垃圾对象便于识别,判断效率高,回收没有延迟。
定义单独的计数器增加内存开销
计数器进行 +-操作增加程序的时间开销
当2个垃圾对象互相引用无法识别
因此java并没有使用引用计数算法作为垃圾回收的方式。
可达性分析算法同样具备容易实现和执行高效的特点,除此之外还解决了循环引用的问题(垃圾互相引用)防止内存泄漏的发生。
所谓的根集合“GC root”就必须是一组引用活跃的对象。
基本判断思路:
如何选取GCroot(根对象集合):
小技巧:
由于root采用栈的方式存储变量和指针,所以如果是一个指针,那么他保存了对内存里面的对象,但是他自己又不放在对内存里面那么他也是一个Root。
注意点:
如果使用根可达分析算法判断内存是否回收,那么分析工作必须在一个能保证一致性的快照中进行。如果不能保证这点分析的准确性无法保证。
这个注意点也是导致GC过程中 必须终止程序工作的原因。
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
finalize方法被调用的时候回导致垃圾对象复活
finalize方法执行的实际是没有保障的,都是由GC线程决定的,不发生GCfinaliez方法将不会被调用。
finalize会影响GC的性能。
如何判断一个对象是否被回收
判断回收至少需要如下2次标记的过程
区别与上述的垃圾标记算法,接下来讲的算法是如何实现垃圾的清除。
当我们成功的区分了内存之中存活和死亡的对象之后,GC接下来的任务就是执行垃圾的回收。释放无用对象占用的空间,以便有足够的空间为新对象分配空间。
目前jvm主流常见的垃圾回收算法有三种:
标记清除算法(Mark-Sweep)、复制算法(Copying)、标记压缩算法(Mark-Compact)
是一种常见的垃圾回收算法,在1960年提出运用在Lisp语言。
执行过程:
当堆中的有效空间被消耗殆尽的时候们就会停止程序(stop the word)然后进行2个工作:标记-清除。
缺点:
注意:
这里的清除并不是真真的清除置空,而是吧需要清除的对象地址保存着限制空闲地址列表,下次有新的对象进行分配空间的时候,判断垃圾列表是否足够,足够就放。
适用于开销数量和存活数量成正比的情况
思想:
在活的内存空间中将内存空间分为2各部分,使用其中一个区域。在垃圾回收的过程之中将正在使用的内存的存活对象复制到未使用的空间,之后清理正在使用的内存中剩余的垃圾对象。最后完成回收
优点:
缺点:
补充:
适合垃圾对象多,存活对象少的场景。
运用区域 新生代的S0、S1区
上述说道 复制算法合适多垃圾的空间进行回收,但是在我们的老年区或者方法区都是垃圾较少,存活对象较多的区域,基于这一特点我们得使用其他算法。
标记清除算法也可运用在老年代和方法区,但是执行的效率低,而且产内碎片,因此产生了标记压缩算法。、
执行过程:
标记压缩算法等同于标记清除算法多了一步内碎片的处理。标记清除没有对象的移动,然而标记压缩有对象的移动。
指针碰撞:
优点:
缺点:
因为上述的3种算法没有哪一种真实能替代另一种,各有优势。
分代收集算法就由此诞生。它基于一个这样的事实,不同的对象什么周期不一样,因此,不同生命周期的对象可以采用不同的收集方式,以便提高效率。一般JVM堆区分为新生去老年区,这样根据各个特点执行不同的回收算法,提高回收效率。
目前所有的GC都是采用分代 收集算法(Generation Collecting)执行回收处理。
在Hotspot中,基于分代的概念,GC所有使用的内存回收算法必须结合新生区和老年区的特性。
内存较小,生命周期短,回收频率高
适用复制算法
内存大。生命周期长。存活率高,回收不频繁。
适用标记清除(压缩)算法或者混合使用
在Hotspot中的CMS回收器为例子,CMS垃圾回收器基于Mark-Sweep(标记清除)算法实现,对于对象的回收率很高,对于碎片问题,CMS采用了Mark-Compact算法的SerialOdl回收器作为补偿措施。当CMS执行回收的时候回收的效果不佳碎片严重,将采用SerialOdl执行FullGC达到老年代的内存整理。
分代思想运用广泛的虚拟机中,几乎所有的垃圾回收器都区分新生区和老年区。
上述提及的算法在处理垃圾回收的过程的时候都会出于stop the world 的状态,在这个状态下应用程序的所有线程都挂起,暂停工作。
为了解决这个问题诞生了 增量收集算法(IncrementalCollecting)
基本思想:
如果一次性的所有垃圾回收处理,需要造成很长的时间停顿,那么可以让垃圾回收线程和应用程序的线程交替执行。每次垃圾收集线程值收集一小片区域的内存空间。接着切换应用程序,直到垃圾收集完成。
总的来说,增量手机算法的基础任然是传统的标记清除和复制算法。增量收集算法通过对线程的重提的妥善处理,允许垃圾收集线程以分阶段的方式完成标记,清理或者复制工作。
缺点:
吞吐量下降,回收成本上升
根据线程数量划分,可以分为串行垃圾回收器和并行垃圾回收器。
指的是在同一个时段内只允许一个cpu执行垃圾回收操作,其他任务线程停止。直到垃圾回收结束 。(适用于处理器较小,较小应用内存的场合)
多个cpu同时执行垃圾回收,提升了吞吐量。不过同样也需要STW(stop the world)停止任务线程。
按照工作模式划分,可以分成并发式垃圾回收器和独占式垃圾回收器。
垃圾回收线程和应用程序线程交互执行,减少应用程序的停顿时间。
垃圾回收器启动,停止其他的应用程序工作,知道垃圾回收结束
除了以上的划分方式更具不同的特点还有如下的划分方式。
吞吐量:用户代码运行时间占总运行时间的比例(程序执行时间+垃圾回收时间)。
暂停时间:执行垃圾回收线程,程序暂停运行的时间。
垃圾收集开销:垃圾回收时间与众运行时间的比率。
收集频率:相对与运行程序,垃圾回收操作发生的频率。
内存占用:java堆区占用大小。
快速:一个对象从诞生到被会受到时间。
重点关注吞吐量和暂停时间。
高吞吐量的较好的原因回让用户觉得只有应用程序在执行任务。,直觉上吞吐量越高程序执行越快。
低延迟较好的原因是,对于用户而言不管是暂停回收垃圾任务还是程序任务都是不友好的。低延迟就会降低用户感知程序挂起的状态。
但是低延迟和高吞吐量是一个矛盾关系,此消彼长。
串行回收器:Serial、Serial Old
并行回收器:ParNew、Parallel Scavenge 、Parallel Old
并发回收器:CMS、G1。
#查看命令行的相关参数
-XX:+PrintCommandLineFlages:
#相关垃圾回收器的参数
jinfo -flag
Serial是一款最基本/历史最悠久的垃圾收集器。
是在HotSpotJVM下的CLient模式的新生代的默认收集器。
Serial 收集器使用的是 复制算法,兼顾串行和STW机制。
Serial Old提供了老年代的垃圾回收,同样兼顾了串行和STW的机制不同的是Serial Old使用的栓发是标记压缩算法。
这个Serial收集器是一个单线程的收集器,意味者只能使用一个cpu或者哟个线程去完成垃圾回收的工作。
优势:
简单高效
单线程执行,没有其他线程干扰。
可以使用如下指令知道新生代老年代使用Serial回收器。
-XX:+USerSerialGC
缺点:
单核,交互能力差,因此现在几乎不适用这个拉萨回收器了。
ParNew就是多线程版本的Serial收集器。
全称 Parallel new,只作用在新生区。
同样是使用复制算法和STW机制 不用同的是使用并行回收方法。
ParNew是很多jvm运行在Server模式下的新生代默认的收集器。
流程:
对于新生区,回收次数频繁,并行效率高。
对于老年代,回收次数少,串行方式节省资源
特点:
由于ParNew收集器是基于并行回收,因此在多cpu的环境下,可以发挥cpu数量多的优势更快完成垃圾回收任务。,提升了吞吐量。
但是在单cpu的环境下,ParNew收集器需要频繁的切换线程产生额外的开销。
# 收到指定使用ParNew回收器 在新生区不影响老年代
-XX:+UserParNewGC
# 限制线程数量,默认和cpu相同
-XX:ParallelGCThreads
Hotspot的新生区也是可以使用ParallelScavenge,这款回收器采用了复制算法,并行回收和STW机制。
但是Parallel Scavenge收集器的目标是达到一个可以控制的吞吐量,因此称之为吞吐量优先收集器。
自适应的策略是区分ParNew和parallel Scavenge的重要区别。
高吞吐量可以高效的运用cpu资源。
parallelOld 采用标记压缩算法 并行回收和STW机制 作用在老年代区域。
流程:
在java8默认使用的是Paralled回收器。
#指定使用parallel 回收器
-XX:USerParallelGC
# 指定老年代是使用并行回收器
-XX:+UserParallelOldGC
# 设置新生代并行收集器的线程数量
-XX:ParallelGCThreads
# 设置垃圾回收最大停顿时间
-XX:MAxGCPauseMillis
#设置垃圾收集器时间占总时间比例
-XX:GCTimeRatio
# 设置自适应策略
-XX:+UserAdaptiveSizePolicy
在jdk1.5 HotspotJVM提出的强交互应用的垃圾收集器。CMS(Concurrent-mark-sweep)
收集器。这款收集器让垃圾收集线程和任务线程同时工作。
cms收集器关注点在于尽可能缩短垃圾收集线程时的停顿时间。提高用户的体验,不会让用户感受到系统的停顿。
cms使用的是标记清楚算法,也是回STW的只是经可能的降低。
但是在CMS作为老年代收集器的时候无法和新生区的ParallelScavenge配合工作,因此新生代只能选则 ParNew或者Serial。
工作流程:
主要分为4个阶段--初始标记--并发标记--从新标记--并发清理
初始标记阶段:
在这阶段程序的线程都会STW ,标记出没有和ROOTGC关联的对象 速度较快。
并发标记:
从ROOTGC的直接关联对象进行遍历,时间较长但是 不会STW可以和任务线程同时发生。
从新标记:
修正并发标记期间,因为用户程序继续运行而产生导致的标记变得的那一部分对象的标记记录。这个时间停顿<并发标记,>初始标记。
并发清理:
清理删除标记判断的已经死亡的对象,释放空间。不用改变对象地址或者指向,因此是并发执行的。
尽管cms是并发回收的,但是在初始化标记阶段和再次标记阶段都需执行STW机制。只是说尽可能减少了暂停的时间,因为并发标记和并发清理都不需要停顿所以说是低停顿的。
在垃圾回收阶段也没有停止用户线程,所以在cms的垃圾回收阶段,应该尽可能的保证应用程序的用户线程有足够的空间。所以说cms不会像其他收集器一样等待老年代空间满了才进行垃圾回收,而是当内存使用到了一定的峰值,就开始进行垃圾回收。确保程序在cms阶段工作中的程序依然有和足够的内存空间使用。但是如果还是堆空间不够的这个时间JVM会启动SerialOld进行老年代的回收,消耗大量的时间。
因为cms使用的标记清楚算法,意味着清除对象之后的空闲地址是不连续的,可能会产生内碎片的问题。这样就需要引入空闲表来管理空闲地址。
并发执行
低延迟
产生内碎片
cms非常依赖cpu资源
无法清理浮动垃圾,在垃圾收集阶段和程序运行阶段,如果在并发标记的时候程序产生垃圾cms无法回收这部分垃圾只能等待下一次。
# 指定使用cms
-XX:+UserConcMarkSweepGC
#设置内存使用的峰值 处罚执行cms
-XX:CMSlnitiatingOccupanyFraction
# 指定在执行完成fullCG后堆空间进行压缩
-XX:CMSFullGCsBeforeCompaction
#用于执行完成Fullgc后对内存进行压缩
-XX:+UserCMSCompactionAtFullCollection
#设置cms线程数量
-XX:ParallelCMSThreads
最新化使用内存和并行开销 选择 serialGC
最大化程序应用吞吐量ParallelGC
最小话GC停顿时间 cmsGC
G1的目标是在延迟可控的情况下获得尽可能高的吞吐量,是全功能收集器的开始。
G1是一个并行回收器,他把内存划分为很多不相干的区域(Region)。使用不同的区域标识Eden。
G1避免了整个java堆的全区域垃圾回收。G1跟踪Region里面的垃圾堆提交的价值大小在后台维护一个优先级列表,根据优先级,进行优先回收最有价值的Region。
G1是面向服务端的垃圾回收器,主要正对多喝cpu和大容量内存的机器。 兼备高吞吐量的性能。
jdk7后正式启用,在jdk9成为默认的垃圾收集器取代了cms,称之为全功能收集器。
在jdk8我们需要手动启动G1
-XX:+UserG1GC
并行并发:g1的时候多个GC线程同时工作,g1有着和应用程序交替执行的能力。不会发生阻塞
分代收集:依然属于分代收集器,区分老年代和新生代,将堆区划分多个R区,可以回收老年代和新生代。
空间整合:剔除了CMS内碎片的问题,G1内存回收以Region为单位Region之间是复制算法,但是整体又是标记压缩算法。可以避免内碎片的产生。
可预测的停顿时间模型:处理尽可能降低停顿时间,还可以简历预测停顿时间。
#启动G1收集
-XX:+UserG1GC
# 设置每个R区大小 值是2的平方 1-32mb
-XX:G1HeapRegionSize
#设置最大的gc停顿时间指标
-XX:MAxGCPauseMillis
#设置stw的gc线程数量 <8
-XX:ParallelGCThread
#设置并发标记线程数量
-XX:ConGCthreads
#设置出发并行GC的周期java堆的占有率的峰值 默认45
-XX:InitiatingHeapOccupancyPercent
使用G1收集器,他会将堆划分为2048个大小相同的Region,可以人为的设置大小。
保留了老年代和新生代的概念,但是不再是物理的隔离,他们是一部分的region。
上提到的Remebered Set就是上述Reset,上页提到的Reference类型就是引用类型,其中Reset的作用是记录当前Region中哪些对象被外部引用指向,比如Old区中的对象会指向Eden区的对象,然后当我们要回收某个Region的时候,直接遍历遍历当前Region中的所有对象就可以了,然后针对性的去找到那些指向当前对象的其他对象,最终发现当前对象是否是根可达的,如果不是,那就应该被删除,其实之前的垃圾回收器都涉及到这个问题,当进行Minor GC的时候,通过GC Roots查找的时候还需要遍历Old区的对象,毕竟Old区对象也可能会指向Eden区对象,但是G1通过Rset避免了全堆的扫描,当引用类型数据写操作时,先暂时中断,然后判断当前引用类型数据是否被其他对象所指向,如果不被指向,那就直接放在Region中就可以了;如果被其他对象指向,那么还要判断这个对象是在当前要插入的Region中,还是在其他Region中;如果在其他Region中,那就需要使用CardTable把当前引用类型数据的指向信息放在Rset中,也就是形成上面的虚线连线,如果在当前Region中,那就不需要指向了,毕竟到时候我们会进行遍历查找根可达对象,那肯定会找到的,所以这种情况也是直接放在Region中就可以了;
G1先准备好对eden区,程序在运行过程中不断创建对象到eden区,当空间耗尽就会出发G1回收年轻代。
年轻代对Eden和S区进行回收