一、GC什么对象
GC的对象是没有存活的对象,判断没有存活的对象有两种常用方法:引用计数和可达性分析。
1.1 java的GCRoots引用对象
在 Java 虚拟机的语境下,垃圾指的是死亡的对象所占据的堆空间。
a. 虚拟机栈中引用的对象。
b. 方法区中静态属性引用的对象。
c.方法区中常量引用的对象。
d.本地方法中JNI引用的对象。
说明:当前对象到GCRoots中不可达时候,即会满足被垃圾回收的可能。这些对象但不是就非死不可,此时只能宣判它们存在于一种“缓刑”的阶段,要真正的宣告一个对象死亡。至少要经历两次标记:
第一次:对象可达性分析之后,发现没有与GCRoots相连接,此时会被第一次标记并筛选。
第二次:对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,此时会被认定为没必要执行。
1.2 结合GC对象回顾java虚拟机内存
说明:a. 虚拟机栈中引用的对象、b. 方法区中静态属性引用的对象(b.1基本类型数据是存储在运行时常量池)、c.方法区中常量引用的对象,d.本地方法中JNI引用的对象,这些对象都存储在java堆。
二、什么时候GC
2.1 判断没有存活的对象有两种常用方法
如何辨别一个对象的存亡是关键问题。
1.引用计数
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
优点:实现简单,判定效率高效,被actionscript3和python中广泛应用。
缺点:无法解决对象之间的循环引用问题。
2.可达性分析
目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。该算法的实质:将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的不可达对象。如下图所示,右侧的对象是到GCRoot时不可达的,可以判定为可回收对象。
思考题:什么是GC Roots?GC Roots与GC对象的关系?
解答:由堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)下列几种,Java 方法栈桢中的局部变量、已加载类的静态变量、JNI handles、已启动且未停止的 Java 线程。因此,GC Roots是GC对象的引用。
可达性分析法的问题:在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。误报使得Java 虚拟机损失该次垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致 Java 虚拟机崩溃。
2.2 触发GC的动作及时机
(1)动作:程序调用System.gc时可以触发。
(2)时机:系统自身来决定GC触发的时机
根据Eden区和From Space区的内存大小来决定,当内存大小不足时,则会启动GC线程并停止应用线程,GC又分为 Minor GC 和 Full GC。
Minor GC触发条件:① 当 Eden 区满时,触发 Minor GC。
② 当 FromSuv 或者 ToSuv 区满时,触发 Minor GC。
Full GC触发条件: ① 调用System.gc时,系统建议执行Full GC,但是不必然执行
② Heap 的老年区空间不足
③ Metaspace 空间不足
④ 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
⑤ 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
三、如何进行GC
GC算法是内存回收的理论方法,而GC垃圾收集器则是是内存回收的具体实现。下面的内容先讲GC常用算法。
3.1 GC算法理论基础
GC算法是内存回收的理论方法。GC常用算法理论有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。即回收垃圾对象的内存共有三种方式,分别为:会造成内存碎片的清除、性能开销较大的压缩、以及堆使用效率较低的复制。目前主流的JVM(HotSpot)采用的是分代收集算法。
3.1.1 标记清除法
标记清除法是垃圾回收算法的思想基础。标记清除算法将垃圾分为两个阶段:标记阶段和清除阶段。
标记阶段:通过根节点,标记所有从根节点开始的可达对象,未标记过的对象就是未被引用的垃圾对象。
清除阶段:清除所有未被标记的对象。
3.1.2 复制算法
复制算法是,将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在适用的内存中存活对象复制到未使用的内存块,然后清除使用的内存块中所有的对象。
3.1.3 标记压缩算法
标记压缩算法是一种老年代的回收算法。
标记阶段:与标记清除算法一致,对可达对象做一次标记。
清理阶段:为了避免内存碎片产生,将所有的存活对象压缩到内存的一端。
四、Java虚拟机的堆划分
Java 虚拟机将堆划分为新生代和老年代。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区即FromSuv和ToSuv。
当调用new 指令时,java虚拟机在Eden区中划出一块作为存储对象的内存。由于堆空间是线程共享的,因此直接在Eden区是需要进行同步的。new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。
问题1:两个线程同时new Object1对象,则堆如何划分内存?
解答:由于堆内存是线程共享的,同步为两个线程分别划分object1的内存空间,即有2个object1对象。该技术被称为TLAB(Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)。
问题2:当 Eden 区的空间耗尽了怎么办?
解答:这个时候Java虚拟机会触发一次Minor GC,来收集新生代的垃圾。存活下来的对象,则会被送到Survivor区。
问题3:新生代的两个Survivor 区,即FromSuv和ToSuv有什么用处?
解答:当Minor GC时,Eden和FromSuv中的存活对象会被复制到ToSuv中,然后交换FromSuv和ToSuv指针,以保证下一次Minor GC时,ToSuv还是空的。满足两种情况之一,可以使对象移动到老年代:1. Minor GC,存活对象从FromSuv复制到ToSuv,其对象的age+1,当超过(默认值)15的时候,转移到老年代;2. 动态对象,如果survivor空间中相同年龄所有的对象大小总和,大于survivor空间的一半,则年级大于或等于该年级的对象就可以直接进入老年代。
注意:Minor GC只针对新生代进行垃圾回收,所以在枚举 GC Roots 的时候,需要考虑从老年代到新生代的引用。为了避免扫描整个老年代,Java 虚拟机引入卡表(Card Table)的技术,大致地标出可能存在老年代到新生代引用的内存区域。
五、GC案例分析
从一个object1分析该对象在分代垃圾回收算法中的回收轨迹。
Minor GC是指发生在新生代的GC,因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;Full GC是指发生在老年代的GC,出现Full GC一般会伴随至少一次的Minor GC,其速度一般比Minor GC慢10倍以上。
步骤1:实例化object1,出生于新生代的Eden区域;
步骤2:Minor GC,object1移动到新生代的Fromsuv区域,object1还存活。
步骤3:Minor GC,通过复制算法将object1移动到新生代的ToSuv区域,同时object1的年龄age+1,object1 依然存活;
步骤4:Minor GC,在新生代的survivor区域中,与object1同龄的对象并没有达到survivor的一半。因此,通过复制算法将FromSuv和ToSuv 区域进行互换,object1对象被移动到了新生代的ToSuv,object1 依然存活;
步骤5:Minor GC,此时survivor中和object1同龄的对象已经达到survivor的一半以上,object1被移动到了老年代区域,object1 依然存活。
满足两种情况之一,都可以使object1对象移动到老年代:
1. Minor GC,存活于survivor 区域的object1对象的age+1,当超过(默认值)15的时候,转移到老年代;注意:minor GC下,步骤2/3/4中的移动/复制全部Tosuv/Fromsuv区域的对象。
2. 动态对象,如果survivor空间中相同年龄所有的对象大小总和,大于survivor空间的一半,则年级大于或等于该年级的对象就可以直接进入老年代。
步骤6:Full GC会触发stop the world。object1存活一段时间后,此时GC Roots不可达object1,而且此时老年代空间比率已经超过了阈值,触发了Full GC,此时object1被回收。
注意:object1 被回收的必要条件是 object1 不可达(GC Roots),即 object1 的引用是弱引用。
以上的步骤采用分代垃圾收集的思想,描述object1对象从存活到死亡的过程。新生代:采用复制算法,老年代:采用标记-清除算法或者标记-整理算法。
stop the world是一种简单除暴的方式,即停止其他非垃圾回收线程的工作,直到完成垃圾回收。Java 虚拟机中的 stop the world 是通过安全点(safepoint)机制来实现的。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 stop the world 的线程进行独占的工作。安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。例如,java执行某个JNI本地方法时,不访问Java对象、调用Java方法、返回至原Java方法,则Java虚拟机的堆栈不会发生改变,所以这段代码可以作为安全点。因此,Java虚拟机在这个安全点,可以同时进行垃圾回收和执行这段代码。