JVM学习笔记(四)-JVM的垃圾收集1

回顾

  上一篇博客写了JVM的内存溢出问题,比较了内存溢出和内存泄漏的区别,然后对虚拟机栈的OOM和SOF、方法区和运行时常量池的OOM、堆的OOM做了相关实验验证,在实验过程中发现了java8对方法区perm gen的改进,即metaspace代替perm gen(java7将字符串常量池移入了堆中)。最后,对于使用stringbuilder造成的heap space的OOM的实验现象提出了两个疑问,目前还没有解决,期待和读者一起讨论。
  前面几篇博客大致将java运行时数据区域的基本内容写完了,本篇博客进入JVM的垃圾收集部分。这部分预计会分两篇博客来写,本篇博客会对对象存活判定算法、垃圾收集算法以及JVM对以上算法的实现进行学习。

对象存活判定算法

  看了一篇百度置顶的简书上的对象存活判定算法的讲解,讲得太好了。。。搞得我都不太好意思写了,本节最后会给上链接,这里我就硬着头皮以那篇文章的思路为基础来写吧!(这里对那篇文章作者表示无比尊敬之情,盗张图先。。)
首先介绍一下大致流程:

JVM学习笔记(四)-JVM的垃圾收集1_第1张图片 对象存活判定算法流程图

  首先进行可达性分析,对于不可达的对象进行两次标记/筛选过程,任何一次标记/筛选阶段没有成功“自救”,则该对象将会被回收。

判定方式

  目前在编程语言中常见的是引用计数法和可达性分析法。

1.引用计数法

  引用计数法虽然没有在主流的JVM中使用,但是在Python、游戏脚本语言等领域也有广泛使用,是一个经典的内存管理算法。
优点:实现简单,判定效率比较高
缺点:无法解决对象循环引用的问题
例如下面的代码:

public class ReferenceCountingGC {
	private ReferenceCountingGC instance = null;
	private static final int _1MB = 1024*1024;
	private byte[] bigsize = new byte[2*_1MB];//占用内存,方便查看GC,因为每个对象都有它
	
	public static void main(String[] args) {
		ReferenceCountingGC objA = new ReferenceCountingGC();
		ReferenceCountingGC objB = new ReferenceCountingGC();
		
		//相互引用
		objA.instance = objB;
		objB.instance = objA;
		
		//置为null
		objA = null;
		objB = null;
		
		//想要触发GC
		System.gc();
	}

}

下面是GC日志:

