前言
垃圾收集器主要考虑的工作是什么内存该回收、什么时候进行回收以及该怎么样进行回收?
通过之前的内容我们都知道,程序计数器、JVM栈及本地方法栈都是线程私有的,执行完毕自动销毁,不需要过多考虑内存回收问题;而堆和方法区是属于共享的区域,运行时创建的对象等信息都存放在这些区域中,垃圾收集器主要也是关注这部分内存的使用情况。
什么内存该回收
垃圾收集器回收的是一些已经无用的对象,判断对象无用的方法主要有:
-
引用计数法
为新建的对象添加引用计数器,每当有个地方引用它时计数器加1,引用失效时则减1。当回收时,发现计数器为0,则回收此对象所占用的内存。
a.优点:实现简单,效率高
b.缺点:解决不了循环引用的场景。如:
/** * Title: ReferemceCountGC * Description: vm args:-XX:+PrintHeapAtGC * * @author lin.xu * @date 2017/12/4. */ public class ReferemceCountGC { public Object instance = null; private static final int _1MB = 1024 * 1024; private byte[] bigSize = new byte[2 * _1MB]; public static void main(String args[]) { ReferemceCountGC objA = new ReferemceCountGC(); ReferemceCountGC objB = new ReferemceCountGC(); objA.instance = objB; objB.instance = objA; System.gc(); } }
-
可达性分析法
JVM根据选定的“GC Roots”,以“GC Roots”作为起点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当对象和“GC Roots”之间没有引用链的时候,则表示对象不可达。
不可达的对象会被进行第一次标记并进行筛选,筛选的条件是对象是否有必要执行
finalize()
方法。若没有覆盖finalize()
方法或者finalize()
方法已执行过,对象被第二次标记将等待下次回收。若对象覆盖finalize()
方法,对象将被放进F-Queue队列中,由JVM开启低优先级的Finalizer
线程去执行,稍后GC将会在F-Queue中进行第二次标记。如果对象在finalize()
方法中与引用链上的对象建立了联系,则被移除“即将回收”的集合。Java语言中,可以选择如下对象作为“GC Roots”:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中类常量引用的对象;
- 本地方法栈中JNI引用的对象
对于判定对象是否可用与“引用”也有关。传统的将引用分为有引用与无引用,但是有些对象是可能接下来会有用的。所以JVM希望当内存空间足够时,则保留这部分对象,如果进行回收后,内存还是不够的话,则回收这些对象。Java对引用进行了扩充,分为如下几种:
-
强引用(Strong Reference)
类似
Object obj = new Object()
这类的引用,只要存在,收集器就不会回收此对象 -
软引用(Soft Reference)
用来描述还有用但并非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收
-
弱引用(Weak Reference)
用来描述非必需对象。无论内存是否足够,都会回收弱引用关联的对象
-
虚引用(Phantom Reference)
称为幽灵引用或幻影引用。无法通过虚引用获取对象实例,其唯一目的是能在这个对象被收集器回收时获取系统通知
方法区也是可以进行对象回收的,其回收内容包括废弃常量和无用的类。但相较于新生代,回收效率很低。回收废弃量与Java堆中的对象回收类似,但是针对类的回收就要严格很多:
- 该类所有的实例均已被回收,即Java类中不存在该类的任何实例;
- 加载该类的
ClassLoader
已经被回收; - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法通过反射来访问该类的方法
怎样进行回收
首先我们来看看回收的算法。
垃圾收集算法
-
标记-清除算法
分为标记和清除两个步骤。首先标记需要回收的内存空间,之后将标记的内存空间进行回收。
不足之处:效率问题,标记和清除效率都不高;空间问题,清除过程中由于未整理内存空间,使得内存空间里很多碎片空间,存储大对象时可能不得不进行一次Full GC操作。
-
复制算法
将内存一分为二,每次只使用其中一半。当其中一半内存快使用完时,将还存活的对象复制到另一半中内存中,把已使用的内存清空。通常用于新生代的回收。
-
标记-整理算法
对内存进行标记,将存活对象都向内存一端移动,然后直接清理存活对象边界外的内存空间。
-
分代收集算法
根据内存区域进行不同的垃圾回收算法。新生代只有少量内存存活可以采用复制算法;老年代回收效率低可以采用标记-清除或者标记-整理算法。
HotSpot虚拟机的算法实现
-
枚举根节点
难点:
- 逐个检查GC Roots到对象的引用会很耗时;
- 可达性分析对执行时间的敏感还体现在GC停顿上。因为在枚举根节点时,为了保持一致性,需要停顿所有的Java执行线程。
解决办法:
HotSpot在类加载完成后,采用OopMap的数据结构存储对象内什么偏移量上是什么类型的数据,在JIT编译过程中,也会在特定的位置记录栈和寄存器中哪些位置是引用。GC扫描时可以依据OopMap来获取信息。
-
安全点(Safe Point)
有了OopMap可以快速枚举出根节点,但是不能为每个指令生成OopMap,否则开销太大。HotSpot是在特定的位置记录OopMap信息,这个特定的位置就指安全点。GC只有等所有线程执行到安全点的时候才会停止用户线程的执行。
需要考虑的问题是如何让线程跑到安全点时停止下来。两种方案可选:
-
抢先式中断(Preemptive Suspension)
GC发生时,所有线程全部中断,如果有线程不在安全点中断,则恢复让其继续执行至安全点。现几乎没有虚拟机采用这种方式。
-
主动式中断(Voluntary Suspension)
GC需要执行的时候,会在安全点设置个标志。当线程执行时,都去轮询这个标志,发现中断标志为真时就主动挂起。
-
-
安全区域(Safe Region)
安全点解决了运行时的线程GC问题。但是某些线程可能处在Sleep状态或者Block状态,那它不能响应中断的请求。这样HotSpot采用安全区域解决。安全区域是指在一段代码片段之中,对象引用关系不会发生变化,这个区域任何地方都是GC安全的。
具体过程就是:线程执行进入了Safe Region区域代码中,标志自己进入安全区域。当JVM发起GC时,忽略处于Safe Region的线程。当线程需要离开安全区域,首先要检测GC是否完成,没完成需要继续等待,直到收到可以离开Safe Region区域的信号为止。
垃圾收集器
-
Serial收集器
最基本、最悠久的收集器。用于新生代,单线程的收集器,需要Stop The World。适用于运行在Client模式下的JVM。
参数 | 描述 |
---|---|
UseSerialGC | 使用Serial + Serial Old的收集器组合进行内存回收 |
SurvivorRatio | 新生代中Eden与Survivor区域的容量比值。默认为8,代表Eden:Survivor=8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小。设置这个参数后,大于这个参数的对象将直接分配到老年代 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持一次Minor GC后,年龄加1,当超过这个参数值则进入老年代 |
HandlerPromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
-
ParNew收集器
Serial收集器的多线程版本。用于新生代,新生代回收并行,老年代回收串行;新生代复制算法,老年代标记-整理算法。适用于CPU多核的情形。
参数 | 描述 |
---|---|
UseParNewGC | 使用ParNew + Serial Old的收集器组合进行内存回收 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
SurvivorRatio | 新生代中Eden与Survivor区域的容量比值。默认为8,代表Eden:Survivor=8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小。设置这个参数后,大于这个参数的对象将直接分配到老年代 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持一次Minor GC后,年龄加1,当超过这个参数值则进入老年代 |
HandlerPromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
-
Parallel Scavenge收集器
类似于ParNew收集器,主要关注吞吐量。与ParNew收集器重要区别是:提供自适应的调节策略。当设置UseAdaptiveSizePolicy后,JVM会根据监控信息,动态调整新生代大小、Eden与Survivor比例、晋升老年代对象大小等参数以提供更合适的GC停顿时间或者最大的吞吐量。适合用于后台运算不需要太多交互的任务。
参数 | 描述 |
---|---|
UseParallelGC | 虚拟机运行在Server模式下的默认值。打开此开关后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收 |
MaxGCPauseMillis | 设置GC的最大停顿时间 |
GCTimeRatio | GC时间占总时间的比率,默认值为99,即允许1%的GC时间 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
SurvivorRatio | 新生代中Eden与Survivor区域的容量比值。默认为8,代表Eden:Survivor=8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小。设置这个参数后,大于这个参数的对象将直接分配到老年代 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持一次Minor GC后,年龄加1,当超过这个参数值则进入老年代 |
HandlerPromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
UseAdaptiveSizePolicy | 动态调和智能Java堆中各个区域的大小以及进入老年代的年龄 |
-
Serial Old收集器
老年代、单线程收集器,采用“标记-整理”算法。主要适用于Client模式下;如果在Server模式,一是作为Parallel Scavenge收集器搭配适用;一是当CMS在并发收集发生Concurrent Mode Failure时作为后备预案使用
-
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,多线程收集器,采用“标记-整理”算法。适用于注重吞吐量及CPU资源敏感的场合。
参数 | 描述 |
---|---|
UseParallelOldGC | 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收 |
-
CMS收集器
CMS基于“标记-清除”算法实现,老年代收集器。适用于重视服务的响应速度,希望系统停顿时间越短,以给用户更好体验的场景。运作过程分以下4步:
- 初始标记:标记GC Roots能直接关联到的对象。需要STOP THE WORLD;
- 并发标记:GC Roots Tracing的过程,依据GC Roots找出存活的对象;
- 重新标记:修正并发标记过程中由于用户线程继续执行而导致标记变动的部分。需要STOP THE WORLD;
- 并发清理
优点:
并发收集、低停顿缺点:
- 对CPU资源非常敏感。CMS启动回收的线程数是(CPU数量+3)/4,当CPU数量很少时,回收线程占用的资源越多,用户线程就占用资源越少;
- 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次的Full GC。并发清理时用户线程仍在执行,那就可能有垃圾产生。这部分垃圾(浮动垃圾)只能在下次收集过程中清理掉;在并发标记时还有用户线程在执行,老年代中就可能被存入对象,如果老年代内存不足,则会抛出“Concurrent Mode Failure”,这时会临时启动Serail Old收集器来进行老年代的垃圾收集。
- 由于采用“标记-清理”算法,会产生大量空间碎片。
参数 | 描述 |
---|---|
UseConcMarkSweepGC | 打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代空间被使用多少后触发垃圾回收。默认为68%。 |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理 |
CMSFullGCsBeforeCompaction | 设置CMS在进行若干次垃圾收集后再启动一次内存碎片整理 |
-
G1收集器
G1是一款面向服务端应用的垃圾收集器,是当今收集器技术发展的最前沿成果之一。
与其他收集器所不同的是,G1收集器是将堆划分为多个Region,每个Region中分配着新生代和老年代(不一定是连续空间)。G1收集器运作大致步骤如下:
-
初始标记(Initial Marking)
标记下GC Roots能直接关联的对象,并修改TAMS(Next Top at Mark Start)值,让下一阶段用户线程并发执行时知道在正确的Region中存放对象。需要停止用户线程。
堆中每个Region都会维护一个Remembered Set,当有Reference写入时,检查引用的对象是否在同个Region中。如果是,则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。这样在枚举根节点时可以不对全堆进行扫描。
-
并发标记(Concurrent Marking)
从GC Roots出发,找出可到达的对象。
-
最终标记(Final Marking)
修正在并发标记过程中由于用户线程继续执行导致变动的对象。JVM将并发标记过程中存储在Remembered Set Logs中的记录与Rembered Set合并,重新计算可到达的对象。并行执行。
-
筛选回收(Live Data Counting and Evacuation)
对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。可并发并行。
G1收集器特点如下:
- 并发与并行:并发标记,最终标记中的并行标记,并行回收
- 分代收集:新生代、老年代依然存在于G1收集器中
- 空间整合:整体上采用“标记-整理”算法,局部看(两个Region之间)采用复制算法
- 可预测的停顿:G1在垃圾回收时会依据用户指期望的停顿时间来选择回收效率最高的Region
-
内存分配与回收策略
-
对象优先在Eden分配
-
大对象直接进入老年代
对象大于PretenureSizeThreshold参数指定大小的对象直接进入老年代
-
长期存活的对象将进入老年代
对象年龄超过MaxTenuringThreshold参数指定大小的对象直接进入老年代
-
动态对象年龄判断
如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象直接进入老年代
-
空间分配担保
在发生Minor GC之前,需要判断老年代可用连续空间是否大于新生代所有对象总空间。如果大于,直接进行Minor GC。如果不成立,则判断是否设置了空间担保(HandlePromotionFailure),若设置了空间担保,则判断老年代可用空间是否大于历次存储老年代的平均值,大于的话则进行MinorGC,不大于或者没有设置空间担保,需要进行Full GC。