1、JVM概述
- JVM: Java Virtual Machine,Java虚拟机,是Java跨平台(一次编译,到处运行)的基础,HotSpot是当今最流行的Java虚拟机。
- Java是编译型语言,还是解释型语言?
- Java源代码由编译器(javac.exe)编译为字节码,字节码由JVM解释器(java.exe)解释执行
- 解释性语言:执行机制是使用一个“解释器”来执行,解释器将程序逐句“翻译”成机器语言来逐句执行。例如Python、Ruby、Shell。
- 编译型语言:执行机制是使用一个**“编译器”**来编译成机器语言,用户可以直接运行(执行)编译好的“可执行文件”。例如Golang、C、C++。
2、类加载机制
- 类加载机制:Java虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验,转化解析和初始化,形成可以被虚拟机直接使用的Java类型的过程
2.1、加载(Loading)
时机:非一次性,需要时加载(new对象,反射等)
过程:ClassLoader(类加载器)通过双亲委派模型进行加载
结果:在堆中创建java.lang.Class的对象,在方法区存储类的相关信息
2.2、连接(Linking)
- 验证:校验类是否符合Java以及JVM规范
- 准备:为类的静态成员分配内存初始化默认值
- 解析:将符号引用转为直接引用
2.3、初始化(Intialization)
- 为类的静态成员赋指定的值,执行静态代码块
3、双亲委派模型
3.1、模型详解
- 双亲委派模型:ClassLoader自己不会首先加载类,而是将请求委派给父加载器,依次向上(自定义类加载器 --> 应用类加载器 -->扩展类加载器 --> 启动类加载器),若不在父加载器范围内,则再由自己加载
- 特点:
- 安全:核心类不会被篡改
- 高效:防止类被重复加载
- 双亲委派模型的源码
父加载器由组合实现,而非由继承实现
补充:组合指的是,一个类使用另一个类作为它的成员,即使用引用其他对象的实例变量来实现Java组合
3.2、classLoader.loadClass与Class.forName
面试题:
为什么JDBC加载mysql驱动时,只能使用Class.forName,而不是Thread.currentThread().getContextClassLoader().loadClass
- 在回答如上问题之前,需要先分析下mysql获取连接的步骤:
在类加载的初始化过程中,为类的静态成员赋指定的值,执行静态代码块 - Class.forName源码截取:(必须完成初始化)
- classLoader.loadClass()源码截取:(不完成解析,也就不完成初始化)
- 经由如上分析,
classLoader.loadClass()
不完成解析,也不完成初始化,也即不能获取数据库连接,而Class.forName()
则可以正常完成类的加载,获取数据库连接
3.3、双亲委派机制的打破
- 打破双亲委派模型机制:只要不向上委托即为打破。
- 场景:Tomcat的webapps目录下可以部署多个war包,每个war包都代表一个独立的web应用,web应用有同名类,但是具体实现都不一样,则需要打破,加载同名的2个不同实现的类。
- 如何打破:为每个Web应用创建一个WebAppClassLoader,重写loadClass,优先加载当前应用目录下的类,如果找不到,再向上委派。
- Tomcat类加载器一览
4、JIT
- JIT:just in time,即时编译器,把翻译过的机器码保存起来,以备下次使用,能够加速 Java 程序的执行速度。
- JIT编译器种类:
Client Compiler,简称C1,-client参数强制,关注局部优化,简单快速,放弃耗时的长时优化
Server Compiler,简称C2, -server参数强制,面向服务端,高性能,复杂 - 工作原理:
- Java源代码编译成字节码后,如果是热点代码,则经由JIT翻译成机器码且保存起来,以便后续再次使用,否则,交由
解释器
逐句解释为机器码 - JVM默认情况下对于即时编译请求在编译完成之前,都按照解释方式执行,编译动作在后台线程执行(-XX:-BackgroundCompilation禁止后台编译,此时编译请求会等待,直到编译完成后直接执行本地代码)
- Java源代码编译成字节码后,如果是热点代码,则经由JIT翻译成机器码且保存起来,以便后续再次使用,否则,交由
- 热点代码:被多次调用的方法 或 被多次执行的循环体
- 如何判断热点代码:计数器热点探测,通过参数
-XX: CompileThreshold
设置阈值次数。(Client模式下默认1500次,Server下默认10000次)
- 方法调用计数器:统计在一段时间内方法的被调用次数,即相对的执行频率
- 回边计数器:统计循环体的调用次数
5、JVM运行时数据区
5.1、线程私有:
- 1)程序计数器:用于存储当前线程所执行的字节码的行号指示器
- 2)虚拟机栈:JVM栈中存放着栈帧,用于执行Java方法,一个方法对应着一个栈帧
- 3)本地方法栈:存放着栈帧,用于执行本地方法(native关键字修饰的方法)
5.2、线程共享:
- 4)方法区:也称为非堆(Non-Heap),存放已被JVM加载的class信息,全局常量、静态变量、JIT编译后的代码缓存
- 在HotSpot虚拟机的实现中被称为永久代,在jdk8之后,用元空间替代了永久代(元空间、永久代只是虚拟机实现方法区的具体方式)
- 运行时常量池:存放编译期间生成的字面量和符号引用,在运行时也可以动态添加
- 5)堆:存放对象实例,是GC的主要区域
补充:
- 直接内存: 不属于JVM运行时数据区的一部分,不受堆大小的限制,也不会触发GC,需要程序员自行管理内存(Unsafe类、NIO的DirectByteBuffer类)
6、对象的创建
- 新对象一般情况下创建在堆里,创建时需要分配内存,分配方式有指针碰撞、空闲列表、TLAB以及虚拟机栈上分配内存
6.1、指针碰撞
- 1)假设Java堆中的内存分配是绝对规整的,指针作为已分配内存和未分配内存的分界线
- 2)给变量分配内存的过程就是将作为分界线的指针向未分配内存空间的方向上移动一个变量大小的偏移量
6.2、空闲列表
- 1)如果Java堆中的内存分配不是规整的,虚拟机就会维护一个空闲列表,用来记录剩余的可用内存空间
- 2)每次为变量分配内存后会动态地更新空闲列表
6.3、TLAB
TLAB(Thread Local Allocation Buffer):也即本地线程分配缓冲
- 背景:多个线程同时申请内存,则定会造成线程安全问题
- 说明:在线程初始化时,会申请一块指定大小的堆内存,只用来给当前线程使用,不存在线程竞争的情况,可通过设置参数
-XX:+UseTLAB
来开启该对象分配方式(默认开启)
6.4、虚拟机栈分配内存
- 创建的对象分配到堆内存,是为了对象在线程之间共享,如果此对象不需要在线程间共享,只局限于当前方法,则可以分配在栈上,而非分配在堆内存上
- 针对作用域不会逃逸出方法的对象,在分配内存时,不再将对象分配到堆内存,而是将对象属性打散后分配到栈上
- 优点:栈帧伴随方法执行结束而销毁(栈帧出栈),减轻GC负担;分配速度快,性能好
虚拟机栈分配内存依赖技术(编译期间)有:逃逸分析+标量替换
逃逸分析
- 逃逸分析,分析对象的动态作用域,供其他优化措施提供依据。比如分析一个对象不会逃逸到方法之外或线程之外,其它优化措施(栈上分配,标量替换等)根据逃逸程度进行代码优化,即方法中的 对象没有 传递到方法之外,jvm可以对其进行代码优化。
- jvm自动会为我们做逃逸分析优化,hotspot虚拟机为我们默认开启了三组jvm参数,分别是:
- -XX:+DoEscapeAnalysis:启用逃逸分析(默认打开)
- -XX:+EliminateAllocations:标量替换(默认打开)
- -XX:+UseTLAB 本地线程分配缓冲(默认打开)
标量替换
- 0标量替换:标量是指不可分割的量(如Java的基本数据类型,对象的地址引用),替换是指将对象成员变量的访问替换为基本数据类型
- 与标量对应的是聚合量,聚合量指的是可以进一步分解的量,比如字符串就是一个聚合量,因为字符串是用字节数组实现的,可以分解
7、对象的格式
下图是64位虚拟机对象头:
7.1、对象头
- 对象头(Header):自身运行时数据(mark word) + class类型指针,如果对象为数组,还应该包括数组长度
1)Mark Word
-
Mark Word保存了对象运行时必要的信息,包括哈希码(HashCode)、GC分代年龄、偏向状态、锁状态标志、偏向线程ID、偏向时间戳等信息。通过类型指针,可以找到对象对应的类型信息。
-
不论是32位还是64位虚拟机的对象头部都使用了4比特记录分代年龄,每次GC时对象幸存年龄都会加1,因此对象在survivor区最多幸存15次,超过15次时,仍然有可达根的对象就会从survivor区被转移到老年代,可以通过
-XX:MaxTenuringThreshold=15
参数设置最大幸存年龄。 -
JVM底层对加锁进行了性能优化,默认虚拟机启动后大约4秒会开启偏向锁功能。当虚拟机未启用偏向锁时,锁的演化过程为无锁->轻量锁(自旋锁)->重量锁; 当虚拟机启用了偏向锁时,锁的演化过程为无锁->偏向锁->轻量锁(自旋锁)->重量锁。
2)类型指针
- 类型指针存放的是该对象对应类的指针,即该指针应该指向方法区的内存区域。
3)数组长度
数组长度只在数组类型的对象中存在。用于记录数组的长度。避免获取数组长度时,动态计算。以空间换时间的做法。
7.2、实例数据
- 实例数据(InstanceData):存储一个类定义的对象的有效信息,包含从父类中继承的信息
7.3、对齐填充
- 对齐填充(Padding):任何对象的大小均为8字节的整数倍,不够则补充占位。
8、对象的销毁
8.1、GC概述
- GC:Garbage Collection(垃圾回收),所有具备动态内存分配能力的语言,都要面对的问题,不只是Java,包括Python、Go等其他语言。
- 垃圾回收需要面对的三个问题:
- 哪些内存区域需要回收.
- 什么时候回收.
- 如何回收(哪些对象需要清理,如何清理这些对象)
1)需要回收的内存区域
- 线程私有的区域(虚拟机栈、本地方法栈):随着线程销毁自然完成垃圾回收
- 线程共享的区域(堆、方法区):需要动态回收空间的区域
2)垃圾回收的时机
- 空间不足:堆内存不足以分配新对象时
- 预设阈值:到达垃圾收集器的设置的阈值
- 手工调用:System.gc()【会停止所有工作线程,建议关闭:-XX:+DisableExplicitGC】
3)垃圾回收的方式
垃圾识别算法
- 垃圾识别算法有引用计数法和可达性分析算法,Java的任何主流垃圾收集器均未采用引用计数法,而采用可达性分析算法
垃圾清理算法
- 垃圾清理算法有标记-清除、(标记)-复制、标记-整理算法
8.2、JVM方法区会垃圾回收吗
- 方法区存储的内容:被JVM加载的class类信息(完整类名、修饰符:public、abstract、final)、常量(static final修饰的静态变量)、静态变量、JIT编译后的缓存代码
- 《Java虚拟机规范》规定:不要求虚拟机在方法区中实现垃圾收集,回收的"性价比"比较低
- 可回收的内容包括不再被引用的常量和class对象,其class被回收的条件有:
- 1)class所有的对象实例都被回收.
- 2)加载该类的ClassLoader已被回收.
- 3)该类的java.lang.Class对象没有任何引用,且无法通过反射访问
8.3、finalize( )方法
- JVM回收对象时,会调用对象的finalize()方法,且只会调用一次,finalize()没有使用场景,释放资源也不要使用finalize()方法
- 出现原因:算是最初Java对C++的妥协,类似于析构函数
9、GC详解
9.1、垃圾识别
1)引用计数法
- 引用计数法:给对象添加一个引用计数器,每当一个地方引用它时,计数器加1,每当引用失效时,计数器减少1.当计数器的数值为0时,也就是对象无法被引用时,表明对象不可在使用。
- 引用计数法的优点是简单,缺点是无法解决循环引用的问题
- Java的任何主流垃圾收集器均未采用引用计数法。Python采用了此垃圾识别算法,在此基础上解决了循环引用的问题
2)可达性分析法
-
可达性分析法:从GC Roots出发,判断对象是否存活的方法
-
如何判断:
- 1)以GC Roots对象作为起点,向下检索,走过的路径称为引用链
- 2)当对象到GC Roots没有任何引用链时,则该对象可被回收
如下图所示,Object5和Object6就可以被当作垃圾来回收
-
可作为GC Roots的对象类型如下(一些当时肯定不会回收的对象做为GC Roots):
- 1)虚拟机栈 --> 栈帧中形式变量引用的对象
- 2)本地方法栈 --> JNI栈帧中形式变量引用的对象
- 3)方法区 --> 类静态属性引用的对象
- 4)堆 --> 被同步锁(synchronized)持有的对象
- 5)虚拟机内部的引用-基本数据类型对应的Class对象,系统类加载器,常驻的异常对象。
-
Java的任何主流垃圾收集器都采用可达性分析法来识别垃圾,且在枚举GC Roots时,都需要暂停所有的用户线程
9.2、垃圾清除
1)标记清除(Mark-Sweep)
- 标记-清除:先标记,再清除。
- 优点是简单,缺点是会有内存碎片
2)标记-复制(Copying)
- 复制算法,将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,GC时会将存活的对象复制到另一块内存中
- 优点是没有内存碎片,缺点是会造成内存空间浪费、效率低
3)标记整理(Mark-Compact)
- 标记整理:让所有存活的对象都向一端移动,再直接清理掉端边界以外的内存
- 优点是无内存碎片,不浪费内存空间,缺点是效率低下
- 创建对象时分配内存的方式有指针碰撞和空闲列表,指针碰撞:采用标记-整理和复制算法,因为要求内存绝对规整;空闲列表:采用标记-清除算,不要求内存绝对规整
9.3、GC分代假说
- 现代主流垃圾收集器遵循的经验方法论:
- 1)弱分代假说:绝大多数对象都是朝生夕死
- 2)强分代假说:熬过越多次垃圾收集的对象就越难以消亡
- 3)跨代引用假说:跨代引用相对于同代引用只占极少数
9.3.1、弱/强分代假说
-
如上的 假说1 + 假说2 => 堆内存划分为新生代、老年代(连续的内存地址空间),将堆进行分代,是为了更好地进行垃圾收集
- 新对象大都存储在“新生代”上,大对象直接来到老年代:XX:PretenureSizeThreshold =<字节大小>
- 新对象熬过N个GC(年龄)后,会移动到“老年代”上。
- 参数静态设置:-XX:MaxTenuringThreshold=N,动态设置:-XX:TargetSurvivorRatio=50
(当累加到 N 年龄时的总和大于50%,那么比N大的都会晋升到老年代
-
分代的好处:针对不同特点的对象,可采用不同的回收策略
- 老年代都是难以消亡的对象,可以较为低频地扫描;新生代都是"短命"的对象,需要较为高频地扫描
- 新生代适合使用"标记-复制"算法(剩下的对象较少,复制的代价较小),老年代适合使用"标记-清除"算法(剩下的对象多,复制的代价大,适合直接清除)
-
分代收集的概念:
- 新生代收集:Young GC/Minor GC
- 老年代收集:Old GC/Major GC
- 整堆收集:Full GC
9.3.2、跨代引用假说
- 跨代引用造成的问题
- 例1:进行Young GC的时候,若老年代引用了年轻代,此时年轻代的对象不能被错误回收。
- 例2:进行Old GC的时候,若新年代引用了老年代,此时老年代的对象不能被错误回收。
- 跨代引用的难点在于GC Roots的枚举,其解决方案是使用记忆集
- 记忆集(RSet),一种用于记录 从非收集区域指向收集区域的指针集合的抽象数据结构,由
写屏障+卡表
一同实现 - 使用记忆集,将老年代划分为若干小块,标识出老年代的哪块内存会存在跨代引用。在枚举GC Roots时,将跨代引用的对象加入GC Roots进行扫描,从而避免扫描整个年轻代或老年代
- 记忆集(RSet),一种用于记录 从非收集区域指向收集区域的指针集合的抽象数据结构,由
9.4、新生代的通用内存划分
- 新生代划分(连续的内存地址空间):Eden区、Survivor from区、Survivor to区
- 关于堆的参数设置:
- -Xms:堆的初始值
- -Xmx:堆的最大值
最佳实践:堆的初始值与最大值要相等,否则,服务在刚刚启动时的GC次数较多(频繁扩容) - -Xmn:设置年轻代的大小(默认新生代:老年代 = 1:2)
- -XX:SurvivorRatio:年轻代中Survivor与Eden的比例(默认Eden:from:to = 1:8:1)
- 新生代通用的是标记-复制算法(Serial、ParNew、 Parallel Scavenge)
- 1)新对象分配到Eden(大对象到老年代)
- 2)对新生代的Eden与From进行垃圾回收,存活的对象转存到新生代的Survivor to区
- 3)原先的From区转变为To区,原先的To区转为From区,如此反复
- 4)当对象的年龄到达15岁时,移动到老年代
注意:
- 如果Survivor to区无法装下存活的对象,则会因为担保机制,将装不下的对象直接转存到老年代
10、主流垃圾收集器
当前主流的垃圾收集器都采用都采用可达性分析来识别垃圾,我们可以将垃圾收集器按照分代收集和分区收集的方式来进行分类
- 分代收集:根据对象存活周期的不同,将内存划分为多块,不同的代选择合适的算法
- 分区收集:分区算法将整个堆空间划分为连续且不同的小区间,每个区间独立地使用和回收
10.1、Serial/Serial Old串行收集器
- Serial:串行收集器,最基本、发展历史最悠久的收集器
- 新生代使用Serial收集器,采用标记-复制算法;老年代使用Serial Old收集器,采用标记-整理算法。新生代和老年代在进行垃圾回收时,都需要暂停所有的用户线程(STW)
10.2、ParNew并行收集器
- ParNew:Serial收集器的多线程版本,新生代的收集器
10.3、Parallel Scavenge/Old吞吐量优先收集器
- Parallel Scavenge/Old:可以设置JVM参数,以达到一个可控制的吞吐量(Throughput)
- 控制吞吐量的参数设置:
- -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间
- -XX:GCTimeRatio:设置垃圾收集时间占总时间的比率
- -XX:+UseAdptiveSizePolicy:动态调整eden、from、to的大小
- 场景:基本不用,老年代首选为CMS,不会用Parallel Old,新生代的Parallel Scavenge又不能和CMS搭配使用
10.4、CMS收集器
10.4.1、CMS概述
- CMS: Concurrent Mark Sweep,获取最短回收停顿时间为目标的收集器,第一款真正意义上的并发+并行收集器
- CMS的清理识别算法
- 在多数情况下,使用
标记-清除
算法;少数时候,会使用标记-整理
算法 (碎片过多而无法分配对象时,则使用SerialOld为其兜底) - 垃圾回收过程
- 初始标记:stop-the-world,标记GC Roots直接关联的,让下一阶段用户线程并发运行时,能正确地在可用Region中分配新对象,需要短暂地停顿用户线程
- 并发标记:从GC Root的直接关联对象开始,遍历整个对象图的过程,该阶段耗时较长,用户线程可与GC线程并发运行
- 重新标记:stop-the-world,修正并发标记期间导致的标记变动(用户线程继续运作而导致标记产生变动的部分对象的标记记录)
- 并发清除:清理垃圾对象,由于不需要移动存活对象,GC线程和用户线程并发执行
- 并发重置:重置CMS收集器的数据结构
- 在多数情况下,使用
CMS的优点:
- 低停顿、并发收集,是B/S结构下的老年代收集器的首选,常配合新生代的ParNew收集器应用在JavaWeb中
CMS的缺点:
- 对CPU资源敏感,会和工作线程争抢
- 无法处理浮动垃圾(并发标记和并发清理阶段新产生的垃圾),只能等待下一次GC清理
- 会产生内存碎片
- 执行过程中的不确定性(Concurrent mode failure):Serial Old登场,触发STW,CMS基于"标记-清除"算法实现,GC时会产生大量的空间碎片,可能会无法找到足够大的连续空间来分配对象
- 在GC过程中,Survivor区的对象要晋升,老年代没有空间再存储对象
- 在GC过程中,大对象直接进入老年代,但老年代无法存放下大对象
- 适当降低触发CMS的阀值: -XX:CMSInitiatingOccupancyFraction
10.4.2、三色标记法
- 三色标记法:是一种垃圾回收算法,它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的,JVM中的
CMS、G!垃圾回收器
所使用的垃圾回收算法即为三色标记算法
1)算法思想
三色标记法将对象的颜色分为了黑、灰、白三种颜色
- 白色:尚未访问的对象(一直没被访问,说明最后会被清理)
- 灰色:表明对象已经访问过,但是本对象引用到的其他对象尚未全部访问完(全部访问后,会转换为黑色)
- 黑色:表明对象已经访问过,而且本对象 引用到的其他对象也全部访问过
2)算法流程
原始状态:
-
初始标记:stop-the-world,标记GC Roots直接关联的对象,包括记忆集中的新生代对象
- 1)初始时,所有对象都在
白色集合
中 - 2)将GC Roots直接引用到的对象移动到
灰色集合
中
- 1)初始时,所有对象都在
-
并发标记:并发追溯标记,程序不会停顿
- 3)从灰色集合中获取对象(将本对象引用到的其他对象全部移动到
灰色集合
中,将本对象移动到黑色集合
中) - 4)重复步骤3,直至
灰色集合
为空时,结束垃圾回收的操作 - 5)结束后,仍然在
白色集合
的对象即为GC Roots不可到达的对象,可以进行垃圾回收
重复步骤3(从灰色集合B,C中获取对象)
- 重新标记
原则:能接受“该清理的标记为不清理-漏标”,不能接受“不该清理的标记为该清理-错标”
- 3)从灰色集合中获取对象(将本对象引用到的其他对象全部移动到
-
存在问题
- 发生"错标"时,需要在并发标记过程中同时满足如下两个条件:
1)插入了一条或者多条黑色到白色对象的引用
2)删除了全部从灰色到白色对象的引用
- GC线程:扫描完成了A(A黑),访问到了C(C灰)
用户线程:删除了C->D的引用,新增了A->D的引用
后果:D最终被标记为白色(应该被标记为黑色),白色会被清除(错标)
- 发生"错标"时,需要在并发标记过程中同时满足如下两个条件:
-
解决思路
在并发标记过程中记录其中一种情况即可- 增量更新(CMS的选择):
并发标记阶段:把新插入的引用记录下来[写屏障](记录了A[黑] -> D[白])
重新标记阶段:将记录过的引用关系中的黑色对象为根,再重新扫描一次(从A重新扫描,D变黑), - 原始快照(G1的选择):
并发标记阶段:把删除的引用记录下来[写屏障](记录了C(灰) ->D(白))
重写标记阶段:将记录过的引用关系中的灰色对象为根,再重新扫描一次(把D标记为黑色) - 解决办法优缺点:
增量更新:速度慢,准确,需要扫描所有黑色对象(黑色对象:自身已经访问过且引用到的其他对象也都访问过的的对象),不会产生浮动垃圾
原始快照:速度快(G1追求更短的STW时间),但不准确,会产生浮动垃圾
- 增量更新(CMS的选择):
-
"错标"解决方案为什么G1选择了原始快照[高阶]
- CMS使用标记-清除算法,由于空间碎片可能会导致STW问题,要尽量避免浮动垃圾;G1采用复制算法,浮动垃圾更能接受。
- G1追求更短的STW时间,原始快照的速度更快。
10.5、G1收集器
1)概述
- 基于Region的堆内存布局是G1收集器建立“停顿时间模型”的实现关键,G1不再将固定大小、固定数量的分代区域划分,而是将连续的Java堆划分为多个大小相等的独立区域(Region),每个Region都可以根据需要,来扮演新生代的Eden、Survivor区域或老年代区域
- 时间停顿模型:能够指定在一个长度M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒
- G1收集器之所以能够建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次回收到的内存空间都是Region大小的整数倍,如此,可有计划地避免在整个Java堆中进行全区域的垃圾回收
- G1处理思路是采用复制算法,让G1收集器去跟踪各个Region中垃圾堆积的"价值"大小,价值即回收所获得的空间大小以及回收所需要时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(-XX:MaxGCPauseMillis参数设置,默认是200毫秒),优先处理回收价值最大的Region,以此保证了G1收集器在有限时间内可以得到尽可能高的收集效率
2)垃圾回收过程
- 初始标记:标记GC Roots能直接关联到的对象,让下一阶段用户线程并发运行时,能正确地在可用Region中分配新对象
- 并发标记:从GC Root开始,对堆中对象进行可达性分析,递归扫描整个堆中的对象图,找出要回收的对象,该阶段耗时较长,与用户程序并发执行
- 最终标记:修正并发标记期间,因用户程序继续运行而导致标记发生变化的那部分对象
- 筛选回收:更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来指定回收计划,可选择多个Region构成回收集,再把决定回收的那部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间(涉及到存活对象的移动,需要暂停用户线程,由多条收集器线程并行完成)
3)G1和CMS的对比
- 垃圾碎片少:G1采用复制算法,CMS采用标记-清除
- 使用范围更广:G1可以用于新生代+老年代,CMS用于老年代
- G1的STW更短、更加可控:
- 部分收集:G1优先回收性价比最高的区域,不是整体都回收,可设置预测垃圾回收的停顿时间(-XX:MaxGCPauseMillis=200)
- 在并发标记阶段,G1采用原始快照模式,而CMS采用增量更新的方式
- G1大对象(大于Region大小的50%)单独存储,不占用老年代,而CMS会将其存放在老年代
- 内存使用率更高:
- 每一个Regin的类型不是固定不变的,可以动态调整
4)G1中的GC
- Young GC:Eden区满则触发,同时回收大对象垃圾
- eden到surivor的复制;新生代到老年代的晋升和之前一样
- 跨代引用问题使用记忆集
- Mixed GC:G1收集器独有的行为,目标是收集整个新生代和部分老年代的垃圾,整堆空间占一定比例则触发( -XX:InitatingHeapOccupancyPercent=45)
- Full GC:Mixed GC跟不上程序分配对象内存的速度,导致老年代满,降级为Serial Old