垃圾收集器与内存分配策略(深入理解Java虚拟机笔记)

目录

概述

那些java堆对象需要回收?

引用计数算法和可达性回收算法

StrongReference、SoftReference、WeakReference、PhantomReference

不可达对象一定要“死”吗

回收方法区

垃圾回收算法

标记-清除算法

复制算法

标记-整理算法

分代收集算法

垃圾收集器

HotSpot垃圾收集算法实现的注意事项

内存分配和回收策略


概述

程序计数器、虚拟机栈(栈帧:局部变量表、操作数栈)、本地方法随线程而生灭;栈帧随方法的出入栈而生灭。这几个区域的内存和回收都具有确定性,不需要过多考虑回收问题。

而java堆区和 方法区,只有在运行期才决定分配了那些了内存?哪些内存需要回收?需要在何时回收?如何回收?

那些java堆对象需要回收?

引用计数算法和可达性回收算法

引用计数算法回收引用计数为0的对象,但对循环引用无效。

从而补充可达性回收算法:

可作为GCRoots的算法有以下几种:

栈帧中局部变量表中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中引用的对象。

StrongReference、SoftReference、WeakReference、PhantomReference

满足以上算法的可回收对象里有一些“鸡肋(食之无味,弃之可惜)”的对象,这些对象被回收我们感到真的有点可惜,所以题目中的引用“破土而出”:

强引用:类似这样“Object obj = new Object()”的obj引用。

软引用:它引用的对象在内存不够用时回收。

弱引用:它引用的对象只能生存到下一次垃圾收集发生之前,且回收时内存是否足够。

虚引用:它引用的唯一目的就是对象被回收时能收到一个系统通知。

不可达对象一定要“死”吗

满足回收算法的不可达对象要经历2次标记才会“死”去。

第一次:算法发现没有与GC Roots相连接的引用链,它会被第一次标记,并接着筛选;

筛选:筛选条件是有无必要执行finalize方法;有,则加入F-Queue队列,稍后虚拟机会自动建立低优先级的Finalizer线程去执行它(对象的finalize方法只会被jvm系统执行一次);没有覆盖finalize方法的、已执行过finalize方法的对象都是没必要执行的对象。

第二次:会对第一次标记过的对象,包括F-Queue中的对象,做第二次标记,标记后被回收。

我们可以在finalize方法里让this对象再次关联GC Roots类型的引用,可以从GC中“拯救”回来,例如:

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 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 = null;
		System.gc();
		// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
		Thread.sleep(500);
		if (SAVE_HOOK != null) {
			SAVE_HOOK.isAlive();
		} else {
			System.out.println("no, i am dead :(");
		}
	}
}

运行结果:

finalize mehtod executed!

yes, i am still alive :

