“深入理解JVM:垃圾收集器与内存分配策略”的版本间差异

来自Wikioe
跳到导航 跳到搜索
第247行: 第247行:
== HotSpot的算法细节实现 ==
== HotSpot的算法细节实现 ==


3.4.1 根节点枚举
=== 根节点枚举 ===
3.4.2 安全点
 
3.4.3 安全区域
=== 安全点 ===
3.4.4 记忆集与卡表
 
3.4.5 写屏障
=== 安全区域 ===
 
=== 记忆集与卡表 ===
 
=== 写屏障 ===


== 经典垃圾收集器 ==
== 经典垃圾收集器 ==

2020年10月24日 (六) 19:37的版本


概述

垃圾收集(Garbage Collection,GC),并非Java语言的伴生产物。


垃圾收集需要完成的三件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

 

对象已死?

引用计数算法(Reference Counting)

引用计数算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

  • 优点:原理简单,判定效率高;
  • 缺点:不能解决对象之间相互循环引用的问题;
    /**
    * testGC()方法执行后,objA和objB会不会被GC呢?
    * @author zzm
    */
    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();
       }
    }
    

可达性分析算法(Reachability Analysis)

  • 当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

可达性分析算法:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

利用可达性分析算法判定对象是否可回收.jpg


Java中可作为“GC Roots”的对象:【看起来能从它往下找的对象】

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象
    譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象
    譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象
    譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  • Java虚拟机内部的引用;【?】
    如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。【???】
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。【?】

再谈引用(强、软、弱、虚)

判定对象是否存活都和“引用”离不开关系。JDK 1.2版之后,Java将引用分为强、软、弱、虚四种:

  1. 强引用(Strongly Reference):是指在程序代码之中普遍存在的引用赋值,即类似“Objectobj = new Object()”这种引用关系。
    只要强引用关系还存在,就永远不会回收掉被引用的对象。
  2. 软引用(Soft Reference):JDK 1.2版之后提供了“SoftReference”类来实现软引用。
    在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  3. 弱引用(Weak Reference):在JDK 1.2版之后提供了“WeakReference”类来实现弱引用。
    被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
  4. 虚引用(Phantom Reference):也称为“幽灵引用”或者“幻影引用”,在JDK 1.2版之后提供了“PhantomReference”类来实现虚引用。
    虚引用的对于对象的生存时间没有影响,也无法通过虚引用来取得一个对象实例。(唯一作用是在对象被收集器回收时收到一个系统通知)

生存还是死亡?(两次标记)

真正宣告一个对象死亡,至少要经历两次标记过程:

  1. 标记:对象在进行可达性分析后发现没有与GC Roots相连接的引用链,将会被第一次标记;
  2. 筛选(对标记的对象):筛选的条件是此对象是否有必要执行“finalize()”方法;
    1. “没有必要执行”:【对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过】直接回收;
    2. “有必要执行”:将该对象放如“F-Queue”队列中,稍后由“Finalizer”(虚拟机自动建立的低优先级)线程去调用对象的“finalize()”方法;
      并不承诺一定会等待对象的“finalize()”方法运行结束(防止等待造成回收系统崩溃);
  3. 标记(对F-Queue中的对象):被标记的对象会被回收;


  • finalize()方法中:如果对象重新与引用链上的对象建立了关联【如把自己(this关键字)赋值给某个类变量或者对象的成员变量】,那么在第二次标记时它将被移出“即将回收”的集合;
  • finalize()方法的不确定性大:不确定是否一定被调用,不确定何时被调用;所以应尽量避免使用;


一次对象自我拯救的演示:

/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
* @author zzm
*/
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 method 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 :(");
		}
		
		// 下面这段代码与上面的完全相同,但是这次自救却失败了
		// (finalize已被执行过,筛选后直接被回收)
		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 method executed!
