GC需要完成的三件事情
- 哪些对象需要回收?
- 什么时候回收?
- 如何回收?
一、那些对象需要回收?(如何确定对象已死)
对象已死:不可能再被任何途径使用的对象
1.引用计数法
引用计数法:给对象添加一个引用计数器,每当一个地方引用它,计数器就加一;当引用失效时,计数器就减一;任何时刻计数器为0的对象就是不可能再被使用的。
问题:难以解决对象间相互引用的问题,所以主流JVM里面都没有选择该方法;
2. 可达性分析
可达性分析:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
GC Roots包括
- 虚拟机栈(栈中本地变量表)中引用的对象,即当前执行方法中的对象
- 方法区中静态属性引用的对象,即 使用static修饰的类属性,所以可使用static修饰的容器放置程序执行上下文一类的属性
- 方法区中常量引用的对象,即 使用final修饰的对象
- 本地方法栈中JNI/Native方法引用的对象
二、如何回收?(垃圾收集算法)
1. 标记-清除算法
首先标记出所有需要被回收的对象,在标记完成后统一回收所有被标记的对象
- 问题:
- 效率问题:标记和清除两个过程的效率都不高;
-
空间问题:标记清除后会产生大量的内存碎片
标记-清除算法.png
2. 复制算法
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另一边,然后再把已使用过的内存空间一次清理掉
HotSpot虚拟机默认Eden和Survivor的大小比例为8:1
3. 标记-整理算法
首先标记出所有需要被回收的对象,标记完成后让所有存活的对象都向一端移动,然后清理掉端边界以外的内存
4. 分代收集算法
一般把Java堆分为新生代和老年代,这样就可以根据各个代的特点采用最适合的收集算法
- 新生代:使用复制算法收集,因为每次垃圾收集都会有大量的对象死去,只有少量存活,只需要付出少量存活对象的复制成本就可以完成收集;
- 老年代: 使用标记-整理算法或标记-清除算法,因为老年代中因为对象存活率高,、没有额外空间对它进行分配担保;
三、HotSpot的算法实现
已可达性分析算法为例,第一步首先找到所有的根节点——遍历根节点
1. 枚举根节点
- Stop the World:指在进行垃圾回收的时候,需要将所有正在执行的Java线程全部停止,以保证分析的准确性;
- 目前的主流JVM使用的都是准确式GC(即JVM可以知道内存中某个位置具体是什么类型),所以JVM不需要遍历所有的执行上下文和全局引用(因为GC Roots主要存在与这两个地方)的位置。
- 在HotSpot实现中,是使用一组称为OopMap的数据结构达到准确式GC的目的
2. 安全点(如何完美进入GC)
因为对象引用变化的情况有点多,对象引用的变化同样引起OopMap内容的变化,因为OopMap是在特定的位置存储这些信息的,所以如果每次OopMap变化都存储,那将会需要大量的额外空间,这样GC的成本会变得很高。所以HotSpot只有在“特定的位置”记录这些信息,这些位置称为安全点;(即程序在执行时并非在所有地方都能停顿下来开始GC,只有到达安全点才能暂停)
- 安全点选取标准:
- 基本以程序“是否具有让程序长时间执行的特征”为标准进行选定
- 因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因过长时间运行,“长时间执行”的最明显特征就是指令序复用,如方法调用、循环跳转、异常跳转等
- 让所有线程在安全点停顿
- 抢占式中断:首先把所有线程全部中断,如果有发现线程中断的地方不在安全点上,就恢复线程,让其运行到安全点。(现在几乎没有JVM采用过这种方法)
- 主动式中断:当GC需要中断时,不直接对线程操作,而是设置一个标志(告诉线程需要中断的标志),各个线程在执行时会主动轮询这个标志,当为true时,自己就中断挂起
3. 安全区域
当线程sleep或者阻塞状态时,这时线程无法响应JVM的中断响应,对于这种情况,就需要安全区域来解决
四、垃圾收集器
虽然会对各个垃圾收集器比较,但是并不是要选出一个最好的收集器。因为到目前为止,还没有
最好的垃圾收集器出现。
-
Serial收集器
Serial_Serial Old收集器运行示意图.png- 新生代收集器
- 使用复制算法
- 单线程的收集器
- 在进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束(stop the world)
- 优点:简单高效(与其他收集器相比),对于单CPU环境能获得更好的效率
-
ParNew收集器
ParNew_Serial Old收集器运行示意图.png- 新生代收集器
- 使用复制算法
- Serial收集器的多线程版本,除使用多条线程进行垃圾收集之外其余都和Serial收集器一样
- 是许多运行在Server模式下的虚拟机首选新生代收集器,主要原因是除Serial外只有它可以和CMS搭配使用
-
Parallel Scavenge收集器(示意图如Paraller Old)
- 新生代收集器
- 使用复制算法
- 关注点与其他收集器不同,目标为达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))
- 提供-XX:MaxGCPauseMillis(控制最大垃圾收集停顿时间)和-XX:GCTimeRatio(直接设置吞吐量大小)
- 提供-XX:+UseAdptiveSizePolicy动态根据JVM运行情况调节新生代及老年代的内存分配
-
Serial Old收集器(示意图如Serial)
- 老年代收集器
- Serial收集器的老年代版本,同样为单线程
- 使用标记-整理算法
-
Paraller Old收集器
Paraller Scavenge_Paraller Old收集器运行示意图.png- 老年代收集器
- Paraller Scavenge收集器的老年代版本
- 使用标记-整理算法
-
CMS收集器(Concurrent Mark Sweep)
CMS收集器运行示意图.png- 老年代收集器
- 使用标记-清除算法
- 目标是获取最短回收停顿时间,目前很大一部分集中运用在互联网站或者B/S系统的服务端上
- 整个过程分为四个步骤(前四个):
- 初始标记:仅仅标记一下GC Roots能直接关联到的对象(即GC Roots的一级节点的对象)
- 并发标记:GC Roots Tracing(根据初始标记的所有对象向下标记对象),与用户程序并行
- 重新标记:修正并发标记期间因为用户程序继续运作而导致变动的那一部分对象的标记记录
- 并发清除:并发清除无用对象,与用户程序并行
- 重置线程:重置CMS的数据结构(相当于让垃圾清理线程回到第一个步骤准备下一次清理)
- 优点:耗时比较长的并发标记和并发清除都与用户线程并行,所以停顿时间会减少很多;
- 缺点:
- 对CPU资源非常敏感:虽然在并行清除、标记等过程中不会停止用户进程,但是CMS同样也占用了CPU资源,在CPU资源不是很充足的时候,会对用户进程影响很大;
- CMS无法处理浮动垃圾:由于并发清理时用户进程也在运行,伴随用户进程运行还会产生新的垃圾(浮动垃圾),但是在当此的垃圾收集中无法进行收集,只能在下一次收集中进行收集。因为有浮动垃圾的产生,所以CMS在垃圾收集中需要预留一些空间为并发程序使用。
- 使用标记-清除算法:产生内存碎片
-
G1收集器(Garbage First)
G1收集器运行示意图.png
- 将整个堆分成多个大小相等的独立区域(Region),虽然保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region的集合。
- G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间优先回收价值最大的Region。
- Region之间的对象引用以及其他收集器中新生代与老生代之间对象引用,虚拟机是使用Remenbered Set来避免全堆扫描的。
- 收集区域为整个堆内存,但是也保留新生代和老年代的特征
- 整体看为标记-整理算法、局部(两个Region)上看是复制算法实现
- 优点:
- 并行和并发:可充分利用多CPU、多核环境下的硬件优势
- 分代收集:不需要和其他收集器配合即可收集整个堆内存
- 空间整合:整体看为标记-整理算法、局部(两个Region)上看是复制算法实现,不会产生内存碎片
- 可预测的停顿:可指定一个长度的M毫秒时间段内,垃圾回收时间不得超过M毫秒
*整个过程
五、内存分配与回收策略
对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存(TLAB),将优先在TLAB上分配;
Minor GC:指发生在新生代的垃圾收集动作
Major GC/Full GC:指发生在老年代的垃圾收集动作
1.对象优先在Eden分配
- 大多数情况下,对象在新生代Eden区中分配。当Eden没有足够的空间进行分配时,虚拟机将发出一次Minor GC
- JVM提供-XX:+PrintGCDetails参数打印GC日志
2. 大对象直接进入老年代
大对象:所谓大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。在写程序时应当避免创建出这种大对象,因为它可能会导致JVM提前进行GC
- JVM提供了-XX:PretenureSizeThreshold参数,使大于这个设置值的对象直接在老年代分配内存。(只对Serial和ParNew两个收集器有效)
3. 长期存活的对象将进入老年代
JVM给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Servivor中,并将对象年龄设置为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),将会被晋升到老年代中。
- 对象晋升老年代的年龄阈(yu【四声】)值,可以通过参数-XX:MaxTenuringThreshold设置。
3. 动态对象年龄的判定
为了能更好的适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
4.空间分配担保
在发生Minor GC之前,JVM会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确认是安全的。
如果不成立,则看HandlerPromotionFailure设置值是否允许担保失败。如果允许,那么会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试进行一次Minor GC,如果小于,或者HandlerPromotionFailure设置为不允许冒险,那这时也要改进为一次Full GC。
不过在JDK1.6之后,HandlerPromotionFailure参数已经不会影响到JVM的空间分配担保策略。规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则进行Full GC。