• 程序计数器、 虚拟机栈、 本地方法栈 3 个区域随线程而生,随线程而灭;因此在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
  • 垃圾收集器所关注的是堆内存

# 确定对象状态(标记)

# 引用计数算法

  • 通过给对象添加引用计数器,记录对象被引用的状态,当引用数量为 0 时,该对象就处于不可再被使用状态。
  • COM(Component Object Model)技术、 ActionScript 3 的 FlashPlayer、Python 语言等都使用了引用计数算法进行内存管理。
  • 该算法很难解决对象之间相互循环引用的问题。

# 可达性分析算法

# 当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

可达性分析算法判定对象是否可回收

# 可作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
  • Java 虚拟机内部的引用, 如基本数据类型对应的 Class 对象, 一些常驻的异常对象(比如 NullPointExcepiton、 OutOfMemoryError) 等, 还有系统类加载器。
  • 所有被同步锁(synchronized 关键字) 持有的对象。
  • 反映 Java 虚拟机内部情况的 JMXBean、 JVMTI 中注册的回调、 本地代码缓存等。

# jdk 引用的演化

# 在 JDK 1.2 以前如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

  • 定义太过狭隘一个对象在这种定义下只有被引用或者没有被引用两种状态。

# 在 JDK 1.2 之后 (强度依次逐渐减弱)

  • 强引用(StrongReference)
    强引用就是指在程序代码之中普遍存在的,类似 “Object obj=new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用(Soft Reference)
    软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。jdk 提供了 SoftReference 类来实现软引用。
  • 弱引用(Weak Reference)
    弱引用关联的对象只能生存到下一次垃圾收集发生之前当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。jdk 提供了 WeakReference 类来实现弱引用。
  • 虚引用(PhantomReference)
    虚引用也称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。jdk 提供了 PhantomReference 类来实现虚引用。

# 宣告对象死亡的过程

# 第一次标记(在可达性分析算法中不可达的对象(失去与 GC Roots 的引用链),将被进行第一次筛选,是否有必要执行 finalize()方法。)

# 没有必要执行(两种情况)
  • 对象没有覆盖 finalize()方法
  • finalize()方法已经被虚拟机调用过
# 有必要执行
  • 这个对象将会放置在 F-Queue 的队列之中
  • 虚拟机自动建立低优先级的 Finalizer 线程去触发对象 finalize()方法(并不承诺会等待它运行结束)

# 第二次标记(GC 将对 F-Queue 中的对象进行第二次小规模的标记)

  • 与第一次标记同样的做法。
  • 如果对象在 finalize()方法中重新与引用链上任何一个对象建立关联(譬如把自己(this 关键字)赋值给某个类变量或者对象的成员变量), 此时它将被移除出 “即将回收” 的集合
  • 如果标记完后对象还是没有必要执行,就真正被回收掉了。

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

