vlambda博客
学习文章列表

JVM(三)垃圾回收机制

1.JVM内存分配

    jvm内存分配:

    第①步:看是否满足逃逸分析,符合逃逸分析的对象在栈中分配,不参与GC,随着栈桢的回收而销毁。

    第②步:若开启了-XX:+UseTLAB(默认开启,-XX:TLABSize=size指定大小),并且空间足够会优先分配在tlab(Eden区);

    第③步:在串行收集的情况下:-XX:+UseSerialGC 通过配置-XX:PretenureSizeThreshold=x(直接入老年代的阈值,单位byte),分配对象空间如果超过阈值x,对象将直接放入老年代(此参数在并行GC无效)。

    第④步:对象优先分配在Eden区,当Eden区空间不足,触发GC,GC后内存还是不够,内存担保机制会将对象直接放入老年代。

    对象在Eden区域分配内存,当Eden区满时,发生Minor GC,Eden区和From区还存活的对象将被复制进入to区( 此时为空,连续复制, from、to只是逻辑上的概念 ),清空from区(下次再开始GC,from就是上次GC的to区)。并且存活对象的年龄还会加1(初始年龄为1),当它的年龄增加到一定程度(默认为15,CMS GC年龄默认为6),就会被晋升到老年代中。可以通过参数-XX:MaxTenuringThreshold来设置。
    那么如果达不到设置年龄就不会放入老年代吗?
    答案是错误的。先熟悉下这个参数-XX:TargetSurvivorRatio=50(目标生存比例默认50%),再来谈谈 GC动态年龄算法 :(老规矩还是画图)

    Hotspot JVM遍历所有对象时,按照年龄从小到大对其所占用的大小进行累加,当累加的某个年龄大小超过了survivor区的TargetSurvivorRatio(默认50%)时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。参见源码:

JVM(三)垃圾回收机制

    我们再通过一段简单的代码以及Java VisualVM 的Visual GC插件(需要安装)来观察GC的情况。

设置参数:-XX:SurvivorRatio=3 -Xmn40m -Xms300m -Xmx300m

JVM(三)垃圾回收机制

可以观察到年龄标记会逐渐增大:

JVM(三)垃圾回收机制

Visual GC的情况:

JVM(三)垃圾回收机制


2.垃圾回收机制

    GC即是Java垃圾回收机制。目前主流的JVM(HotSpot)采用的是分代收集算法。开发者一般不需要专门编写内存回收和垃圾清理代码,解决内存泄露和溢出的问题。与C++不同的是,Java采用的是类似于树形结构的可达性分析法来判断对象是否还存在引用。即:从gcroot开始,把所有可以搜索得到的对象标记为存活对象。

    缺点:

  • 1. 有可能不知不觉浪费了很多内存

  • 2. JVM花费过多时间来进行内存回收

  • 3. 内存泄露

2.1 什么时候GC

    GC触发的条件有两种:

  • 1.程序调用System.gc时可以触发;

  • 2.系统自身来决定GC触发的时机。根据Eden区和From区的内存大小来决定。当内存大小不足时,则会启动GC线程。

2.2 GC什么对象

    GC前的第一步就是要判断那些对象已经死亡?怎么判断呢?通常有两种算法:

1.引用计数

    给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;计数器为0的对象就是可以回收的。

    这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题。


2.可达性分析

    基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

额...我们还是画个图吧:

    那么GC Roots对象是哪些对象呢?

  • 1.Java栈和本地方法栈中所有引用的对象;

  • 2.方法区中的常量和静态变量;

  • 3.所有线程对象;

  • 4.所有跨代引用对象;

  • 5.和已知GCRoots对象同属一个CardTable的其他对象。

    前三种很好理解,第4,5种涉及到跨代引用那么什么是跨代引用呢?正如字面的意思,年轻代的对象被老年代引用了。为什么跨代是GC roots对象?

    对年轻代做可达性分析时,如果一个对象A,没有被 GCRoots(1,2,3的情况)引用,那它可以被回收吗?不一定,因为它可能被老年代的对象引用了(跨代引用)。所以,为了知道对象A是否可以被回收,还需要遍历一遍老年代!!!但现实不可能去遍历老年代,太慢了。了解决问题,引入了「跨代引用是 GC Root」的解决办法如果老年代的 Old 对象,引用了年轻代的Young对象,在对年轻代进行可达性分析时,Old 对象算作 GC Root。这样就不用遍历老年代了。

    那么什么是CardTable呢?

    分代回收算法需要有一个表(记忆集),用来记录所有的跨代引用。HotSpot使用CardTable记录老年代对年轻代的引用。把老年代按照4KB的大小分块,每一块对应在CardTable中都是1bit。当值为1时,表示这4KB的内存中有对年轻代的引用,需要加入到GC Roots中。所以,此时年轻代的对象不能被GC回收。

    虽然引入记忆集后,现在不需要遍历整个老年代了,但也会有一个很有意思的问题:

    如上图,老年代对象即便已经事实上不可达了,但是因为记忆集的存在,会导致从该对象出发的跨代引用依旧会被当成GC root,直至该对象被回收引起记忆集中相关条目的擦除。这个问题就是因为使用记忆集带来的“滞后性”,它提高了时间效率,但是却降低了空间利用率。

    不过无论如何,它依然确保了垃圾回收所遵循的原则:垃圾回收确保回收的对象必然是不可达对象,但是不确保所有的不可达对象都会被回收