JVM优化回收算法
Posted 段王爷的府邸
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM优化回收算法相关的知识,希望对你有一定的参考价值。
前言
我们在日常开发中,无需关心内存的管理工作,因为这些操作JVM已经帮我们实现了,但是如果我们开发不严谨,就会有无效的对象一直占用着内存资源,最终导致OOM,所以当出现此类问题时候,我们难以入手,不知道从哪搞起,所以针对JVM的内存回收就尤为显得重要了。
什么是垃圾对象?
我们前面提起过堆是垃圾回收的主要战场,那么JVM是如何判断哪些是垃圾呢?我们现在大概应该知道堆中保存着java中几乎所有的对象实例,我们在进行对这些对象回收前,首先我们肯定要确定这些对象哪些是无效的(也就是可以回收的),哪些是不可以回收的。所以对于JVM自动化内存的管理,要有一套专门的算法来判断这些对象实例的“生”与“死”。
对象存活判定算法之引用计数算法
这个算法可以大部分人都比较了解,因为笔者也是认为这个算法是JVM中垃圾回收的算法。据了解此算法已经诞生50多年了,属于历史比较悠久的算法了,此算法见名知意,就比如对象A,任何某个对象对A的引用,那么的对象A的计数器就+1,当引用失效的时候就对应着-1,如果此对象的计数器为0,那么此对象就可以被认为无效对象。也就是说可以被垃圾回收的对象。
注:这一点在面试的时候会被提及到,但是需要注意的是,此算法并没有在HotSpot中所使用,为什么呢?我跟大部分读者一样,感觉上面好像没毛病。但是JVM并没有采用此算法进行内存管理(至于什么地方使用了此算法进行垃圾回收,不是本篇所涉及内容,大家自行了解),至于为什么没有使用此算法,最大的问题是因为它解决不了循环引用的问题,如下
通过我画的上面的图,我们可以看到,如果把a、b对象实例置为null,但是堆中的两个对象实例还存在的引用问题,也是就说他们的计数器都不为0,那么这两个对象将不会被回收。
对象存活判定算法之可达性分析算法
什么是可达性分析算法?笔者认为此算法的精髓在于可达性。此算法通过GC Roots作为顶点,向下搜索,从GC Roots到下一个节点的路径就是引用链,那如果GC Roots跟下一个节点没有引用链,那就说明这个节点不可达,我们可以将这个节点作为我们的对象实例,那么什么是GC Roots呢?所谓“GC roots”,或者说引用链的“根集合”,就是一组必须活跃的引用。 注意,是一组必须活跃的引用,不是对象。
可以作为GC Roots的引用包括此下几种。
所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;也就是当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
静态数据结构里指向GC堆里的对象的引用,我们使用了static修饰的变量
本地方法栈,与虚拟机栈同理,只不过本地方法栈是调用的native方法
我们拿虚拟机栈来举例说明为什么可以作为GC Roots的引用,我们在方法中创建对象的时候,除了在堆中创建此对象实例,还会在此方法的栈帧的局部变量表中保存此对象实例的引用,一旦此方法调用结束并弹栈,那么所对应的引用也会随之清除,相反如果栈帧局部变量表中有指向堆中对象实例的引用,说明此对象是有效的。所以虚拟机栈中对象的引用链可以作为GC Roots如下所示
注:我们所常知的HotSpot就是使用的可达性算法,我们在从图1来看,即使a、b的引用都为null,那么作为GC Roots, 堆中的两个对象实例并不是可达对象,所以可以正常被回收。我们也可以通过代码来证明这一点,如下:
我们可以看到,年轻代被回收了3731K,所以是说明HotSpot并没有采用我们开始所述的引用计数算法来判断对象的生死,这一点如果在面试时被问起,一定不能搞混!
对象重生问题
其实即使上述可达性算法分析出来此对象是不可达的,也不一定会马上回收,因为要真正的回收一个对象,是要经过两次标记的,也就是说第一次分析没有引用链之后,会判断当前对象是否覆盖了 finalize方法, 若未覆盖,则直接将其回收。 若对象未执行过finalize方法将其放入队列,由低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,会再次判断该对象是否可达,若不可达,则进行回收,否则,对象重生。我们通过示例来看一下对象的重生问题。
垃圾回收算法
此上所述只能代表它们可以分析应该回收哪些对象,但是没有承担垃圾回收的重任
注:常见的垃圾回收算法有:标记清除算法,复制算法,标记压缩(整理)算法,分代算法。
标记清除算法
见名知意,标记跟清除嘛,此算法可以分为两个阶段,一阶段:从根节点起始标识引用对象,二阶段:清理未被标记的对象,但是需要注意的是,标记的过程,需要STW(Stop The World),也就要暂停程序,为什么呢?因为不这样的话,在标记的过程中,会导致标记的状态不一致的发生,就可能导致把不需要回收的对象回收掉。 我们通过示例图来直观的看下一二阶段要做的事情
在内存中清理的效果如下
通过此上分析,我们可以看到,标记清楚算法缺点是它需要扫描对象做标记,如果对象数量比较庞大,那么这个效率是很慢的,再者就是他会STW,导致程序暂停,但是最关键的是,我们从内存清理凶过来看,在清理完毕之后会存在碎片化的问题,这种清理出来的空闲内存是不连续的,像这种情况,如果有个对象无法申请到连续的空闲内存,就不得不提交触发GC,甚至又会导致STW,这样是不可取的,但是这种垃圾回收算法是基础的,我们后续要介绍的算法也是基础这样思路来改进的.
复制算法
此算法就是将可用内存划分为两块大小相等的区域,每次只使用一块区域,当这个区域的空间使用完了,就把当前区域还存活的对象复制到另一个区域。此算法相比较于标记清除,不会产生碎片化的问题,因为每次它会将其中的一个区域作回收,我们通内存清理的效果图来看一下。
但是此算法也会存在效率问i题,假如说我们此次操作的一批对象是存活概率比较高的,那么就会存在两个区域多次复制的问题, 我们可以通过 XX:MaxTenuringThreshold来设置,默认15还有一点就是这种算法也会比较浪费资源,因为他会将可用内存空间缩小为1/2。
在HotSpot中,此算回收算法用来收集年轻代,因为在年轻代的大部分对象都是将要被回收的,所以没有必要全部按1/2划分内存空间,HotSpot将年轻代划分为1个Eden区,和两个Survivor区,它们之间的比例是8:1:1,这样只有1/10的内存空间会被浪费,但是同时也保证不了,每次都会有1/10的对象存活,如果一旦大于Survivor区可用内存不足的时候,这就是就需要年老代做担保,这个后续我们会讲到。
标记压缩(整理)算法
上面所述的复制算法如果操作存活率高的对象就会导致效率变低,还浪费内存空间,所以复制算法将不会被采用到年老代的回收算法,年老代采用的是标记压缩算法,它的原理跟标记清除基本一致,但是标记压缩算法在标记完成之后,不会直接采用将垃圾对象清除的方式,而它是让存活的对象都压缩到内存空间的一端,然后再清除存活对象边界以外的垃圾对象。如下
分代收集算法
我们在前几个章节也提起过,JVM将堆内存分为年轻代跟年老代,这样我们就能根据每块划分区域的特性来使用最适合的算法,有句老话怎么说来着?适合自己的才是最好的。哈哈哈~因为年轻代都是新创建的对象,大部分对象都会被回收掉,所以年轻代采用的是复制算法,而年老代都是存活率高的对象,所以它采用的是标记压缩算法
------------------------------------手动分割线-------------------------------------------
ps:笔者也是菜鸡一枚,本篇中只讲述了其原理,没有对实现的细节进行讲解,这个后续如果有时间,笔者可以花时间整理下,大家如果有兴趣也可以自行了解,还有本篇中只是描述了常见的回收算法,当然在各种虚拟机、平台下所采用的算法可能不一致,比如三色标记回收等等,它们的基础原理其实都大差不差的,这个感兴趣的也可以自行了解下即可。
下一篇我们来讲述JVM实现的垃圾收集器哦
以上是关于JVM优化回收算法的主要内容,如果未能解决你的问题,请参考以下文章