/**
  * 此代码演示了两点: 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 mehtod executed!");
 		FinalizeEscapeGC.SAVE_HOOK = this;
 	}
 
 	public static void main(String[] args) throws Throwable {
 		SAVE_HOOK = new FinalizeEscapeGC();
 		// 对象第一次成功拯救自己
 		SAVE_HOOK = null;
 		System.gc();
 		// 因为 finalize 方法优先级很低,所以暂停 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();
 		// 因为 finalize 方法优先级很低,所以暂停 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:(
  • 任何一个对象的 finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize()方法不会被再次执行。
  • finalize()能做的所有工作,使用 try-finally 或者其他方式都可以做得更好、 更及时。

# 回收方法区

# 废弃常量
  • 回收废弃常量与回收 Java 堆中的对象非常类似。如果一个常量、字面量没有被其他任何地方引用,将会被系统清理出常量池( 常量池中的其他类(接口)、 方法、 字段的符号引用也与此类似)
# 无用的类(类需要同时满足下面 3 个条件才能算是 “无用的类”):
  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该
    类的方法。

是否对类进行回收,HotSpot 虚拟机提供了 - Xnoclassgc 参数进行控制。
在大量使用反射、 动态代理、 CGLib 等 ByteCode 框架、 动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

# 垃圾收集算法(追踪式垃圾收集)

# 标记 - 清除算法

  • 最基础的收集算法
  • 如同它的名字一样,算法分为 “标记” 和 “清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
  • “标记-清除”算法示意图

# 复制算法

  • 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。(缺点是需要牺牲一半的内存)
  • 由于新生代中的对象大多数都是 “朝生夕死” 的,因此可将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
  • HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1.
  • 我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
  • 在对象存活率较高时就要进行较多的复制操作,效率将会变低。在老年代一般不能直接选用这种算法。
  • 复制算法示意图

# 标记 - 整理算法

  • “标记-整理”算法示意图
  • 标记过程仍然与 “标记 - 清除” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

# 分代收集理论

遵循三条经验法则:

  • 弱分代假说(Weak Generational Hypothesis) : 绝大多数对象都是朝生夕灭的。
  • 强分代假说(Strong Generational Hypothesis) : 熬过越多次垃圾收集过程的对象就越难以消亡。
  • 跨代引用假说(Intergenerational Reference Hypothesis) : 跨代引用相对于同代引用来说仅占极少数。
  • 一般至少会把 Java 堆划分为新生代(Young Generation) 和老年代(Old Generation) 两个区域

分代 GC 名词

  • 部分收集(Partial GC) : 指目标不是完整收集整个 Java 堆的垃圾收集。(部分区域收集时,会面临跨代引用问题。在新生代使用记忆集解决)
  • 新生代收集(Minor GC/Young GC) : 指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC) : 指目标只是老年代的垃圾收集。(目前只有 CMS 收集器会有单独收集老年代的行为。)
  • 混合收集(Mixed GC) : 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有 G1 收集器会有这种行为。
  • 整堆收集(Full GC) : 收集整个 Java 堆和方法区的垃圾收集。

# 新生代

  • 每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

# 老年代

  • 对象存活率高、 没有额外空间对它进行分配担保,就必须使用 “标记 — 清理” 或者 “标记 — 整理” 算法来进行回收。(常采用 “标记 — 整理”)

# HotSpot 的算法实现

# 枚举根节点

  • 可达性算法中不可以出现分析过程中对象引用关系还在不断变化的情况。为确保在一致性的快照中进行 GC,进行 GC 时必须停顿所有 Java 执行线程(Sun 将这件事情称为 “Stop The World”)
  • 目前的主流 Java 虚拟机使用的都是准确式 GC。
  • HotSpot 实现中,把对象内什么偏移量上是什么类型的数据计算出来,在 JIT 编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

# 安全点(Safepoint)

  • HotSpot 也的确没有为每条指令都生成 OopMap,只是在 “特定的位置” 记录了这些信息,这些位置称为安全点。
  • 程序执行时并非在所有地方都能停顿下来开始 GC,只有在到达安全点时才能暂停。
  • 以程序 “是否具有让程序长时间执行的特征” 为标准进行选定安全点。“长时间执行” 的最明显特征就是指令序列复用,例如方法调用、 循环跳转、 异常跳转等,所以具有这些功能的指令才会产生 Safepoint。

# 如何在 GC 发生时让所有线程(这里不包括执行 JNI 调用的线程)都 “跑” 到最近的安全点上再停顿下来

  • 抢先式中断(Preemptive Suspension)(几乎没有使用)
    在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它 “跑” 到安全点上。
  • 主动式中断(Voluntary Suspension)
    当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

# 安全区域(Safe Region)

  • 线程处于 Sleep 状态或者 Blocked 状态,这时候线程无法响应 JVM 的中断请求,“走” 到安全的地方去中断挂起,JVM 也显然不太可能等待线程重新被分配 CPU 时间。
  • 安全区域是指在一段代码片段之中,引用关系不会发生变化。 在这个区域中的任意地方开始 GC 都是安全的。
  • 在线程执行到 Safe Region 中的代码时,首先标识自己已经进入了 Safe Region,那样,当在这段时间里 JVM 要发起 GC 时,就不用管标识自己为 Safe Region 状态的线程了。 在线程要离开 Safe Region 时,它要检查系统是否已经完成了根节点枚举(或者是整个 GC 过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开 Safe Region 的信号为止。

# 记忆集(Remembered Set)与卡表

  • 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
  • hotSpot 虚拟机中记忆集的实现方式:卡表(Card Table)(每个记录精确到一块内存区域, 该区域内有对象含有跨代指针。)
hotSpot中卡表实现简述
  • HotSpot 默认的卡表标记逻辑:CARD_TABLE [this address>> 9] = 0;
  • 字节数组 CARD_TABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块(称为卡页,大小为 >>9 即 512 字节)。
  • 只要卡页内有一个(或更多) 对象的字段存在着跨代指针,对应的卡表就会数组元素值标为 1(元素变脏),否则标为 0。
  • 在垃圾收集发生时,只需要筛选出卡表中变脏的元素,就能得出哪些卡页内存块中包含跨代指针,将其加入 GCRoot 一起扫描。
  • hotSpot 通过写屏障维护卡表状态。虚拟机层面对 “引用类型字段赋值” 这个动作的 AOP 切面,分为写前屏障和写后屏障。(应用写后屏障更新卡表)
  • 高并发场景下卡表面临 “伪共享” 问题。(现代 cpu 缓存系统以缓存行为单位存储,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行, 就会彼此影响(写回、 无效化或者同步) 而导致性能降低)
  • JDK 7 之后可以使用 - XX: +UseCondCardMark 决定是否开启卡表更新的条件判断。 开启会增加一次额外判断的开销, 但能够避免伪共享问题(只有当该卡表元素未被标记过时才将其标记为变脏)。
三色标记法
  • 黑色(已访问过,并且所有引用均扫描过)、灰色(已被访问过,但至少存在一个未被扫描过的引用)、白色(尚未被垃圾收集器访问过)。

并发扫描时对象消失问题

  • 增量更新(黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了)
  • 原始快照(无论引用关系删除与否, 都会按照刚刚开始扫描那一刻的对象图快照来进行搜索)
  • 虚拟机通过写屏障操作记录引用关系的插入或删除。
  • CMS 基于增量更新做并发标记,G1、 Shenandoah 则是用原始快照来实现。

# HotSpot 虚拟机垃圾收集器

HotSpot虚拟机的垃圾收集器
如果两个收集器之间存在连线,就说明它们可以搭配使用。 虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

# Serial 收集器(Stop The World)

Serial/Serial Old收集器运行示意图

  • 依然是虚拟机运行在 Client 模式下的默认新生代收集器.

# ParNew 收集器(并发(Concurrent)收集器)

ParNew/Serial Old收集器运行示意图

  • 是许多运行在 Server 模式下的虚拟机中首选的新生代收集器。除了 Serial 收集器外,能与 CMS 收集器配合工作的收集器。
  • ParNew 收集器也是使用 - XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用 - XX:+UseParNewGC 选项来强制指定它。
  • 在 CPU 非常多的环境下,可以使用 - XX:ParallelGCThreads 参数来限制垃圾收集的线程数。

# 并发和并行

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个 CPU 上。

# Parallel Scavenge 收集器(新生代收集器)

  • Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。
  • 吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
  • 控制最大垃圾收集停顿时间的 - XX:MaxGCPauseMillis 参数(大于 0 的毫秒数)
  • 直接设置吞吐量大小的 - XX:GCTimeRatio 参数(大于 0 且小于 100 的整数,垃圾收集时间占总时间的比率,相当于是吞吐量的倒数)
  • “吞吐量优先” 收集器。
  • GC 自适应的调节策略开关(GC Ergonomics)-XX:+UseAdaptiveSizePolicy

# Serial Old 收集器(Serial 收集器的老年代版本)

  • 同样是一个单线程收集器,使用 “标记 - 整理” 算法。
  • Serial/Serial Old收集器运行示意图

# Parallel Old 收集器(Parallel Scavenge 收集器的老年代版本)

  • 使用多线程和 “标记 - 整理” 算法。
  • 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
  • Parallel Scavenge/Parallel Old收集器运行示意图

# CMS 收集器(Concurrent Mark Sweep)

  • 是一种以获取最短回收停顿时间为目标、基于 “标记 — 清除” 算法实现的收集器。

# 运作过程

# 初始标记(CMS initial mark)
  • Stop The World
  • 标记一下 GC Roots 能直接关联到的对象,速度很快
# 并发标记(CMS concurrent mark)
  • 进行 GC RootsTracing 的过程(可达性分析,找出存活的对象)
# 重新标记(CMS remark)
  • Stop The World
  • 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
    Concurrent Mark Sweep收集器运行示意图
# 并发清除(CMS concurrent sweep)
# 并发低停顿收集器(Concurrent Low Pause Collector)

# CMS3 个明显的缺点

  • CMS 收集器对 CPU 资源非常敏感。它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量会降低。CMS 默认启动的回收线程数是(CPU 数量 + 3)/4,也就是当 CPU 在 4 个以上时,并发回收时垃圾收集线程不少于 25% 的 CPU 资源,并且随着 CPU 数量的增加而下降。 但是当 CPU 不足 4 个(譬如 2 个)时,CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%.
  • CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现 “Concurrent ModeFailure” 失败而导致另一次 Full GC 的产生。由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。 这一部分垃圾就称为 “浮动垃圾”。

Concurrent ModeFailure 产生原因:CMS 在垃圾收集阶段需要确保用户线程运行,就需要预留足够的空间给用户线程使用。当老年代使用空间达到收集启动阈值(-XX:CMSInitiatingOccupancyFraction)时,会被激活进行。在老年代增长不是太快情况下,适当调高阈值可以降低内存回收次数提高性能。但此时如果 CMS 运行期间预留的内存无法满足程序需要,就会出现该错误(同时虚拟机临时启动 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了)。因此阈值设置的太高,很容易导致这个错误出现,性能反而降低。

  • CMS 基于标记 - 清除算法,则无法避免在收集结束时有大量的空间碎片产生,导致缺少连续空间用来分配大对象。

使用 - XX:+UseCMSCompactAtFullCollection 开关参数(默认就是开启的)用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程(无法并发将延长停顿时间)。
还可以使用 - XX:CMSFullGCsBeforeCompaction 参数(默认值是 0,即每次 GC 后都进行碎片整理),设置执行 N 次不压缩的 Full GC 后,执行一次碎片整理。

# G1 收集器(Garbage-First)

  • G1 是一款面向服务端应用的垃圾收集器。

# G1 收集器的特点

  • 并行与并发
    G1 能充分利用多 CPU、 多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
  • 分代收集
    G1 可以独立管理整个 GC 堆,并采用不同方式处理新创建的对象和已经存活了一段时间、 熬过多次 GC 的旧对象以获取更好的收集效果。
  • 空间整合
    从整体来看是基于 “标记 — 整理” 算法实现的收集器,从局部(两个 Region 之间)上来看是基于 “复制” 算法实现的。因此 G1 收集器运作期间不会产生内存空间碎片,收集后提供规整的可用内存。
  • 可预测的停顿
    除追求低停顿外,还能建立可预测的停顿时间模型。(能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒)。
  • 与其他收集器不同,G1 通过把整个 java 堆内存分为多个大小相等的区域(Region),并保留有新生代和老年代的概念,用来描述一部分 Region(不需要连续)的集合。
  • G1 收集器通过建立可预测的停顿时间模型,有计划地避免在整个 java 堆中进行全区域垃圾收集:
    G1 通过跟踪各个 Region 里面垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的比值)在后台维护一个优先列表,根据允许的收集时间回收价值最大的 Region(Garbage-First)。保证了 G1 在有限时间内可以获取尽可能高的收集效率。
  • G1 在每个 Region 中使用 Remembered Set 记录该区域内对象的引用信息,在 GC 根节点的枚举范围中加入 Remembered Set 来避免全堆扫描 Region 之间、其他收集器中新生代、老年代之间对象引用。
    当虚拟机发现程序在对 Reference 类型数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。

# 运作过程(不计算维护 Remembered Set 的操作)

G1收集器运行示意图

# 初始标记(Initial Marking)

标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这阶段需要停顿线程,但耗时很短。

# 并发标记(Concurrent Marking)

从 GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

# 最终标记(Final Marking)

修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行。

# 筛选回收(Live Data Counting and Evacuation)

首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划(可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。)

# GC 日志分析(GC 日志格式随收集器不同会有差异,但也有一定共性)

33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs]
  • GC 发生距虚拟机启动 33.125 秒时,是一次普通 GC(区别 Full GC(老年代 GC(Major GC/Full GC)) ->“Stop-The-World”),所属区域为 Serial 收集器(新生代 “DefaultNew Generation”->“[DefNew”,ParNew 收集器新生代名称就会变为 “Parallel New Generation”->“[ParNew”,Parallel Scavenge 收集器新生代称为 “PSYoungGen”),GC 前该内存区域已使用容量为 3324K>GC 后该内存区域已使用容量为 152K(该内存区域总容量为 3712K),该内存区域 GC 占用时间为 0.0025925 secs(秒)。GC 前 Java 堆已使用容量 3324K->GC 后 Java 堆已使用容量 152K(Java 堆总容量 11904K)
[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]
两款低延迟垃圾收集器

实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟

  • Shenandoah:初始标记、并发标记、最终标记、并发清理、并发回收、初始引用更新、并发引用更新、最终引用更新、并发清理。
  • ZGC: 动态创建和销毁 Region。并发标记、并发预备重分配、并发重分配、并发重映射。

# 垃圾收集相关常用参数

  • 垃圾收集器相关参数

# 内存分配与回收策略

对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过 JIT 编译后被拆散为标量类型并间接地栈上分配)。

# 对象优先在 Eden 分配

  • 大多数情况下,对象在新生代 Eden 区中分配。 当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC(新生代 GC,非常频繁,一般回收速度也比较快)。
  • 虚拟机提供了 - XX:+PrintGCDetails 这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。

# 大对象直接进入老年代

  • 需要大量连续内存空间的 Java 对象(很长的字符串以及数组)
  • 虚拟机提供了一个 - XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。 这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制

# 长期存活的对象将进入老年代

  • 虚拟机给每个对象定义了一个对象年龄(Age)计数器。
  • 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1。
  • 对象在 Survivor 区中每 “熬过” 一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就将会被晋升到老年代中。
  • 对象晋升老年代的年龄阈值,可以通过参数 - XX:MaxTenuringThreshold 设置。

# 动态对象年龄判定(对象的年龄不是必须达到了 MaxTenuringThreshold 才能晋升老年代)

  • 如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

# 空间分配担保

  • 在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
  • 如果这个条件成立,那么 Minor GC 可以确保是安全的。
  • 如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。
  • 如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。
  • 如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的。

Minor GC 是有风险的原因:
新生代使用复制收集算法,在老年代进行分配担保时,无法明确知道在实际完成内存回收后一共有多少对象会活下来,通过取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行 Full GC 来让老年代腾出更多空间。如果某次 Minor GC 存活后的对象突增,远远高于平均值的话,依然会导致担保失败。
JDK 6 Update24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 Full GC。

  • 如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。