yes, i am still alive :)
no, i am dead :(

回收方法区

方法区的垃圾收集主要回收两部分内容:

  1. 废弃常量:没有被对象或对象属性引用的常量(与回收Java堆中的对象类似);
  2. 不再使用的类:需要同时满足下面三个条件:
    1. 该类所有的实例都已经被回收;
      也就是Java堆中不存在该类及其任何派生子类的实例。
    2. 加载该类的类加载器已经被回收;
      (这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。)
    3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
    • Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。


关于是否要对类型进行回收,HotSpot虚拟机提供了“-Xnoclassgc”参数进行控制,还可以使用“-verbose:class”以及“-XX:+TraceClass-Loading”、“-XX:+TraceClassUnLoading”查看类加载和卸载信息,其中“-verbose:class”和“-XX:+TraceClassLoading”可以在Product版的虚拟机中使用,“-XX:+TraceClassUnLoading”参数需要FastDebug版的虚拟机支持。
  • 在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

【???】

垃圾收集算法

标记-清除算法(Mark-Sweep)


“半区复制”(Semispace Copying):算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

“标记-清除”算法示意图.jpg


问题:

  1. 执行效率不稳定:随对象数量增长而降低;
  2. 内存空间碎片化:标记、清除之后会产生大量不连续的内存碎片,导致分配较大对象时无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。

标记-复制算法(Semispace Copying)


算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,另一块用于。

标记-复制算法示意图.jpg


问题:

  1. 可用内存缩小为了原来的一半;

“Appel式回收”

Appel式回收:

  1. 把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。
  2. 发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。


  • (“Appel式回收”:现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代);
    如:HotSpot虚拟机的Serial、ParNew等新生代收集器。
  • HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1
  • 当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(老年代)进行“分配担保”(Handle Promotion);

标记-整理算法(Mark-Compact)


算法:

  1. 标记:过程仍然与“标记-清除”算法一样,
  2. 整理:将所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
标记-复制算法示意图.jpg


问题:

  1. 移动操作必须全程暂停用户应用程序才能进行(“Stop The World”);

分代收集理论(Generational Collection)


  • (当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计);
  • (除了分代,已有分区收集的理论:即将内存划分为多个区,分别进行内存管理);


分代收集理论建立在两个分代假说之上:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的;
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡;
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

由此引出了垃圾收集器的一致的设计原则:“收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储”。


对不同的区域“”

  1. 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集;
  2. 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。
    • 目前只有“CMS”收集器会有单独收集老年代的行为。
    • 另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
  3. 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。
    • 目前只有“G1”收集器会有这种行为。
  4. 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。“Full GC”:;


  • 通常能单独发生收集行为的只是新生代,实际上除了CMS收集器,其他都不存在只针对老年代的收集


关于“跨代引用”:

   依据这条假说(跨代引用假说),我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。
   
   此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

即,新生代维护了“记忆集”(Remembered Set),来标识老年代中存在跨代引用的内存块,用于Minor GC时被加入扫描整理范围。

HotSpot的算法细节实现

根节点枚举

安全点

安全区域

记忆集与卡表

写屏障

经典垃圾收集器

3.5.1 Serial收集器 3.5.2 ParNew收集器 3.5.3 Parallel Scavenge收集器 3.5.4 Serial Old收集器 3.5.5 Parallel Old收集器 3.5.6 CMS收集器 3.5.7 Garbage First收集器

低延迟垃圾收集器

3.6.1 Shenandoah收集器 3.6.2 ZGC收集器

选择合适的垃圾收集器

3.7.1 Epsilon收集器 3.7.2 收集器的权衡 3.7.3 虚拟机及垃圾收集器日志 3.7.4 垃圾收集器参数总结

内存分配与回收策略

 大对象优先在Eden分配 3.8.2 大对象直接进入老年代 3.8.3 长期存活的对象将进入老年代 3.8.4 动态对象年龄判定 3.8.5 空间分配担保