JVM判断对象已死的方法和垃圾收集算法

摘自<<深入理解JAVA虚拟机>> 周志明著

文章目录


####概述
垃圾收集(Garbage Collection ,GC)。
GC需要完成的三件事

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

内存的动态分配和回收技术已经比较成熟了,了解GC和内存分配是为了帮助我们在排查内存溢出内存泄露提供理论支撑,当垃圾收集成为系统达到高并发的瓶颈时,我们就需要对这些自动化和内存分配合GC进行适当的监控和调节。

我们知道Java在运行时区域分为各个不同的部分,其中程序计数器、虚拟机栈、本地方法栈这三个区域随着线程而生随着线程而死,栈中的栈帧随着方法的进入和退出有序的进行入栈和出栈的过程,因此这这三个部分不需要考虑太多的GC回收问题。但是java堆和方法区就不一样了,接口的实现多样化和一个方法的多个分支需要的内存都是不确定的,只有在运行的时候才能知道需要的内存大小,这两个部分的内存是动态分配的,GC的回收也主要是针对这个部分。

对象死了还是活着
java堆几乎存放着所有的对象实例,GC在收集之前需要先判断这些对象是否存活,死了的对象才能被回收。

####判断对象是否存活的方法

#####引用计数算法
引用计数算法很简单,就是给对象添加一个引用计数器,每个地方引用的时候就+1,当引用失效的时候就-1,当引用计数器的数字是0的时候就表示没有被引用了,也就是不在使用的对象,可以理解为可以被回收的对象了,但是在jvm里面不会用引用计数算法,因为引用计数算法很难解决java对象互相引用的问题,如以下代码:

package com.madman.base;

public class ReferenceCountingGC {

	public Object instance = null;

	private static final int _1MB = 1024 * 1024;

	/**
	 * 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
	 */
	private byte[] bigSize = new byte[2 * _1MB];

	public static void testGC() {
		ReferenceCountingGC objA = new ReferenceCountingGC();
		ReferenceCountingGC objB = new ReferenceCountingGC();
		objA.instance = objB;
		objB.instance = objA;

		objA = null;
		objB = null;

		// 假设在这行发生GC,objA和objB是否能被回收?
		System.gc();
	}

	public static void main(String[] args) {
		testGC();
		System.out.println("运行结束");
	}
}

运行时打印GC信息,配置参数

-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps

GC日志,查看是否被回收~~

0.142: [GC (System.gc()) [PSYoungGen: 6717K->632K(76288K)] 6717K->640K(251392K), 0.0017775 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.144: [Full GC (System.gc()) [PSYoungGen: 632K->0K(76288K)] [ParOldGen: 8K->558K(175104K)] 640K->558K(251392K), [Metaspace: 2703K->2703K(1056768K)], 0.0074551 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
运行结束
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b500000,0x000000076b6eb9e0,0x000000076f500000)
  from space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
  to   space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
 ParOldGen       total 175104K, used 558K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000)
  object space 175104K, 0% used [0x00000006c1e00000,0x00000006c1e8b828,0x00000006cc900000)
 Metaspace       used 2711K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

根据上面的日志我们可以得知gc回收发挥了作用,并且根据名字我们可知道这里用的垃圾收集器是(Parallel Scavenge)因为他的新生代名字是PSYoungGen,也从侧面反映了java没有使用引用计数算法.

#####根搜索算法(GC Roots Tracing)
根搜索算法就是通过一系列的名为(GC Roots)的对象为起点,从这些节点开始向下搜索,搜索所走过的路劲为引用链(Reference Chain),当一个对象到Gc Roots没有任何引用链相连(可以理解为GC Roots 到对象之间没有相连的线),这样就说明这个对象没有被引用了,这时就可以被回收了。

在java语言里,可作为Gc Roots的对象包括以下几种情况

  • 虚拟机栈(栈帧中的本地变量表)中的引用对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈JNI(也就是大家说的native方法)的引用对象

####java的引用分类
java中的引用可理解为如果reference类型的数据中存储的数值代表的另外一块内存的起始地址,就称这块内存代表着一个引用。

java引用参考链接
Java四种引用包括强引用,软引用,弱引用,虚引用。
java强引用,软引用,弱引用,虚引用

java引用经过不断的扩展,可分为强引用,软引用,弱引用,虚引用,引用强度一次从高到弱。

#####强引用
强引用就是在程序代码中普遍存在的类似"Object obj=new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收被引用的对象。
#####软引用
软引用的强度是仅次于强引用的,如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可使用SoftReference来实现。
#####弱引用
弱引用的强度比软引用更次,也就是说只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 WeakReference 来标记此对象。
#####虚引用
虚引用顾名思义就是形同虚设,虚引用也称为幻影引用:一个对象是都有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用。

