6.垃圾回收
6.1 判断垃圾
6.1.1 引用计数法
当一个对象被引用一次则计数+1,失去引用计数-1,当计数为0则判断为垃圾。但当对象间存在循环引用时(如下图)会无法被回收。
6.1.2 可达性分析算法
Java中使用可达性分析算法来判断垃圾。肯定不会被垃圾回收的对象为根对象,可以经由根对象直接或间接引用的对象不会被垃圾回收,反则反之。打个比喻:连在串上的葡萄就是不可以被回收的对象,散在盘中的葡萄就是可以被垃圾回收的。
哪些对象可以作为根对象呢?使用eclipse的MAT(memory analyzer)可以进行分析。这个工具比jvisual更加专业,可以找到内存泄漏。
运行如下代码。
/** * 演示GC Roots */ public class Demo2_2 { public static void main(String[] args) throws InterruptedException, IOException { List<Object> list1 = new ArrayList<>(); list1.add("a"); list1.add("b"); System.out.println(1); System.in.read(); list1 = null; System.out.println(2); System.in.read(); System.out.println("end..."); } }
使用命令jps查看当前运行代码的进程为17332。
PS F:\资料 解密JVM\代码\jvm> jps 13472 Launcher 3296 Jps 12868 RemoteMavenServer36 17332 Demo2_2 11180
在list回收前、后分别使用jamp抓取目标进程内存的快照,转储为二进制文件,并设置live参数在抓取快照前主动触发垃圾回收。回收前的操作命令如下。
jmap -dump:format=b,live,file=gcRootDemo.bin 17332 1
使用elipse的MAT工具可以来分析内存泄漏问题,官网下载安装MAT,https://www.eclipse.org/mat/downloads.php。
使用MAT工具,菜单栏file->open dump file打开刚才抓取的快照文件。打开文件时报错Invalid HPROF file header,其中一个原因为人工改变了文件的编码格式,重新抓取并不要改变编码格式。将两个文件都打开以方便对照。如下图,查看文件的GC Roots。
可以看到GC Root的具体情况,被分为了4类。
System Class是程序运行所必须的核心类。
第二类是执行本地方法时操作系统所引用的Java对象的类。
Busy Monitor是指正在加锁的对象,如果这对象被回收了,则锁无法被释放,故不会被回收
最后是活动着的线程中,局部变量所引用的对象不能被当成垃圾回收。比如下图中的ArrayList其实就是对应代码中list1被垃圾回收前所指对象,在list对象回收后的抓取的内存快照gcRootDemo2.bin中该对象不存在了。
6.2 五种引用ava中有五种引用类型。
只要沿着GC Root可以找到该对象,则不会被垃圾回收。如图中A1对象,只有当B,C对象对A1的引用都断开时,才会被垃圾回收。
6.2.2 软引用
当发生垃圾回收且内存不够时,则会对其进行进行回收。如图中A2对象,当B对象的引用断开,那么进行垃圾回收且内存不够时,A2对象将会被回收。
6.2.3 弱引用
当发生垃圾回收时,就会对其进行回收。如图中A3对象,当B对象的引用断开,那么进行垃圾回收,A3对象将会被回收。
特别的,软引用和弱引用本身也属于对象,可以配合引用队列进行使用。
6.2.4 虚引用
虚引用与终结器引用必须配合引用对象进行使用。如前文中提到的ByteBuffer对象,会创建一个虚引用Cleaner,并且会将ByteBufer所分配的直接内存传递给Cleaner引用。当ByteBufferber对象被垃圾回收后,Cleaner会进入引用队列。ReferenceHandler会定期扫描引用队列中新入队的对象,当Cleaner被扫描到就会执行其clean()方法,调用Unsafe对象的freememory()将直接内存释放。
6.2.5 终结器引用
所有对象都继承自Object,而Object中有一个finalize()方法,对象可以重写finalize()方法,在对象进行垃圾回收时该方法将被调用。但是对象已经没有强引用了,finalize()方法怎么被调用呢?其实就是通过终结器引用实现的。在B对象断开A4的强引用后,终结器引用会被加入引用队列,由一个优先级很低的finalizeHandler进行扫描,当扫描到引用队列中的终结器引用后,会执行其所引用的A4对象的finalize()方法。由于finalize()方法不会被立刻执行,而是先进行入队,并且负责扫描的finalizeHandler优先级低,可能导致finalize()迟迟得不到执行,因此不推荐使用它进行资源回收.
6.3 软引用应用
配置运行下列代码,显然会报OutOfMemoryError。
/** * 演示软引用 * -Xmx20m */ public class Demo2_3 { private static final int _4MB = 4 * 1024 * 1024; public static void main(String[] args) throws IOException { List<byte[]> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { list.add(new byte[_4MB]); } } }
而以上常见在实际编程中其实是常见的,比如在读取图片内容时。软引用可以解决这种内存占用的问题。
public static void soft() { // list --> SoftReference --> byte[] List<SoftReference<byte[]>> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]); System.out.println(ref.get()); list.add(ref); System.out.println(list.size()); } System.out.println("循环结束:" + list.size()); for (SoftReference<byte[]> ref : list) { System.out.println(ref.get()); } }
以上代码中,list和SoftReference是强引用,但是SoftReference和byte[]是软引用。打印结果如下。
[B@7f31245a 1 [B@6d6f6e28 2 [B@135fbaa4 3 [B@45ee12a7 4 [B@330bedb4 5 循环结束:5 null null null null [B@330bedb4
观察到在循环结束前可以调用到bye[]数组,但是循环结束前四个数组已经变成了null。
添加虚拟机参数:-XX:+PrintGCDetails -verbose:gc,打印垃圾回收的细节与详细参数,然后再次运行查看垃圾回收完整过程。
[B@7f31245a 1 [B@6d6f6e28 2 [B@135fbaa4 3 [GC (Allocation Failure) [PSYoungGen: 2209K->488K(6144K)] 14497K->13130K(19968K), 0.0024761 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [B@45ee12a7 4 [GC (Allocation Failure) --[PSYoungGen: 4696K->4696K(6144K)] 17338K->17502K(19968K), 0.0020330 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 4696K->4466K(6144K)] [ParOldGen: 12806K->12675K(13824K)] 17502K->17142K(19968K), [Metaspace: 3331K->3331K(1056768K)], 0.0100720 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC (Allocation Failure) --[PSYoungGen: 4466K->4466K(6144K)] 17142K->17158K(19968K), 0.0024079 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [Full GC (Allocation Failure) [PSYoungGen: 4466K->0K(6144K)] [ParOldGen: 12691K->740K(8704K)] 17158K->740K(14848K), [Metaspace: 3331K->3331K(1056768K)], 0.0118198 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [B@330bedb4 5
循环结束:5
null null null null [B@330bedb4 Heap PSYoungGen total 6144K, used 4377K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000) eden space 5632K, 77% used [0x00000000ff980000,0x00000000ffdc64f0,0x00000000fff00000) from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) ParOldGen total 8704K, used 740K [0x00000000fec00000, 0x00000000ff480000, 0x00000000ff980000) object space 8704K, 8% used [0x00000000fec00000,0x00000000fecb90f0,0x00000000ff480000) Metaspace used 3352K, capacity 4500K, committed 4864K, reserved 1056768K class space used 359K, capacity 388K, committed 512K, reserved 1048576K
观察到在第三个循环结束后,内存已经快不足,进行了新生代回收。在第四个循环结束后,又进行了一次新生代的回收,但是效果不理想(4696K->4696K),于是触发了一次Full GC。由于进行垃圾回收且内存仍然不足,又触发了一次新的垃圾回收,将软引用所引用的对象释放。像缓存的图片等不重要的对象,可以通过软引用来引用,当内存空间不足时就会回收它们。
同时我们也注意到,前四个软引用所指的对象已经是null了,没有必要再把这四个软引用保留在list集合中。可以配合引用队列来完成软引用的回收。