Java垃圾回收机制

Java垃圾回收机制
对象被判定为垃圾的标准
没有被其他对象引用
垃圾回收之标记算法(如何判断没有被其他对象引用?)
引用计数算法
可达分析算法
引用计数算法
通过判断对象的引用数量来决定对象是否可以被回收
每个对象实例都有一个引用计数器,被引用+1,完成引用则-1
任何引用计数器为0的对象实例可以被当做垃圾收集
堆中的每个实例都有一个引用计数器,当一个实例对象创建时分配给一个引用变量,则引用计数器被设置为1,若再被另一个变量引用,引用计数器再+1。若该对象的实例引用超过生命周期或被设置成新值,引用计数器会-1。
比如再某一个方法里定义了引用变量指向该对象实例,当方法结束时由于该变量是局部变量,存储在java虚拟机栈上,方法结束时栈会销毁。此时该引用计数器会-1。
优点:
执行效率高,程序执行受影响小
缺点:
由于实现过于简单,无法检测出循环引用的情况,导致内存泄漏。(如父对象有对子对象的一个引用,子对象反过来引用父对象,此时引用计数器永远不会为0)
JVM没有明确表示用哪种垃圾回收算法,因这个短板,并从表现来看未采用此种算法。
可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收
可达性算法是从离散数学中的图论来引入的,程序把所有引用关系看做是一张图,通过一系列的称为 GC Roots 的对象作为起点,然后向下搜索。搜索所走过的路径称为引用链 (Reference Chain), 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达,也就说明此对象是不可用的。

如图所示垃圾回收器对整个对象图进行遍历,从GC Root开始沿着引用链向下寻址,回收器将所有访问到的对象标记为存活(蓝色可达为存活,灰色不可达为)。回收器将会再某一时刻将不可达对象清除。
什么对象可以被作为GC根
虚拟机栈中引用的对象(栈帧中的本地变量表)
比如在java方法里new了一个对象并赋值给了一个局部变量,那么在该局部变量未被销毁之前,该对象可以被作为GC Root。
方法区中的常量引用的对象
比如在类里面定义一个常量,该常量保存的是某个对象的地址,被保存的对象成为GC的根对象,当别的对象引用到它就会形成上图关系。
方法区中的类静态属性引用的对象
本地方法栈中JNI(Native方法)的引用的对象
活跃线程的引用对象
垃圾回收之回收算法
标记-清除算法(Mark And Sweep)
标记:从根集合进行扫描,对存活的对象进行标记
清除:对堆内存从头到尾进行线性遍历,回收不可达对象

缺点
效率问题:标记和清除过程的效率都不高;
空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World。
复制算法
分为对象面和空闲面
对象在对象面创建
对象面内存用完时将存活的对象从对象面复制到空闲面里
将对象面所有对象内存清除

多数商用JVM年轻代采用此种算法(年轻代对象存活率仅为10%)
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记-整理算法

标记:从根集合进行扫描,对存活的对象进行标记
清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。 

分代收集算法
垃圾回收算法的组合拳
按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
目的:提高JVM的回收率
 GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。
   “分代收集”算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

年轻代(Minor GC)
发生在年轻代中垃圾收集动作,采用的复制算法。年轻代是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命,具有有朝生夕死的性质。
年轻代:尽可能快速地收集掉那些生命周期短的对象
Eden区
两个Survivor区

当系统创建一个对象时,这个对象的年龄也被确定了(0岁),总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区,这时From区的对象增加1(1岁)。
这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区(年龄继续加1)。
再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。
经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(默认阈值15),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。
老年代(Full Gc)
对象如何晋升到老年代
经历一定Minor次数依然存活的对象
Survivor区中存放不下的对象
新生成的大对象(-XX:+PretenuerSizeThreshold)

常用的调优参数
-XX:SurvivorRatio :Eden与两个Survivor的比值,默认 8:1:1
-XX:NewRatio:老年代和年轻代内存大小比例,默认 2:1
-XX:MaxTenuringThreshold:对象从年轻代晋升到老年代经过GC次数的最大阈值

老年代:存放生命周期较长的对象,采用标记清除算法或者标记整理算法。
Full GC清理整个堆空间,包括年轻代和永久代。

垃圾收集器
Stop-The-Word
JVM由于要执行GC而停止了应用程序的执行
任何一种GC算法中都会发生
多数GC优化通过减少Stop-the-world发生的时间来提高程序性能(提高吞吐量,低停顿)
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
SafePoint
分析过程(可达算法分析)中对象引用关系不会发生变化的点
产生Safepoint的地方:方法调用;循环跳转;异常跳转等
安全点数量适中
Jvm的运行模式
Server(启动速度较慢,进入稳定期长期运行时,比Client运行快。(重量级虚拟机,采用了更多的优化))
Client(轻量级)

查看JVM运行模式命令 java -version
常见的垃圾收集器及之间的关系

新生代垃圾收集器
Serial收集器(-XX:+UseSerialGC,复制算法)

单线程收集,进行垃圾收集时,必须暂停所有工作线程
简单高效,Client模式下默认的年轻代收集器

Serial收集器是最基本、历史最悠久的垃圾收集器。它是一个单线程收集器,“单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是 它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
Serial 收集器由于没有线程交互的开销,可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。

ParNew收集器(-XX:UseParNewGC,复制算法)
ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为(控制参数、收集算法、分配规则、回收策略等等)和 Serial 收集器完全一样。

除了支持多线程收集,ParNew 相对 Serial 似乎并没有太多改进的地方。但是它却是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。ParNew单核状态下不如Serial,多核线程下才有优势。

多线程收集,其余的行为、特点和Serial收集器一样
单核执行效率不如Serial(存在线程交互开销),在多核下执行才有优势(默认开启收集线程数与CPU数一样)

Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
Parallel Scavenge 收集器关注点是吞吐量(CPU运行代码的时间与CPU总消耗时间的比值)。 而CMS 等垃圾收集器的关注点更多的是缩短用户线程的停顿时间(提高用户体验)。停顿时间越短就越适合和用户进行交互(响应速度快,可以优化用户体验),而高吞吐量则可以高效的利用CPU时间,尽快完成用户的计算任务。

Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,可以选择把内存管理优化交给虚拟机去完成。

比起关注用户线程停顿时间,更关注系统的吞吐量
在多核下执行才有优势,Server模式下默认的年轻代收集器

老年代收集器
Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

单线程收集,运行垃圾收集时,必须咋暂停所有工作线程
简单高效,Client模式下默认的老年代收集器
Parallel Old收集器(-XX:+UseParalleOldGC,标记-整理算法)
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

多线程,吞吐量优先
CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常重视服务的响应速度,以期给用户最好的体验。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。

初始标记:stop-the-world
并发标记:并发追溯标记,程序不会停顿
并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
并发清理:清垃圾对象,程序不会停顿
并发重置:重置CMS收集器的数据结构

CMS一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

对 CPU 资源敏感
无法处理浮动垃圾;
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间 碎片产生。
共用垃圾收集器
G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,开发人员希望在未来可以换掉CMS收集器,它有如下特点
并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。这就意味着不会产生大量的内存碎片
可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1 收集器的运作大致分为以下几个步骤:
初始标记
并发标记
最终标记
筛选回收

G1收集器将整个Java堆内存划分为若干个内存大小相等的Region,年轻代和老年代不再物理隔离,他们都是一部分Region的集合。

posted @ 2019-11-02 19:47  vipzk  阅读(100)  评论(0)    收藏  举报