####finalize方法
finalize方法是给对象一次跳脱GC回收的复生符,在对象被GC的时候出发对象的finalize方法(前提是对象实现了finalize方法)。但是finalize方法只能被使用一次,第二次的时候还是逃脱不了被GC的厄运。
测试代码:

package com.madman.base;

public class FinalizeEscapeGC {

	public static FinalizeEscapeGC SAVE_HOOK = null;

	public void isAlive() {
		System.out.println("yes, i am still alive :)");
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("finalize mehtod executed!");
		FinalizeEscapeGC.SAVE_HOOK = this;
	}

	public void print() {
		System.out.println("打印方法");
	}

	public static void main(String[] args) throws Throwable {
		SAVE_HOOK = new FinalizeEscapeGC();

		// 对象第一次成功拯救自己
		SAVE_HOOK = null;
		System.gc();
		// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
		Thread.sleep(500);
		if (SAVE_HOOK != null) {
			SAVE_HOOK.isAlive();
		} else {
			System.out.println("no, i am dead :(");
		}
		SAVE_HOOK.print();
		// 下面这段代码与上面的完全相同,但是这次自救却失败了
		SAVE_HOOK = null;
		System.gc();
		// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
		Thread.sleep(500);
		if (SAVE_HOOK != null) {
			SAVE_HOOK.isAlive();
		} else {
			System.out.println("no, i am dead :(");
		}
		SAVE_HOOK.print();

	}
}

结果

finalize mehtod executed!
yes, i am still alive :)
打印方法
no, i am dead :(
Exception in thread "main" java.lang.NullPointerException
	at com.madman.base.FinalizeEscapeGC.main(FinalizeEscapeGC.java:46)

PS:如果注释掉27行的System.gc(); 那它就不会触发finalize方法了,从侧面证明finalize方法只有在被GC的时候才会被触发。

####垃圾收集算法
#####标记-清除算法

最基础的收集算法是"标记-清除",该算法分为"标记"、"清除"两个步骤,首先标志出所有需要被回收的对象,在标记完成后统一回收掉所有被虚拟机标记的对象,这是最基础的算法,在GC中基本很少使用,但是很多都是基于这种算法进行优化的,标记清除算法存在两个问题是:效率问题和空间问题,标记和清除的过程都需要消耗很多的时间,空间问题主要是清除对象之后会产生很多不连续的的内存碎片,并且如果程序需要产生大对象的时候无法找到足够的连续内存而不得不提前触发一次GC操作,导致GC很频繁。

#####复制算法
在标记清除算法中效率是个大问题,因此出现了一个复制算法,它将内存按照容量的大小划分大小相等的两块,每次只适用其中的一块,当这一块使用完了就复制存活的对象到另外一块上,然后把使用过的内存空间一次性清掉,这样每次都是对其中的一块进行内存回收,就不用考虑内存碎片的问题了,只需要移动对指针,按照顺序分配内存就行,简单高效,但是这样每次只能使用一般的内存,内存的使用效率打了半折了~~~~,内存缩小为原来的一半了,代价太高

现在的商业虚拟机大部分都是采用这种垃圾收集算法,通过研究表明,新生代中的对象98%都是朝生夕死的,所有并不需要按照1:1的比例分配内存,而是将内存划分为一块较大的Eden空间和两块比较小的Surivivor空间,每次使用Eden空间和另外一个小的Survivor空间,当GC的时候把Eden和Survivor的存活对象放到另外一个Survivor中去,但是如果存活的对象大小超过另外一个Survivor的大小的时候就需要依赖其他内存(这里说的老年代)进行担保(Handle Promotion),也就是把存活对象放到老年代去了。(这里有个疑问是所有存活的对象都放到老年代去了,还是剩下的那个Survivor装不下的对象才放到老年代去了???)

#####标记整理算法
复制算法在对象存活率很高的时候需要进行较多的复制操作,效率会变低,并且要想不浪费50%的空间,按照比例8:1:1来分配的时候还需要老年代作为担保人以保证所有存活对象有可存放的地方,但是老年代如果也采用这种方法,那谁作为他的担保人呢,因此老年代一般不会使用这种收集机制。

根据老年代的特点,提出的另外一种算法是"标记-整理(Mark-Compact)"算法,标记过程和"标记-清除"算法一样,标记完之后把所有存活的对象都往一端移动,然后直接清理掉边界以外的内存。

#####分代收集算法
JVM的垃圾收集都是采用"分代-收集(Generational Collection)“算法,根据对象的存活周期的不同奖内存划分为几块,一般把java堆划分为新生代和老年代,这样就可根据不同年代的特点采用最合适的收集算法,新生代中,对象朝生夕死这种特点就采用复制算法,老年代中因为对象存活率高,没有额外空间为他担保就采用"标记-清除”、"标记-整理"算法进行回收。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值