[GC (System.gc()) [PSYoungGen: 9876K->1292K(36864K)] 9876K->1300K(121856K), 0.0794454 secs] [Times: user=0.16 sys=0.00, real=0.08 secs] 
[Full GC (System.gc()) [PSYoungGen: 1292K->0K(36864K)] [ParOldGen: 8K->1188K(84992K)] 1300K->1188K(121856K), [Metaspace: 4725K->4725K(1056768K)], 0.0098699 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 36864K, used 317K [0x00000000d6c00000, 0x00000000d9500000, 0x0000000100000000)
  eden space 31744K, 1% used [0x00000000d6c00000,0x00000000d6c4f738,0x00000000d8b00000)
  from space 5120K, 0% used [0x00000000d8b00000,0x00000000d8b00000,0x00000000d9000000)
  to   space 5120K, 0% used [0x00000000d9000000,0x00000000d9000000,0x00000000d9500000)
 ParOldGen       total 84992K, used 1188K [0x0000000084400000, 0x0000000089700000, 0x00000000d6c00000)
  object space 84992K, 1% used [0x0000000084400000,0x0000000084529058,0x0000000089700000)
 Metaspace       used 4732K, capacity 4930K, committed 5248K, reserved 1056768K
  class space    used 510K, capacity 561K, committed 640K, reserved 1048576K

  可以看到在一次GC后,9876→1292,堆中的对象是被回收了的。这从侧面说明Hotspot虚拟机不是使用的引用计数法。

2.可达性分析算法

  可达性分析的两个重要概念:GC roots和引用链。

GC roots

  gc roots是引用链的根节点,能够作为gc roots的对象有以下几类:

1.虚拟机栈本地变量表中引用的对象
2.本地方法栈中JNI(java native interface)引用的对象
3.方法区的常量、类静态属性引用的对象

引用链
  从gc roots出发,向下搜索,搜索走过的路径叫做引用链。
如下图所示:

JVM学习笔记(四)-JVM的垃圾收集1_第2张图片 引用链

可达性分析

  如果一个对象和gc roots之间没有通过任何引用链相连,那么就说该对象是不可达的。不可达的对象即将进入标记/筛选阶段。

标记/筛选阶段

1.第一次标记/筛选过程

  简单来说,第一次标记过程就是查看对象需不需要执行finalize方法。如果需要,就进入下一标记阶段,如果没有必要执行,就结束标记阶段,基本判定该对象需要被回收了。

是否有必要执行finalize方法的判断依据

1.如果该对象对应的类中没有覆盖finalize方法,则说明没有必要执行
2.如果在之前,该对象已经执行过一次finalize方法了,则说明没有必要执行(因为finalize方法只能执行一次)

2.第二次标记/筛选过程

在第一次标记/筛选过程后,被认为有必要执行finalize方法的对象放入一个F-QUEUE中,JVM会自动创建一个低优先级的线程执行F-QUEUE中的finalize方法。这里的“执行”并不一定表示要完全执行完。因为如果一个finalize方法中有死循环,难道还要一直等到它执行吗?队列会被阻塞的!

判定对象是否存活的依据:

在finalize方法中,该对象又重新连接到了gc roots的引用链上

实验

package gctest;

public class FinalizeEscapeGC {
	private static FinalizeEscapeGC instance = null;//instance是gc root
	
	@Override
	protected void finalize() throws Throwable {
		
		super.finalize();
		FinalizeEscapeGC.instance = this;
		System.out.println("I save mylife!");
	}
	
	public void isAlive() {
		System.out.println("I'm alive!");
	}
	
	public static void main(String[] args) {
		
		instance = new FinalizeEscapeGC();
		instance = null;//对象和gc roots之间失去连接
		System.gc();
		
		//第一次拯救过程
		try {
			Thread.sleep(500L);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		try {
			instance.isAlive();//判断对象是否存活,若存活,则不会是空指针
		} catch (NullPointerException e) {
			System.out.println("nullpointerException happens");
		}
		
		
		instance = null;//对象和gc roots之间失去连接
		System.gc();
		
		//第二次拯救过程,就是把第一次的代码重复一遍
		try {
			Thread.sleep(500L);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		try {
			instance.isAlive();//判断对象是否存活,若存活,则instance不会是空指针
		} catch (NullPointerException e) {
			System.out.println("NullPointerException happens");
		}
		
	}
	
}
JVM学习笔记(四)-JVM的垃圾收集1_第3张图片 运行结果

  可以看到对象开始进行了一次自救,后来自救失败,和instance失去了连接,instance为null。

参考简书文章超链接

简书关于对象存活判定算法的超链接

方法区的回收

  方法区同样是由GC存在的。主要是对废弃常量和无用的类进行回收。
废弃常量:不再被使用的常量(指的是常量池中的字面量和符号引用),比如常量池中存入了一个“ABC”,但是任何地方都没有使用到它,就判定为废弃的常量。
无用的类:

1.该类的实例对象都被回收
2.加载该类的类加载器被回收
3.该类的java.lang.Class对象没有在任何地方引用到,无法通过反射访问该类中的方法

本节总结

  对象一旦被不可达分析判定为不可达后,要经历两次标记过程的重重考验才能成功自救,并且只能通过finalize方法自救一次。

垃圾回收算法

  共分为三种收集算法和一种收集思想。即标记-清除算法,标记-整理算法,复制算法,分代收集思想。

1.标记-清除算法

  这个算法很好理解,就是判定死亡的对象就回收那块内存,啥也不管了。
优点:简单
缺点:会形成大量空间碎片,导致没有完整的内存分配给新创建的对象,提前出发full GC.

JVM学习笔记(四)-JVM的垃圾收集1_第4张图片 标记清除算法

2.标记-整理算法

  标记整理算法在标记清除算法基础上进行了改进,在回收完死亡对象的内存后,将存活的对象向内存空间的一端进行移动,这样就解决了空间碎片的问题。
优点:不会产生空间碎片,防止提前进行Full GC
缺点:执行速度要比标记清除慢

JVM学习笔记(四)-JVM的垃圾收集1_第5张图片 标记整理算法

3.复制算法

  复制算法,简单来说,就是你现在有两块空间,一块A存对象,另一块B空。在回收死亡对象后,将A中的存活对象复制到B中,然后清空A的内存,这样就又形成了一块存对象的空间和一块空的空间。
优点:不会有空间碎片问题,在分配空间时很高效
缺点:牺牲掉了一部分内存空间作为空的内存空间。

JVM学习笔记(四)-JVM的垃圾收集1_第6张图片 复制算法

4.分代收集思想

  分代收集思想就是将堆内存分为老年代和新生代,对不同的区域执行不同的垃圾回收算法,使得内存的分配与回收更加高效!比如,老年代适合使用标记清除算法和标记整理算法。新生代则进一步分为Eden区、from survivor区和to survivor区,执行复制算法。

JVM中对象存活判定和GC算法的实现

1.枚举根节点

  在进行可达性分析时,我们需要有根节点信息和引用链信息,这就需要有两方面的要求:STW(stop the world)和引用信息
STW:可达性分析对时间是敏感的,不能再分析的同时,引用关系还在发生变化,所以在GC进行时必须停顿java所有线程,称为stop the world。
引用信息:建立引用关系必须知道当前内存中哪些是对象的引用,传统方法是直接扫描整个内存,获取引用信息。Hotspot采用oopMap的方式。

1.记录栈、寄存器等区域中哪些位置是GC管理的指针
2.一段代码内可以有多处使用oopMap,但不是每条指令都会使用oopMap,也就是安全点,safepoint将一段代码分为好几段
3.oopMap的作用域也只在它所在的那一段里

2.安全点

  GC只有在安全点出才能执行,安全点一般是在“程序能长时间执行的地方”,特征是指令序列复用。(看书的时候这里一直不明白为什么要这么选,后面会讲到)

1.循环跳转
2.方法调用
3.异常跳转

为什么这样选择呢?

1.如果选择的安全点过少,每次GC之间间隔时间太长,安全点选择过多,GC执行又过于频繁。
2.想象如果A、B两个安全点之间有一个上面说的循环跳转、异常跳转等代码段,刚好这段代码执行时间非常长长长。。。。,甚至是死循环,那想要在B安全点进行GC不是要等到天荒地老?(即GC之间间隔时间太长)

怎样让所有线程都在安全点呢?

  GC是整个JVM的,JVM中会有很多个线程在执行,那么如何保证GC时所有线程都在安全点呢?
分为抢先式中断和主动式中断
抢先式中断

特点:不照顾线程感受
过程:GC时先中断所有的线程,然后让那些没有到安全点的线程自己再跑到安全点
使用:现在已经没有使用抢先式中断的了

主动式中断

特点:照顾线程感受,让它自己去吧
过程:设置一个GC标志,线程执行到安全点或创建对象分配内存时,主动去轮询这个标志,为真时就主动中断自己(这个时候是安全点,中断就中断呗)
使用:大家都说好

3.安全区域

  上面考虑到了多线程,但是没有考虑到线程在执行过程中可能会sleep或者block呀。如果等待它休眠结束或者CPU时间片分配过来,又是天荒地老了啊!所以引出安全区域的概念

定义:安全区域是指这一段代码中的引用关系不会发生变化
线程在安全区域行为本质上是一个握手过程
过程:
1.线程A进入safe region,设置一个标志Ready flag.
2.GC如果在线程A处于safe region的时间内进行,由于ready flag的存在,不再检查
线程A
3.线程A将要离开safe region时,轮询GC设置的标志,若为真表示GC还没有执行完,则线程A中断自己,保证自己不离开safe region。若此时GC已经执行完毕,则A顺利离开region。

总结

  本篇博客对对象存活判定算法和垃圾收集算法进行了学习,后面将会对垃圾收集器、对象的内存分配与回收策略以及JVM对以上算法的实现进行学习。

你可能感兴趣的:(JVM学习,JVM,学习)