文章目录
- 8 垃圾回收
- 8.1 基本理论
- 8.1.1 对象的finalization机制
- 8.1.2 理解System.gc
- 8.1.3 内存溢出和内存泄漏
- 8.1.4 Stop The World
- 8.1.5 安全点和安全区域
- 8.1.6 Java中的引用
- 8.2 垃圾回收算法
- 8.2.1 引用计数法
- 8.2.2 可达性分析
- 8.2.2.1 使用MAT查看GC Roots
- 8.2.2.2 使用JProfiler分析OOM
- 8.2.3 复制算法
- 8.2.4 标记清除
- 8.2.5 标记压缩
- 8.2.6 标记清除压缩
- 8.2.7 小结
- 8.2.8 增量收集算法
- 8.2.9 分区算法
- 8.3 垃圾回收器
- 8.3.1 评估垃圾回收器的性能指标
8 垃圾回收
8.1 基本理论
【什么是垃圾】
垃圾是指在运行程序中没有任何指针指向的对象。
8.1.1 对象的finalization机制
提供finalization机制来允许开发人员自定义对象销毁前的处理逻辑。 Object中定义了finalize方法,可以被覆写。
永远不要主动调用对象的finalize方法,这个应该交由垃圾收集器调用。 理由如下:
(1)在finalize时可能会导致对象复活。
(2)finalize的执行时刻是没有保障的,完全由GC线程决定。 极端情况下,如果不发生GC,那么finalize方法永远不会执行。
(3)一个糟糕的finalize方法严重影响GC的性能。
虚拟机中的对象存在三种状态:
① 可触及的:从根节点开始,可以到达这个对象
② 可复活的:对象的所有引用都被释放,但是对象有可能在finalize中复活。
③ 不可触及的:对象的finalize被调用,并且没有复活,那么就会进入不可触及状态。 不可触及的对象不可能被复活,因为finalize只会被调用一次。
判定一个对象objA是否可以回收,至少要经历两次标记过程:
- 如果对象objA到GC Roots没有引用链,则进行第一次标记。
- 进行筛选,判断此对象是否有必要执行finalize方法。
① 如果对象objA没有重写finalize方法,或者finalize方法已经被调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
② 如果对象objA重写了finalize方法,并且还未执行,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize方法。
③ finalize方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue中的对象进行二次标记。如果objA在finalize方法中与引用链上的任何一个对象建立了联系,那么在二次标记时,objA会被移出“即将回收”集合。之后,如果对象又出现了没有引用存在的情况,对象会直接变为不可触及的状态。 finalize方法只会被调用一次。
/*** 注释掉了finalize方法的结果* first gc* obj is dead* second gc* obj is dead* * 没有注释掉finalize方法的执行结果:* first gc* Call override finalize method.* obj is still alive* second gc* obj is still alive*/
public class CanReliveObj {public static CanReliveObj obj;@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("Call override finalize method.");obj = this;}public static void main(String[] args) throws InterruptedException {obj = new CanReliveObj();obj = null;System.gc();System.out.println("first gc");// finalizer线程优先级很低,主线程sleep两秒,等待finalizer执行。Thread.sleep(2000);if (obj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}System.out.println("second gc");// finalizer线程优先级很低,主线程sleep两秒,等待finalizer执行。Thread.sleep(2000);if (obj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}}
}
8.1.2 理解System.gc
显示触发full gc,同时对新生代和老年代进行回收。 但是System.gc无法保证对垃圾收集器的调用(无法确保开始执行的时间)。
public class SystemGcTest {public static void main(String[] args) {new SystemGcTest();System.gc();System.runFinalization();}@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("SystemGcTest has override finalize() method");}
}
The java.lang.Runtime.runFinalization() method runs the finalization methods of any objects pending finalization. Calling this method suggests that the Java virtual machine expend effort toward running the finalize methods of objects that have been found to be discarded but whose finalize methods have not yet been run. When control returns from the method call, the virtual machine has made a best effort to complete all outstanding finalizations.
The virtual machine performs the finalization process automatically as needed, in a separate thread, if the runFinalization method is not invoked explicitly. The method System.runFinalization() is the conventional and convenient means of invoking this method.
8.1.3 内存溢出和内存泄漏
【内存溢出】
OOM:没有空闲的内存,并且垃圾收集器也无法提供更多的内存。
【内存泄漏】
内存泄漏:只有对象不会再被程序用到了,但是GC又不能回收他们的情况,称为内存泄漏。
实际情况中,可能会存在一些不好的实现,会导致对象的生命周期变得很长,甚至导致了OOM,这种就叫做广义上的“内存泄漏”。
举例:
-
单例模式
如果单例对象持有外部对象的引用的话,这个外部对象是不能被回收的,会导致内存泄漏产生。 -
一些提供close的资源未关闭导致内存泄漏
数据库连接、网络连接和IO连接必须手动close,否则是不会被回收的。
8.1.4 Stop The World
8.1.5 安全点和安全区域
用户线程只有在特定的位置才能停顿下来GC,这些位置称为“安全点(Safe Point)”。如果安全点太少会导致GC等待的时间太长。如果太多会导致运行时性能问题。通常会选择一些执行时间较长的指令作为safe point,如方法调用、循环跳转和异常跳转。
【如何保证在GC的时候,检查所有线程都跑到最近的安全点上停顿下来】
- 抢先式中断
目前没有虚拟机采用了。
首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。 - 主动式中断
设置一个中断标志,各个线程运行到安全点的时候主动轮询这个标志,如果中断标志为true,则将自己挂起。
线程处于Sleep状态或者Blocked状态,这时候无法响应JVM的中断请求,无法走到安全点。此种情况下,需要靠安全区域来解决这个问题。
安全区是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何位置开始GC都是安全的。
【安全区域的运作机制】
- 当线程运行到safe region的时候,标识自己已经进入了Safe Region,如果这段时间内发生了GC,JVM会忽略标识为Safe Region状态的线程。
- 当线程即将离开Safe Region时,会检查JVM是否已经完成了GC,如果完成了GC,继续运行。否则线程必须等待直到收到可以安全离开Safe Region的信号为止。
8.1.6 Java中的引用
强引用:不回收
软引用:内存不足即回收
弱引用:发现即回收
虚引用:对象回收跟踪
【终结器引用】
终结器引用用来实现对象的finalize方法。
无需手动编码,其内部配合引用队列使用。
在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次gc的时候才能回收被引用对象。
8.2 垃圾回收算法
8.2.1 引用计数法
【怎样判断垃圾——引用记数法】
优点:实现简单,垃圾对象便于识别;判定效率高,回收没有延迟性(只要引用计数是0,随时都可以回收)。
Python使用的是引用计数法。Python解决循环引用的方法:(1)手动接触引用(2)弱引用。
8.2.2 可达性分析
又名根搜索算法,追踪性垃圾收集算法。
【可以作为GC Roots的对象】
- 虚拟机栈中引用的对象
方法的参数,局部变量等。 - 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
Java应用类型的静态变量 - 方法区中常量引用的对象
字符串常量池中的引用 - 所有被同步锁synchorinized持有的对象
- Java虚拟机内部的引用
基本数据类型对应的Class对象,一些常驻的异常对象(如NPE,OOM),系统类加载器。 - 反应虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
【Stop The World】
可达性分析必须要在一个能保障一致性的快照中进行。 如果不满足的话,分析结果的准确性无法保证。 这就是GC进行时必须“Stop the World”的一个重要原因。 即使几乎不发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
8.2.2.1 使用MAT查看GC Roots
① 待查看的代码
package org.example.gcroot;import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;public class GcRootTest {public static void main(String[] args) {List<Object> numList = new ArrayList<>();Date birth = new Date();for (int i = 0; i < 100; i++) {numList.add(String.valueOf(i));try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("数据添加完毕,请操作");new Scanner(System.in).next();numList = null;birth = null;System.out.println("numList, birth已经置空,请操作");new Scanner(System.in).next();System.out.println("程序结束");}
}
② 在运行过程中使用visualvm导出堆dump,在第一次程序阻塞和第二次程序阻塞的时候都dump一次
③ 使用MAT查看两次导出的dump文件,查看gc roots
8.2.2.2 使用JProfiler分析OOM
① 启用在OOM的时候导出HeapDump
② 使用JProfiler查看Biggest Object
③ 查看Thread Dump,查看哪个线程的哪一行有问题。
8.2.3 复制算法
Copying
【优点】
实现简单,运行高效。
可以保证空间连续性,不会出现“碎片”问题。
【缺点】
需要两倍的内存空间。空间浪费。
G1将内存拆分成很多的region,在复制过程中,GC需要维护region之间对象的引用关系。内存占用和时间开销都是不小的。
如果系统中存活的对象很多,复制算法就不理想。这样会复制很多存活的对象,过犹不及。
8.2.4 标记清除
标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。 注意:标记清除算法的标记标记的是可达对象,不是垃圾对象。
清除:Collector对堆内存进行线性遍历,如果发现某个对象的Header中没有可达标记,则将其回收。
缺点:
效率不算高。 两次扫描
在GC的时候需要停止整个应用程序。
存在内存碎片,需要维护空闲列表。
【何为清除?】
清除并不是真的置空,而是把要清除的对象地址保存在空闲的地址列表里面。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果足够就存放。
8.2.5 标记压缩
标记压缩算法又叫标记整理算法。
【优点】
- 没有内存碎片
- 没有复制算法内存减半的高额代价。
【缺点】 - 效率上讲,低于复制算法
- 需要调整引用的地址
- STW
8.2.6 标记清除压缩
8.2.7 小结
Mark阶段的开销与存活对象的数量成正比。
Sweep阶段的开销与所管辖的区域的大小成正比。
Compact阶段的开销与存活对象的数据成正比。
以HotSpot中的CMS回收器为例,CMS是基于MARK-SWEEP实现的,对于对象的回收效率很高。对于碎片问题,CMS使用基于标记压缩算法的Serial Old收集器进行补偿。 当内存回收不佳的时候,将采用Serial Old执行Full GC以达到对老年代内存的整理。
8.2.8 增量收集算法
优点:
缺点:系统吞吐量的下降。
8.2.9 分区算法
将大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而非整个堆空间。 从而减少一次GC所产生的停顿。
8.3 垃圾回收器
8.3.1 评估垃圾回收器的性能指标
【吞吐量】
运行用户代码时间占总运行时间的比例。
总运行时间 = 程序的运行时间 + 内存回收的时间
【垃圾收集开销】
吞吐量的补数,垃圾收集所用时间占总运行时间的比例。
【暂停时间】
进行垃圾收集时,程序的工作线程被暂停的时间
【收集频率】
收集操作发生的频率
【内存占用】
Java堆区所占内存的大小。
【回收速率】
一个对象从诞生到被回收所经历的时间。
串行回收器:Serial、Serial Old
并行回收器:ParNew、Parallel Scavenge、Parallel Old
并发回收器:CMS、G1
新生代收集器:Serial、ParNew、 Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:G1