no, i am dead :(

回收方法区

方法区的垃圾收集率低于java堆区。

回收的主要内容有:废弃常量和无用的类。

方法区中的常量池里的常量有字面量和符号引用,没有任何地方引用这个字面量(例如“abc”这个字符串字面量,没有任何String对象和其他地方引用),符号引用也通此理。

回收无用的类有3个苛刻的条件:

  1. java堆中不存在该类的实例
  2. 加载该类的ClassLoader已被回收
  3. 没有任何地方引用Class对象和该Class对象对应的其他反射成员

使用反射,动态代理,GCLib等的框架,都需要方法区内存的动态回收,以便方法区内存溢出。

垃圾回收算法

标记-清除算法

首先对象的标记判定阶段由引用计数为0和可达性算法完成,然后进入一一清除阶段。不足有2:

  1. 标记和清除2阶段的效率都不高;
  2. 标记清除之后产生大量内存碎片,致使分配较大对象时无法满足,从而导致一次垃圾回收行为。

可能的算法执行过程如图:

复制算法

为了解决回收效率,将内存分为等量的2分,一次使用其中一份,这份使用完时,把存活的对象复制到另一份上,再清除掉原来的那份以便下次使用。

优点:没有内存碎片,实现简单,运行高效;

不足:运行时内存缩小为了原来的一半。

可能的算法执行过程如图:

另,现在商用虚拟机都采用这种类似的复制算法来回收新生代,新生代中的大量对象(98%)存活时间很短,所以GC时(98%对象“已死”)需要复制到的空间(紧接着讲到的Survivor2)并不需要太大,所以一般会把内存划分为Eden、Survivor1和Survivor2,它们依次占比8:1:1,这样运行时内存占90%。

当然,我们不能保证每次都回收不多于10%的存活对象,所以多于10%的话,我们开启担保机制,让本次多于10%的存活对象进入老年代,具体执行规则见后文内存分配和回收策略部分。

标记-整理算法

如果对象存活率高的话,复制算法会有较多的复制操作,从而gc效率会降低,一般不适用于老年代区域。因此根据老年代特点,本节算法“破冰而出”。

标记过程和“标记-清除”算法的标记过程一样,但接着不直接清除标记的对象,而是所有存活的对象向内存的一端移动挨个排列,然后清除存活对象边界外的内存。

可能的算法执行过程如图:

分代收集算法

该算法把内存分为新生代和老年代,且新生代划分为Eden、Survivor1和Survivor2几个区域。复制算法可以在新生代 “施展拳脚”,老年代对象存活率高,无额外空间担保分配,可以采用“标记-清除”和“标记-整理”算法回收。

垃圾收集器

以上垃圾收集算法是内存回收的方法论,而垃圾收集器是内存回收的具体实现。

Hotspot包含的垃圾收集器,如图:

上图的连线说明了7种收集器之中几种可以搭配在新生代和老年代上进行内存回收。CMS和G1较复杂,其中G1可以同时收集新生代和老年代的垃圾。

具体原理见书。

HotSpot垃圾收集算法实现的注意事项

可达性对象判活算法中需要枚举GC Roots对象,而GC Roots对象只要在全局性引用(常量对象或类型静态对象)上和栈帧中的局部变量表中。而栈中的方法很多的情况下,站里的引用也多,那么要枚举GC Roots对象必然会消耗很多时间,为了解决这种情况,OopMap数据结构被设计出来承载GC Roots对象的引用,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用,这样GC在扫描时就可以直接通过OopMap得知这些GC Roots对象了。

GC时,会“Stop The World”,至少在枚举GC Roots时“Stop The World”。

要想GC,就需要让所有用户线程“跑”到安全点上暂停,这样才能让对象引用关系处于不再变化的情况,从而让GC可以枚举所有GC Roots找出不可达对象。

如果用户线程处于sleep或blocked状态,是无法让他们“跑”到安全点上暂停的,所以针对这种情况引入安全区域的概念。

安全区域是指一段代码片段之中,引用关系不会发生变化。

在用户线程要离开Safe Region时,它要检查GC是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

内存分配和回收策略

新生代GC,例如,大多数情况下,对象在新生代Eden区中分配,当Eden区内存不够时,发生Minor GC(新生代GC)。

老年代GC,英文称:Major GC或Full GC,顾名生意发生在老年代。

JVM的启动参数-XX:PretenureSizeThreshold意思是当对象需要分配的内存大于该值时,对象直接分配到老年代,该参数只对Serial和ParNew两款收集器有效。

         在第一次GC时,对象只会在Eden区和Survivor1区,Survivor2区是空的,紧接着Eden区中所有存活的对象都会被复制到Survivor2区, Survivor1区存活对象也会被复制到Survivor2区,并设置Survivor2区所有存活对象的年龄为1,然后清理Eden区和Survivor1区。

下一次GC时,在Eden区存活的对象被复制到Survivor1区,而此时Survivor2区所有存活的对象根据他们的年龄值(此时年龄为1)来决定去向,年龄达到一定值(年龄阈值,可以通过JVM启动参数-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到Survivor1区域,并设置Survivor1区所有存活对象的年龄为原来年龄+1,最后清理Eden区和Survivor2区。若此时若MaxTenuringThreshold不等于1,Survivor2区所有存活的对象就都被复制到Survivor1区域。

同理,Survivor1区和Survivor2区在每次GC时交换使用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值