【Java虚拟机探究】7.GC算法与种类(下)

上一篇我们讲解了“引用计数法”、“标记清除法”、“标记压缩法”以及“复制算法”这几种垃圾回收的算法。那这些算法都是如何识别一个垃圾对象的呢?换言之,是如何判定该对象是垃圾对象的呢?这里需要给出一个可触及性的定义。本篇主要讲解可触及性的相关知识。

一、什么是可触及性

可触及性分为“可触及的”、“可复活的”和“不可触及”的三种状态。(下面提到的finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。)

1.可触及的
代表从根节点开始进行扫描,在根节点的引用链条中含有这个对象,则说明这个对象就是可触及的对象。

2.可复活的
所谓“可复活的”的对象,就是说这个对象是现阶段不可触及的,但是它处于一个有可能会被再次触及的一个状态,像这样的对象也是不能做回收的,因为它有可能会“复活”。
一旦所有引用被释放,在没有执行Object类的finalize()方法之前,都是可复活状态,因为在finalize()方法中可能会复活该对象。

3.不可触及的
该对象不在根节点的应用链条中,并且也不可能再被其它对象所引用了。
在引用释放后并执行了Object类的finalize()方法后,该对象就是不可复活的,则这种对象就是真正意义上可回收的对象。

finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
下面是一个例子,我们通过重写Object类的finalize()方法,将一个“可复活”的类进行复活。
public class CanReliveObj {
    public static CanReliveObj obj;
    
	@Override//重写Object类的finalize方法
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("CanReliveObj finalize called");
		obj = this;//重新指定对象为当前类
	}
	
	@Override//重写Object类的toString方法
	public String toString() {
		return "I am CanReliveObj";
	}
    
	public static void main(String[] args) throws InterruptedException {
		obj = new CanReliveObj();
		obj = null;//可复活
		System.gc();//进行垃圾回收
		Thread.sleep(1000);
		if(obj == null){
			System.out.println("obj 是 null");
		}else{
			System.out.println("obj可用:"+obj.toString());
		}
		System.out.println("第二次gc");
		obj = null;//不可复活
		System.gc();//进行垃圾回收
		Thread.sleep(1000);
		if(obj == null){
			System.out.println("obj 是 null");
		}else{
			System.out.println("obj可用:"+obj.toString());
		}
	}
}
运行结果:

说明:
首先我们重写了Object类的finalize方法,当对象失去引用后会调用该方法,此时我们会给对象重新赋值,让它“复活”。重写toString方法为了证明该对象是可用的。之后在mian方法中首先为obj赋值一个对象,然后设置为null,此时obj即将要失去引用,但是在执行finalize之后,obj又被复制了一次,此时obj变为可达,gc不进行回收,所以第一次会显示obj可用。第二次的时候将obj重新赋值为null,然后进行垃圾回收,因为finalize方法只会调用一次,此时obj真正的失去了引用,被gc回收掉了,所以第二次是不可用的状态。

对于finalize方法,有以下建议:
(1)避免使用finalize(),可能会因为操作不慎导致错误,可以使用try-catch-finally来代替它。
(2)因为finalize()优先级比较低,系统自动发生gc是不确定的,所以finalize()被调用的时间也是不确定的。

二、根

在可触及性中有一个很重要的概念----根。对象可触及性的判断都是从根节点开始,检测在根节点的引用链上是否能找到这个对象。

一般来说,在栈中引用的对象,可以被看做根。前面提到过,一般栈存储的是方法区中的静态成员变量或者是常量引用的对象(全局对象)、JNI方法栈中引用的对象、所调用的函数的局部变量,这些对象都是正在被使用的,如果被他们引用的,都是可触及的,所以这些对象可以被作为根。

三、Stop-The-World

“Stop-The-World”是由gc垃圾回收所引起的一个现象,是Java中一种全局暂停的现象,会影响所有的线程。在全局停顿的过程中,所有Java代码停止,只有native代码可以执行,但是不能和JVM交互。

但“Stop-The-World”也不是完全由gc操作引起的,也存在于线程死锁检查、堆Dump或线程Dump引起的,这些情况都是人为编程引起的。

那么为什么gc会产生全局停顿?下面有一个形象的说法,来说明该问题:
例如在聚会的时候打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。


gc引起的全局停顿时间一般比较短,例如新生代的gc仅仅需要0.00几秒(因为使用了复制算法),而老年代的gc,因为使用的标记清除等算法可能会花费比较多的时间(100多秒甚至几十分钟,与堆的大小有关)。而gc花费时间很长的时候,危害是不容忽视的,它会使服务长时间停止,没有响应。遇到HA(High Available高可用)系统,一般HA系统是主机运行,备机不运行,一旦主机宕机了,备机自动启动。所以此时主机的全局停顿可能会引起主备切换,严重危害生产环境。

下面使用一个代码实例来演示全局停顿的效果:
import java.util.HashMap;

public class MyThread extends Thread{
    HashMap<Long,byte[]> map = new HashMap<Long,byte[]>();
	@Override
	public void run() {
		try {
			while(true){
				System.out.println("time:"+new SimpleDateFormat("ss.SSS") .format(new Date()));
				if(map.size()*512/1024/1024>=400){
					//当map大小大于400M时,清理内存
					System.out.println("===准备清理===:"+map.size());
					map.clear();
				}
				for(int i=0;i<1024;i++){
					map.put(System.nanoTime(), new byte[512]);
				}
				Thread.sleep(1);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
    
	public static void main(String[] args) {
		MyThread t = new MyThread();
		t.start();
	}
}
该代码是一个线程,每隔1毫秒进行一次map装填,当map大小大于450M的时候,清除map。所以该线程就是一个不断做内存消耗和内存清理的线程。
运行参数:

说明:
-Xmx512m -Xms512m:设置512M的堆和512兆的栈。
-XX:+UseSerialGC:设置使用串行垃圾回收器。这里串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。
-Xloggc:$CATALINA_BASE/gc.log:代表gc日志产生的路径。
-XX:+PrintGCDetails:打印GC详细信息。
-Xmn1m:设置年轻代为1M大小。
-XX:PretenureSizeThreshold=50:参数设定超过对象超过多少时,分配到老年代中,这里为50k。
-XX:MaxTenuringThreshold=1:用于控制新生代对象能经历多少次Minor GC才晋升到老年代,这里定义1次Minor GC就被定义为老年代。

运行结果:

从结果来看,应该不会有任何停顿产生,每次map填充操作的时间在0.002-0.003秒之间,而这组时间间隔:
time:09.373
time:09.853
明显花费的时间比日常操作要多,中间产生了大概0.48秒左右的停顿,这就是由gc引起的,那详细的原因是什么呢?我们来看一下打印出的gc日志(刷新一下工程,在工程的目录下):

可以看到,在该时刻发生了垃圾回收,继而发生了全局停顿的效果,停顿时间大概0.48秒左右。

转载请注明出处:https://blog.csdn.net/acmman/article/details/80631512

猜你喜欢

转载自blog.csdn.net/u013517797/article/details/80631512