考虑哪些内存需要回收、什么时候回收、如何回收;
首先程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,大体上都是在编译期可知的(运行期会由 JIT 编译期进行优化),因此它们在方法或者线程结束之后对应的内存空间就回收了
下面只考虑 Java 堆和方法区,因为一个接口中的各个实现类需要的内存可能各不相同,一个方法的各个分支需要的内存也不一样,只能在程序运行期才能知道会创建哪些对象和回收都是动态的,需要关注这部分内存变化。
一般使用 new 语句创建对象的时候消耗 12 个字节,其中引用在栈上占 4 个字节,空对象在堆中占 8 个字节。如果该语句所在方法执行结束之后,对应 Stack 中的变量会马上进行回收,但是 Heap 中的对象要等到 GC 来回收。
java.lang.Class
对象没有在任何地方被引用,且不能在任何地方通过反射访问该类的方法;引用计数算法(Reference Counting)
给对象添加一个引用计数器,当有一个地方引用它则计数器 +1,当引用失效的时候计数器 -1,任何时刻计数器为 0 的对象就是不可能再被使用的;
引用计数算法无法解决对象循环引用的问题。问题:循环引用能不能解决
如下面代码中两个对象处理互相引用对方,再无任何引用
package chapter3;
import org.junit.jupiter.api.Test;
public class ReferenceCountingGC {
public Object instance = null;
private static final int memory = 1024 * 1024;
/**
* 该成员属性作用为:占用内存,以便能在 GC 日志中看清楚是否被回收过
*/
private byte[] bigSize = new byte[2 * memory];
@Test
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 直接进行 GC
System.gc();
}
}
分析:testGC() 方法的前四行执行之后,objA 对象被 objA 和 objB.instance 引用着,objB 也类似;执行objA=null 和 objB=null 之后,objA 对象的 objA 引用失效,但是 objB.instance 引用仍然存在,因此如果采用单纯的引用计数法,objA 并不会被回收,除非在执行 objB=null 时,遍历 objB 对象的属性,将里面的引用全部置为无效。
根搜索算法( GC Roots Tracing )【可达性】
在实际的生产语言中(Java、 C#等)都是使用根搜索算法判定对象是否存活;
算法基本思路就是通过一系列的称为 GC Roots 的点作为起始点进行向下搜索,当一个对象到 GC Roots 没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。下图中 object5/6/7 之间虽然互相有引用,但是它们到 GC Roots 是不可达的,因此会被判定为是可回收对象。
在JDK 1.2之后,Java 对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、 软引用(Soft Reference)、 弱引用(Weak Reference)、 虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱
强引用就是指在程序代码之中普遍存在的,类似Object obj = new Object()
这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用是用来描述一些还有用但并非必需的对象。 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。 如果这次回收还没有足够的内存,才会抛出内存溢出异常。 在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 在JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。
示例:
MyObject aRef = new MyObject();
SoftReference aSoftRef = new SoftReference(aRef);
一旦 SoftReference 保存了对一个 Java 对象的软引用后,在垃圾线程对这个 Java 对象回收前,SoftReference 类所提供的 get() 方法返回 Java 对象的强引用。另外,一旦垃圾线程回收该 Java 对象之后,get() 方法将返回 null。在 Java 集合中有一种特殊的 Map 类型:WeakHashMap, 在这种 Map 中存放了键对象的弱引用,当一个键对象被垃圾回收,那么相应的值对象的引用会从 Map 中删除。WeakHashMap 能够节约存储空间,可用来缓存那些非必须存在的数据。
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要执行finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在 finalize() 方法中执行缓慢或者发生了死循环,将很可能会导致 F-Queue 队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
代码示例:
package chapter3;
/**
* 此代码演示两点:
* 1.对象可以在GC时自我救赎。
* 2.这种自我救赎的机会只有一次,因为finalize()方法最多只会被调用一次。
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC saveMe = null;
public void isLive() {
System.out.println("我还活着!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("执行finalize()方法中……");
// 完成自我救赎
saveMe = this;
}
public static void main(String[] args) throws InterruptedException {
saveMe = new FinalizeEscapeGC();
// 对象第一次拯救自己
saveMe = null;
System.gc();
// 因为finalize方法优先级比较低,所以暂停进行等待
Thread.sleep(5000);
if (saveMe == null) {
System.out.println("我已经死亡!");
} else {
saveMe.isLive();
}
// 对象第二次自我救赎,失败
saveMe = null;
System.gc();
Thread.sleep(5000);
if (saveMe == null) {
System.out.println("我已经死亡!");
} else {
saveMe.isLive();
}
}
}
执行结果:
执行finalize()方法中……
我还活着!
我已经死亡!
从结果可以看出 saveMe 对象的 finalize() 方法确实被 GC 收集器触发过,但是在被收集前逃脱了;
同时程序中两段相同的代码执行结果一次逃脱一次失败,因为任何一个对象的 finalize() 方法都只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法不会被再次执行,因此第二段代码中自救失败。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BBlrmUrP-1578190259908)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/finalize%20%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B.jpg)]
标记-清除算法(Mark Sweep)
标记-整理算法(Mark-Compact)
复制算法(Copying)
分代算法(Generational)
新生使用复制算法,老年代一般采用标记-清除算法或者标记-整理算法;
算法分为“标记”和“清除”两个阶段, 首先标记出所有需要回收的对象,然后回收所有需要回收的对象;
缺点:
上图中,左侧是运行时虚拟机栈,箭头表示引用,则绿色就是不能被回收的
标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存
没有内存碎片;
比标记清理耗费更多的时间进行整理;
当前商业虚拟机的垃圾收集都是采用“分代收集”( Generational Collecting)算法,根据对象不同的存活周期将内存划分为几块。
一般是把 Java 堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次 GC 都有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本,就可以完成收集。同时老年代中对象存活率较高,没有额外空间对其进行担保,必须使用“标记-清理” 或者 “标记-整理” 进行回收。
HotSpot JVM 6中共划分为三个代:
Object o=new Object()
这种方式赋值的引用 ,Soft、Weak、 Phantom 这 三种则都是继承 Reference;在分代模型(新生代和老年代)的基础上,GC 从时机上分为两种: Scavenge GC 和 Full GC
system.gc()
当 Java 执行系统停顿(保证分析过程中不会出现对象引用关系的变更)下来之后,并不需要一个漏的检查完所有执行上下文和全局的引用位置(这两者通常作为 GC Roots 的节点),虚拟机应当有办法直接得知哪些地方存放着对象引用。在 HotSpot 的实现中,是使用一组称为 OopMap (OOP:Ordinary Object Pointer 普通对象指针)的数据结构来达到该目的。在类加载完成之后,HotSpot 就把对象内什么偏移量上面是什么类型的数据计算出来了,在 JIT 编译过程中,也会在特定位置记录栈和寄存器中哪些位置是引用。所以 GC 扫描时候就可以得知。
CMS 收集器在枚举根节点时候也必须停顿。
在 OopMap 的协助下,HotSpot 可以快速且准确的完成 GC Roots 枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那将会需要大量的额外空间,这样 GC 的空间成本将会更高。
实际上,HotSpot 并没有为每条指令都生成 OopMap,而只是在 “特定位置” 记录了这些信息,这些位置称为 安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始 GC,只有在达到安全点时才能暂停。
Safepoint 的选定既不能太少以致于让 GC 等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。因为每条指令执行的时间非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行” 的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等等,所以具有这些功能的指令才会产生 Safepoint。
对于安全点,另一个需要考虑的问题是如何在 GC 发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断和主动式中断。
抢先式中断:不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程(应用线程)全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。但是现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应 GC 事件。
主动式中断:当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
使用安全点似乎已经完美地解决了如何进入 GC 的问题,但是实际情况却并不一定。安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint。但是在程序不执行的时候就无法做到这一点,比如线程在休眠或阻塞状态。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始 GC 都是安全的。我们也可以把 Safe Region 看做是被扩展了的 Safepoint。
在线程执行到 Safe Region 中的代码时,首先标识自己已经进入了 Safe Region,那样,当在这段时间里JVM要发起 GC 时,就不用管标识自己为 Safe Region 状态的线程了。在线程要离开 SafeRegion 时,它要检查系统是否已经完成了根节点枚举(或者是整个 GC 过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开 Safe Region 的信号为止。
收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
垃圾收集器的‘并行”和并发
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Av0aHHrY-1578190259909)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/%E5%B8%B8%E8%A7%81%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8.png)]
单线程收集器,收集会暂停所有工作线程(Stop The World,STW),使用复制收集算法,虚拟机运行在 Client 模式时的默认新生代收集器;(因为该模式下虚拟机管理的内存小,并且该收集器没有线程交互,接收机效率高,整体的停顿时间可接受)
最早的收集器,单线程进行 GC, 新生代和老年代都可以使用;
在新生代,采用复制算法;
在老年代,采用标记-整理算法,因为是单线程 GC,没有多线程切换的额外开销,简单实用,是HotSpot Client模式默认的收集器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GV31ys3w-1578190259910)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/serial.png)]
-XX:ParallelGCThreads
来控制 GC 线程数的多少。需要结合具体 CPU 的个数 Server 模式下新生代的缺省收集器-XX:+UseConcMarkSweepGC
或者 -XX:+UseParNewGC
来指定其为新生代收集器;[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6uIG2lwa-1578190259910)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/parnew.png)]
新生代、使用复制算法、并行多线程收集器;
适合需要与用户交互的程序,具有较高的响应速度,适合在后台运算而不需要太多交互的任务。
Parallel Scavenge 收集器也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew 收集器有所不同,它是以吞吐量最大化(即 GC 时间占总运行时间最小)为目标的收集器实现,它允许较长时间的 STW 换取总吞吐量最大化;
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间);
参数:-XX:MaxGCPauseMillis
控制最大垃圾收集停顿时间,数值为大于 0 的毫秒数,收集器会尽可能保证内存回收时间不超过设定值。值不能太小,GC 停顿时间缩短是以牺牲吞吐量和新生代空间换取的,新生代越小,会导致垃圾回收更加频繁,停顿时间下降但是吞吐量也下降。
参数:-XX:GCTimeRatio
直接设置吞吐量(是个百分比)大小;值为 0-100,默认值为 99,即表示允许最大 1 / (1 + 99) 的垃圾收集时间。
参数:-XX:+UseAdaptiveSizePolicy
为开关参数,打开后无需设定新生代大小、Eden 和Survivor 比例等等,虚拟机会根据系统运行情况自动调节。即 GC 自适应的调节策略(GC Ergonomics)
是 Parallel Scavenge 收集器的老年代版本,使用多线程和 标记 - 整理算法。吞吐量优先收集器
从 JDK 1.6 开始提供,在此之前,新生代使用了 PS 收集器的话,老年代只能使用 Serial Old 收集器(无法充分利用服务器的多 CPU 处理能力)整体效果不好,因为 PS 无法和 CMS 收集器配合工作;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N5jR3oif-1578190259911)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/image-20191212220755481.png)]
-XX:+UseConcMarkSweepGC
打开[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4UAgh7ho-1578190259911)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/cms.png)]
代码示例一:对象定义在错误范围
// 方式一:如果 Foo 实例对象的生命周期较长,会导致临时性内存泄露(这里的 names 变量其实就是临时作用)
Class Foo{
// names 变量定义在类中,即使没有其他地方使用该变量,但是因为 Foo 实例存在,所以该变量一直存在
private String[] names;
public void doIt(int length){
if(names == null || names.length < lenth){
names = new String[length];
populate(names);
print(names);
}
}
}
// 修改方式二: JVM 喜欢生命周期短的对象,更加的高效
class Foo{
public void doIt(int length){
// 将 names 从成员变量变成局部变量,当 doIt 方法执行完成之后里面的局部变量都会被回收,所以不论 Foo 这个实例存活多长时间,都不会影响 names 被回收
String[] names = new String[length];
populate(names);
print(names);
}
}
代码示例二:异常处理不当
// 方式一:如果 doSomeStuff() 中抛出异常,则 rs.close() 和 conn.close() 不会被调用,导致内存泄露和 DB 连接泄露
Connection conn = DriverManager.getConnection(url, name, passwd);
try{
String sql = "do a query sql";
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery();
while (rs.next()){
doSomeStuff();
}
rs.close();
conn.close();
}catch(Exception e){
}
// 方式二:修改如下,将资源关闭操作放在 finally 语句中
Connection conn = null;
ResultSet rs = null;
try{
String sql = "do a query sql";
stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery();
while (rs.next()){
doSomeStuff();
}
}catch(Exception e){
} finally {
if (rs != null){
rs.close();
}
if(stmt != null){
stmt.close();
}
conn.close();
}
代码示例三:数据集合管理不当
测试是在 Client 模式虚拟机进行,默认未指定收集器组合情况下是使用 Serial / Serial Old 收集器(ParNew / Serial Old 收集器组合的规则类似)来验证内存分配和回收策略。
VM Options:
-verbose:gc
:会输出详细的垃圾回收的日志-Xms20M
:设置虚拟机启动时候堆初始大小为 20 M-Xmx20M
:设置虚拟机中堆最大值为 20 M-Xmn10M
:设置堆中新生代大小为 10 M-XX:+PrintGCDetails
:打印出 GC 详细信息-XX:SurvivorRatio=8
:表示 Eden 空间和 survivor 空间占比为 8:1package com.gjxaiou.gc;
/**
* @Author GJXAIOU
* @Date 2019/12/13 20:50
*/
public class MyTest1 {
public static void main(String[] args) {
int size = 1024 * 1024;
// 这种情况下只有 GC,如果数组大小都是 3 * size,则还会包括 Full GC
byte[] myAlloc1 = new byte[2 * size];
byte[] myAlloc2 = new byte[2 * size];
byte[] myAlloc3 = new byte[3 * size];
System.out.println("hello world");
}
}
输出结果:
// (触发 GC 的原因)[新生代使用 Parallel Scavenge 收集器:垃圾回收之前新生代存活对象占用的空间->垃圾回收之后新生代存活对象占用的空间(新生代总的空间容量,因为这里包括 Eden 和 survivor 区域,survivor 包括 FromSurvivor 和 toSurvivor,两者只有一个可以被使用)] 执行 GC 之前总的堆中存活对象占空间的大小,包括新生代和老年代 -> GC 之后堆中活着占空间大小【因为前面对象还活着,所以变化不大】(总的堆中可用容量),执行 GC 花费时间
[GC (Allocation Failure) [PSYoungGen: 5751K->824K(9216K)] 5751K->4928K(19456K), 0.0018545 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
hello world
Heap
PSYoungGen total 9216K, used 4219K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 41% used [0x00000000ff600000,0x00000000ff950ce0,0x00000000ffe00000)
from space 1024K, 80% used [0x00000000ffe00000,0x00000000ffece030,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
// GC 时候发现前面对象太大无法放入 Survivor 空间(Survivor 大小为 1 M),所以只能通过分配担保机制提前转移到老年代中。
ParOldGen total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
Metaspace used 3135K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 342K, capacity 388K, committed 512K, reserved 1048576K
上面运算结果计算比较:
PSYoungGen: 5751K->824K(9216K)] 5751K->4928K(19456K)
:5751 - 824 = 4927K,表示执行完 GC 之后,新生代释放的空间(包括真正释放的空间和晋升到老年代的空间), 5751 - 4928 = 823k,表示执行完 GC 之后,总的堆空间释放的容量(真正释放的空间),所以 4927 - 823 = 4104k ,表示从新生代晋升到老年代的空间,正好和 :ParOldGen total 10240K, used 4104K
符合。
输出结果二:将创建数组大小均改为: 3 * size 之后会产生 Full GC
[GC (Allocation Failure) [PSYoungGen: 7963K->824K(9216K)] 7963K->6976K(19456K), 0.0026002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// Full GC 会对老年代和元空间进行回收
[Full GC (Ergonomics) [PSYoungGen: 824K->0K(9216K)] [ParOldGen: 6152K->6759K(10240K)] 6976K->6759K(19456K), [Metaspace: 3132K->3132K(1056768K)], 0.0051304 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
hello world
Heap
PSYoungGen total 9216K, used 3396K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 41% used [0x00000000ff600000,0x00000000ff9512a0,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
// Par:Parallel Old(老年代垃圾收集器)
ParOldGen total 10240K, used 6759K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 66% used [0x00000000fec00000,0x00000000ff299e18,0x00000000ff600000)
Metaspace used 3151K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
新生代和老年代
java -XX:+PrintCommandLineFlags -version
控制台输出结果为:
-XX:InitialHeapSize=266067584 -XX:MaxHeapSize=4257081344 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
其中-XX:+UseParallelGC
表示默认对新生代使用 Parallel Scavenge
,对老年代使用 Parallel Old
垃圾收集器;
测试程序:
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=4194304 -XX:+UseSerialGC
其中 -XX:PretenureSizeThreshold=4194304
表示当我们创建对象的字节大于 PretenureSizeThreshold
的数值(单位:字节),对象将不会在新生代分配而是直接进入老年代(避免 Eden 区和两个 Survivor 去之间发生大量的内存复制);该参数需要和串行垃圾收集器配合使用,因此在上面参数中同时制定了使用 Serial 垃圾收集器。 该参数只对 Serial 和 ParNew 收集器有用。
package com.gjxaiou.gc;
/**
* @Author GJXAIOU
* @Date 2019/12/15 10:27
*/
public class MyTest2 {
public static void main(String[] args) {
int size = 1024 * 1024;
byte[] bytes = new byte[5 * size];
}
}
从下面结果中:tenured generation total 10240K, used 5120K
可以看出是直接在老年代进行了分配;
Heap
def new generation total 9216K, used 1983K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedefd20, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 5120K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 50% used [0x00000000ff600000, 0x00000000ffb00010, 0x00000000ffb00200, 0x0000000100000000)
Metaspace used 3149K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
测试二:去掉上面程序中 VM Options 中的 -XX:+UseSerialGC
,同时将字节数组空间改为 8 * size
,结果如下:
Heap
PSYoungGen total 9216K, used 1983K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 24% used [0x00000000ff600000,0x00000000ff7efd20,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400010,0x00000000ff600000)
Metaspace used 3202K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 346K, capacity 388K, committed 512K, reserved 1048576K
因为 Eden 空间的大小为 8 * size,但是因为新创建的对象大小为 8 * size,因此 Eden 空间容纳不了,因此直接进入老年代(对象是不可能拆分放入两个代的)
测试三:同上,但是将空间大小改为 10 * size
[GC (Allocation Failure) [PSYoungGen: 1819K->808K(9216K)] 1819K->816K(19456K), 0.0006639 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 808K->808K(9216K)] 816K->816K(19456K), 0.0005789 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 808K->0K(9216K)] [ParOldGen: 8K->612K(10240K)] 816K->612K(19456K), [Metaspace: 3116K->3116K(1056768K)], 0.0037378 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(9216K)] 612K->612K(19456K), 0.0002203 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(9216K)] [ParOldGen: 612K->594K(10240K)] 612K->594K(19456K), [Metaspace: 3116K->3116K(1056768K)], 0.0041631 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 9216K, used 410K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 5% used [0x00000000ff600000,0x00000000ff666800,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 594K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 5% used [0x00000000fec00000,0x00000000fec94b58,0x00000000ff600000)
Metaspace used 3200K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 347K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.gjxaiou.gc.MyTest2.main(MyTest2.java:10)
测试四:恢复原来参数 -XX:+UseSerialGC
,代码更改如下:
package com.gjxaiou.gc;
/**
* @Author GJXAIOU
* @Date 2019/12/15 10:27
*/
public class MyTest2 {
public static void main(String[] args) {
int size = 1024 * 1024;
byte[] bytes = new byte[5 * size];
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
程序执行过程中使用 JVisualVM 观察堆空间状况:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tO7TWfc7-1578190259913)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/image-20191215104352640.png)]
程序输出结果和对应的监控图片为:
// 该 GC 是因为当启动一个检测工具(这里为 JVisualVM),会对原有的进程进行一次 Touch 动作,会创建一些对象从而造成内存空间不够从而会进行 GC(Minor GC),
[GC (Allocation Failure) [DefNew: 8192K->1024K(9216K), 0.0218174 secs] 13312K->6779K(19456K), 0.0218597 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
[GC (Allocation Failure) [DefNew: 9216K->494K(9216K), 0.1351672 secs] 14971K->7272K(19456K), 0.1351873 secs] [Times: user=0.00 sys=0.00, real=0.13 secs]
[Full GC (System.gc()) [Tenured: 6777K->7059K(10240K), 0.0075978 secs] 13027K->7059K(19456K), [Metaspace: 9186K->9186K(1058816K)], 0.3133278 secs] [Times: user=0.01 sys=0.00, real=0.31 secs]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P2QgH7QF-1578190259914)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/image-20191215104636489.png)]
因为默认情况下只有创建对象的时候才会可能出现垃圾回收的操作,但是调用 System.gc()
,会告诉 JVM 需要进行垃圾回收,JVM 会自行决定什么时候进行垃圾回收,同时可能在没有创建对象情况下执行垃圾回收;
同时可以使用 jmc 查看运行结果,可以看出 Eden 空间大小变化情况;
jps -l
查看当前进程对应的进程编号
jcmd 进程号 VM.flags
查看运行参数
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:+PrintTenuringDistribution
其中:-XX:MaxTenuringThreshold=5
:在可以自动调节对象晋升(Promote)到老年代阈值的 GC 中,设置该阈值的最大值;默认情况下新生代中对象经过一次 GC 对应的年龄就 + 1,这里当年龄 >5 的时候该对象就晋升到老年代。这只是一个最大值,但是可能没有到达该阈值 JVM 也会将其晋升到老年代。
并且该参数的默认值为:15,其中在 CMS 中默认值为:6,在 G1 中默认值为:15,因为在 JVM 中该数值由 4 个 bit 来标识,所以最大值为 1111,即为 15;
经历过多次 GC 之后,新生代中存活的对象会在 From Survivor 和 To Survivor 之间来回存放,而前提是这两个空间有足够的的大小来存放这些数据,在 GC 算法中会计算每个对象年龄的大小,如果到达某个年龄后发现该年龄的对象总大小已经大于 Survivor(其中一个 Survivor) 空间的 50 %,这个时候就需要调整阈值,不能在继续等到默认的 15 次 GC 之后才完成晋升,因为会导致 Survivor 空间不足,所有需要调整阈值,让这些存活的对象尽快完成晋升来释放 Survivor 空间。
示例代码:
package com.gjxaiou.gc;
/**
* @Author GJXAIOU
* @Date 2019/12/15 12:43
*/
public class MyTest3 {
public static void main(String[] args) {
int size = 1024 * 1024;
byte[] myAlloc1 = new byte[2 * size];
byte[] myAlloc2 = new byte[2 * size];
byte[] myAlloc3 = new byte[2 * size];
byte[] myAlloc4 = new byte[2 * size];
System.out.println("hello world");
}
}
结果显示:
-XX:InitialHeapSize=20971520 -XX:InitialTenuringThreshold=5 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=5 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
[GC (Allocation Failure)
// new threshold 5 是动态计算的阈值,该值 <= 后面设置的最大值 5
// 所需 Survivor 空间为 1048576/1024/1024 = 1M,和设置的一样
Desired survivor size 1048576 bytes, new threshold 5 (max 5)
[PSYoungGen: 7799K->808K(9216K)] 7799K->6960K(19456K), 0.0028644 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 808K->0K(9216K)] [ParOldGen: 6152K->6754K(10240K)] 6960K->6754K(19456K), [Metaspace: 3104K->3104K(1056768K)], 0.0049277 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
hello world
Heap
PSYoungGen total 9216K, used 2372K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 28% used [0x00000000ff600000,0x00000000ff851200,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 6754K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 65% used [0x00000000fec00000,0x00000000ff298bd0,0x00000000ff600000)
Metaspace used 3126K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 338K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
默认情况下:Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
综合测试代码
-verbose:gc -Xmx200M -Xmn50M -XX:TargetSurvivorRatio=60 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:MaxTenuringThreshold=3 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:TargetSurvivorRatio=60
表示当一个 Survivor 空间中存活的对象占据了 60% 的空间,就会重新计算晋升的阈值(不在使用配置或者默认的阈值)package com.gjxaiou.gc;
/**
* @Author GJXAIOU
* @Date 2019/12/15 13:27
*/
public class MyTest4 {
public static void main(String[] args) throws InterruptedException {
// 下面两个字节数组在 main() 方法中,不会被GC
byte[] byte1 = new byte[512 * 1024];
byte[] byte2 = new byte[512 * 1024];
myGC();
Thread.sleep(1000);
System.out.println("----111111111------");
myGC();
Thread.sleep(1000);
System.out.println("----22222222------");
myGC();
Thread.sleep(1000);
System.out.println("----333333333------");
myGC();
Thread.sleep(1000);
System.out.println("----444444444------");
byte[] byte3 = new byte[1024 * 1024];
byte[] byte4 = new byte[1024 * 1024];
byte[] byte5 = new byte[1024 * 1024];
myGC();
Thread.sleep(1000);
System.out.println("----555555555------");
myGC();
Thread.sleep(1000);
System.out.println("----666666666------");
System.out.println("hello world");
}
// 方法中定义的变量当方法执行完成之后生命周期就结束了,下次垃圾回收时候就可以回收了
private static void myGC() {
for (int i = 0; i < 40; i++) {
byte[] byteArray = new byte[1024 * 1024];
}
}
}
程序运行结果为:
2019-12-15T14:18:18.013+0800: [GC (Allocation Failure) 2019-12-15T14:18:18.022+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age 1: 1712592 bytes, 1712592 total
: 40346K->1706K(46080K), 0.0091928 secs] 40346K->1706K(199680K), 0.0186267 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
----111111111------
2019-12-15T14:18:19.032+0800: [GC (Allocation Failure) 2019-12-15T14:18:19.032+0800: [ParNew
// 3145728(3M),因为默认 8:1:1,即 Survivor 空间为 5M,对应的 60% 即为 3M;
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age 1: 342632 bytes, 342632 total
- age 2: 1762376 bytes, 2105008 total
: 41847K->2413K(46080K), 0.0007192 secs] 41847K->2413K(199680K), 0.0007452 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
----22222222------
2019-12-15T14:18:20.034+0800: [GC (Allocation Failure) 2019-12-15T14:18:20.034+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age 1: 80 bytes, 80 total
- age 2: 342096 bytes, 342176 total
- age 3: 1761160 bytes, 2103336 total
: 42927K->2424K(46080K), 0.0006154 secs] 42927K->2424K(199680K), 0.0006435 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
----333333333------
2019-12-15T14:18:21.037+0800: [GC (Allocation Failure) 2019-12-15T14:18:21.037+0800: [ParNew
// 上面 age = 3 的这里垃圾回收之后变成 4 晋升为老年代了
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age 1: 80 bytes, 80 total
- age 2: 80 bytes, 160 total
- age 3: 341992 bytes, 342152 total
: 43144K->1050K(46080K), 0.0017927 secs] 43144K->2738K(199680K), 0.0018190 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
----444444444------
2019-12-15T14:18:22.040+0800: [GC (Allocation Failure) 2019-12-15T14:18:22.040+0800: [ParNew
// 这里阈值变成了 1,因为对象空间超过 Survivor 空间的 60% 即为 3M,重新计算了阈值,计算公式为取当前年龄和 MaxThreshold 的最小值,因为新创建数组,当前年龄为 1,所以最终为 1;
Desired survivor size 3145728 bytes, new threshold 1 (max 3)
- age 1: 3145856 bytes, 3145856 total
- age 2: 80 bytes, 3145936 total
- age 3: 80 bytes, 3146016 total
: 41777K->3128K(46080K), 0.0009780 secs] 43465K->5151K(199680K), 0.0010024 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
----555555555------
2019-12-15T14:18:23.042+0800: [GC (Allocation Failure) 2019-12-15T14:18:23.042+0800: [ParNew
// 上面的 age 为 1,2,3 的经过一次 GC 之后全部晋升到老年代了,下面是新加入新生代的对象
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age 1: 80 bytes, 80 total
: 43859K->14K(46080K), 0.0011804 secs] 45882K->5109K(199680K), 0.0012060 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
----666666666------
hello world
Heap
par new generation total 46080K, used 18015K [0x00000000f3800000, 0x00000000f6a00000, 0x00000000f6a00000)
eden space 40960K, 43% used [0x00000000f3800000, 0x00000000f49946a8, 0x00000000f6000000)
from space 5120K, 0% used [0x00000000f6000000, 0x00000000f6003840, 0x00000000f6500000)
to space 5120K, 0% used [0x00000000f6500000, 0x00000000f6500000, 0x00000000f6a00000)
concurrent mark-sweep generation total 153600K, used 5095K [0x00000000f6a00000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3735K, capacity 4536K, committed 4864K, reserved 1056768K
class space used 410K, capacity 428K, committed 512K, reserved 1048576K
Process finished with exit code 0
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
上面提到了Minor GC依然会有风险,是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。
取平均值仍然是一种概率性的事件,如果某次Minor GC后存活对象陡增,远高于平均值的话,必然导致担保失败,如果出现了分配担保失败,就只能在失败后重新发起一次Full GC。虽然存在发生这种情况的概率,但大部分时候都是能够成功分配担保的,这样就避免了过于频繁执行Full GC。
1.6 之后:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则进行 Full GC, HandlePromotionFailure 不在有用。
CMS 垃圾收集器属于老年代的收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。从名字(包含"Mark Sweep")上就可以看出,CMS 收集器是基于"标记-清除"算法实现的,整个过程分为4个步骤,包括:
初始标记(CMS initial mark):初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;
并发标记(CMS concurrent mark):该阶段就是进行 GC Roots Tracing 的过程;
重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短;
并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要 “Stop The World”。由于整个过程中耗时最长的并发标记和并发清除过程收集器收集线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ke7KupRO-1578190259915)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/426b97e3848.png)]
优点:并发收集、低停顿;
CMS收集器有3个明显的缺点:
-XX:CMSInitiatingOccupancyFraction
指定,该百分值太小则 GC 过于频繁,太大会导致预留内存无法满足程序需要,出现 “Concurrent Mode Failure”,这时候只能采用 Serial Old 收集器来进行老年代垃圾收集,更加浪费时间。-XX:+UseCMSCompactAtFullCollection
开关参数(默认开启),用于在 CMS 收集器顶不住要进行 Full GC 时候开启内存碎片合并整理过程,该过程无法并发停顿时间较长。-XX+CMSFullGCsBeforeCompaction
用于设置执行多少次不压缩的 Full GC 之后,跟着来一次带压缩的。默认值为 0 ,表示每次进入 Full GC 都进行碎片整理。这是CMS中两次stop-the-world事件中的一次。这一步的作用是标记存活的对象,有两部分:
标记老年代中所有的GC Roots对象(即直接被 GC Root 引用的对象),如下图节点1;
标记年轻代中活着的对象引用到的老年代的对象(指的是年轻带中还存活的引用类型对象,引用指向老年代中的对象)如下图节点2、3;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wzcn8try-1578190259915)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/20170502172953141.png)]
在Java语言里,可作为GC Roots对象的包括如下几种:
ps:为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数,线程数不要超过cpu的核数;
在这个阶段垃圾收集器会遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到的 GC Roots 遍历查找。并发标记阶段,它会与用户的应用程序并发运行。并不是老年代的所有存活的对象都会被标记,因为在标记期间用户的程序可能会改变一些引用。(例如结点 3 下面结点的引用发生了改变)
从“初始标记”阶段标记的对象开始找出所有存活的对象;
因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
如下图所示,也就是节点1、2、3,最终找到了节点4和5。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ePqbBtLS-1578190259916)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/20170502175211859.png)]
这个阶段因为是并发的容易导致concurrent mode failure
这也是一个并发阶段,与应用的线程并发运行,并不会 Stop 应用的线程,在并发运行的过程中,一些对象的引用可能会发生改变,但是这种情况发生时, JVM 会将包含这个对象的区域(Card)标记为 Dirty,这就是 Card marking
在 Pre-clean 阶段,那些能够从 Dirty 对象到达的对象也会被标记,这个标记做完之后, Dirty Card 标记就会被清除了。
前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Direty的Card
如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UX4aSmh1-1578190259916)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/20170502211600103.png)]
最后将6标记为存活,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qtyIm1L0-1578190259917)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/20170502211950472.png)]
这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生aboart的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。
ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻带的引用,是的下个阶段的重新标记阶段,扫描年轻带指向老年代的引用的时间减少;
这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻带的对象无用的对象,并将对象放入幸存带或晋升到老年代,这样再进行年轻带扫描时,只需要扫描幸存区的对象即可,一般幸存带非常小,这大大减少了扫描时间
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻带的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候,是为了减少连续 STW 发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。
另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled
至此,老年代所有存活的对象都被标记过了,现在可以通过清除算法去清理老年代不再使用的对象。
通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。
这个阶段主要是清除那些没有标记的对象并且回收空间;
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bxYlEgwC-1578190259918)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/image-20191219091858884.png)]
这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。
-XX+UseCMSCompaceAtFullCollection
开关参数(默认开启),用于在 CMS 收集器顶不住要进行 Full GC 时候开启内存碎片的合并整理过程,内存整理过程是无法并发的,解决了空间碎片问题但是增加了停顿时间;针对上面步骤的代码验证
设置虚拟机参数为:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC
因为 CMS 只能运行到老年代,对应的新生代会自动采用与 CMS 对应的垃圾回收器
程序为:
package com.gjxaiou.gc;
/**
* @Author GJXAIOU
* @Date 2019/12/18 13:19
*/
public class MyTest5 {
public static void main(String[] args) {
int size = 1024 * 1024;
byte[] myAlloc1 = new byte[4 * size];
System.out.println("----111111111----");
byte[] myAlloc2 = new byte[4 * size];
System.out.println("----222222222----");
byte[] myAlloc3 = new byte[4 * size];
System.out.println("----333333333----");
byte[] myAlloc4 = new byte[2 * size];
System.out.println("----444444444----");
}
}
输出结果:
// 前面没有执行任何的垃圾回收,因为 Eden 区域放置 4M 对象可以放下
----111111111----
// 因为第二次 new 又需要分配 4M 空间,Eden 空间不够用,使用垃圾回收,对应新生代是 ParNew 收集器
[GC (Allocation Failure) [ParNew: 5899K->670K(9216K), 0.0016290 secs] 5899K->4768K(19456K), 0.0016630 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
----222222222----
// 新生代垃圾回收
[GC (Allocation Failure) [ParNew: 5007K->342K(9216K), 0.0023932 secs] 9105K->9168K(19456K), 0.0024093 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
// 老年代垃圾回收 老年代存活对象占用空间大小(老年代总的空间大小)
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8825K(10240K)] 13319K(19456K), 0.0003398 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
----333333333----
----444444444----
Heap
par new generation total 9216K, used 6780K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 78% used [0x00000000fec00000, 0x00000000ff2499d0, 0x00000000ff400000)
from space 1024K, 33% used [0x00000000ff400000, 0x00000000ff455a08, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
concurrent mark-sweep generation total 10240K, used 8825K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3144K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
另一端代码,将上面代码中的 byte[] myAlloc4 = new byte[2 * size];
修改为:byte[] myAlloc4 = new byte[3 * size];
得到的结果如下:
----111111111----
[GC (Allocation Failure) [ParNew: 5765K->637K(9216K), 0.0024098 secs] 5765K->4735K(19456K), 0.0024726 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
----222222222----
[GC (Allocation Failure) [ParNew: 4974K->240K(9216K), 0.0041475 secs] 9072K->9060K(19456K), 0.0041812 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
----333333333----
----444444444----
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8819K(10240K)] 16522K(19456K), 0.0002890 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
Heap
par new generation total 9216K, used 7764K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 91% used[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[0x00000000fec00000, 0x00000000ff358e70, 0x00000000ff400000)
from space 1024K, 23% used [0x00000000ff400000, 0x00000000ff43c2d0, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
concurrent mark-sweep generation total 10240K, used 8819K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3126K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 338K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
下面抓取一下gc信息,来进行详细分析,首先将jvm中加入以下运行参数:
-XX:+PrintCommandLineFlags [0]
-XX:+UseConcMarkSweepGC [1]
-XX:+UseCMSInitiatingOccupancyOnly [2]
-XX:CMSInitiatingOccupancyFraction=80 [3]
-XX:+CMSClassUnloadingEnabled [4]
-XX:+UseParNewGC [5]
-XX:+CMSParallelRemarkEnabled [6]
-XX:+CMSScavengeBeforeRemark [7]
-XX:+UseCMSCompactAtFullCollection [8]
-XX:CMSFullGCsBeforeCompaction=0 [9]
-XX:+CMSConcurrentMTEnabled [10]
-XX:ConcGCThreads=4 [11]
-XX:+ExplicitGCInvokesConcurrent [12]
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses [13]
-XX:+CMSParallelInitialMarkEnabled [14]
-XX:+PrintGCDetails [15]
-XX:+PrintGCCause [16]
-XX:+PrintGCTimeStamps [17]
-XX:+PrintGCDateStamps [18]
-Xloggc:../logs/gc.log [19]
-XX:+HeapDumpOnOutOfMemoryError [20]
-XX:HeapDumpPath=../dump [21]
先来介绍下下面几个参数的作用:
\0. [0]打印出启动参数行
\1. [1]参数指定使用CMS垃圾回收器;
\2. [2]、[3]参数指定CMS垃圾回收器在老年代达到80%的时候开始工作,如果不指定那么默认的值为92%;
\3. [4]开启永久带(jdk1.8以下版本)或元数据区(jdk1.8及其以上版本)收集,如果没有设置这个标志,一旦永久代或元数据区耗尽空间也会尝试进行垃圾回收,但是收集不会是并行的,而再一次进行Full GC;
\4. [5] 使用cms时默认这个参数就是打开的,不需要配置,cms只回收老年代,年轻带只能配合Parallel New或Serial回收器;
\5. [6] 减少Remark阶段暂停的时间,启用并行Remark,如果Remark阶段暂停时间长,可以启用这个参数
\6. [7] 如果Remark阶段暂停时间太长,可以启用这个参数,在Remark执行之前,先做一次ygc。因为这个阶段,年轻带也是cms的gcroot,cms会扫描年轻带指向老年代对象的引用,如果年轻带有大量引用需要被扫描,会让Remark阶段耗时增加;
\7. [8]、[9]两个参数是针对cms垃圾回收器碎片做优化的,CMS是不会移动内存的, 运行时间长了,会产生很多内存碎片, 导致没有一段连续区域可以存放大对象,出现”promotion failed”、”concurrent mode failure”, 导致fullgc,启用UseCMSCompactAtFullCollection 在FULL GC的时候, 对年老代的内存进行压缩。-XX:CMSFullGCsBeforeCompaction=0 则是代表多少次FGC后对老年代做压缩操作,默认值为0,代表每次都压缩, 把对象移动到内存的最左边,可能会影响性能,但是可以消除碎片;
106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]
(concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]
\8. [11]定义并发CMS过程运行时的线程数。比如value=4意味着CMS周期的所有阶段都以4个线程来执行。尽管更多的线程会加快并发CMS过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加CMS线程数是否真的能够带来性能的提升。如果未设置这个参数,JVM会根据并行收集器中的-XX:ParallelGCThreads参数的值来计算出默认的并行CMS线程数:
ParallelGCThreads = (ncpus <=8 ? ncpus : 8+(ncpus-8)*5/8) ,ncpus为cpu个数,
ConcGCThreads =(ParallelGCThreads + 3)/4
这个参数一般不要自己设置,使用默认就好,除非发现默认的参数有调整的必要;
\9. [12]、[13]开启foreground CMS GC,CMS gc 有两种模式,background和foreground,正常的cms gc使用background模式,就是我们平时说的cms gc;当并发收集失败或者调用了System.gc()的时候,就会导致一次full gc,这个fullgc是不是cms回收,而是Serial单线程回收器,加入了参数[12]后,执行full gc的时候,就变成了CMS foreground gc,它是并行full gc,只会执行cms中stop the world阶段的操作,效率比单线程Serial full GC要高;需要注意的是它只会回收old,因为cms收集器是老年代收集器;而正常的Serial收集是包含整个堆的,加入了参数[13],代表永久带也会被cms收集;
\10. [14] 开启初始标记过程中的并行化,进一步提升初始化标记效率;
\11. [15]、[16]、[17]、[18] 、[19]是打印gc日志,其中[16]在jdk1.8之后无需设置
\12. [20]、[21]则是内存溢出时dump堆
吞吐量:
响应能力:
以上是用来评价一个系统的两个很重要的指标,介绍这两个指标的原因是因为G1就是用来解决这样的问题而应运而生的。
以上可以看到G1在吞吐量和响应能力上都进行了兼顾。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3211mdyT-1578190259918)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/image-20191218163635270.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D28paQoq-1578190259919)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/image-20191218163937612.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sVxqfiZE-1578190259920)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/G1.png)]
RSet 记录了其他 Region 中的对象引用本 Region 中对象的关系,属于 points-into 结构( 谁引用了我的区域中的对象)RSet 的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描 RSet 即可。每个 Region 都有一个对象的 RSet。
示例:Region1 和 Region3 中的对象都引用了 Region2中的对象,因此在 Region2 的 RSet 中记录了这两个引用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IZ7J29zI-1578190259920)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/region.png)]
参考链接:https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
global concurrent marking 的执行过程类似于 CMS,但是不同的是在 G1 GC 中,它主要是为 Mixed GC 提供标记服务的(即表示应该回收哪些老年代),并不是一次 GC 过程的一个必须环节。
下面为全局并发标记执行过程
初始标记( initial mark, STW) :它标记了从 GC Root 开始直接可达的对象。并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发执行时候(因为该阶段需要暂停用户线程),能在正确可用的 Region 中创建新对象。
并发标记( Concurrent Marking) :这个阶段从GC Root 开始对堆中的对象进行可达性分析标记,标记线程与应用程序线程并发执行,并且收集各个 Region 的存活对象信息。
重新标记( Remark, STW) :标记那些在并发标记阶段发生变化的对象,记录在线程 RSet Logs 中,同时将 RSet Logs 中的数据合并到 RSet 中,将被回收。(该阶段需要停顿但可以并行)
清理(Cleanup) :首先对各个 Region 中的回收价值和成本进行排序,根据用户期望的 GC 时间停顿时间来制定 回收计划,所以可能只清空一部分 Region。清除空 Region (没有存活对象的),加入到 free list。
第一阶段 initial mark 是共用了 Young GC 的暂停,这是因为他们可以复用 rootscan 操作,所以可以说 global concurrent marking 是伴随 Young GC 而发生的;
第四阶段 Cleanup 只是回收了没有存活对象的 Region,所以它并不需要 STW。
由一些参数控制,另外也控制着哪些老年代 Region 会被选入 CSet (收集集合),下面是一部分的参数
参数 | 含义 |
---|---|
-XX:G1HeapRegionSize=n | 设置 Region 大小,并非最终值 |
-XX:MaxGCPauseMillis | 设置 G1 收集过程目标时间,默认值200 ms,不是硬性条件 |
-XX:G1NewSizePercent | 新生代最小值,默认值 5% |
-XX:G1MaxNewSizePercent | 新生代最大值,默认值 60% |
-XX:ParallelGCThreads | STW 期间,并行 GC 线程数 |
-XX:ConcGCThreads=n | 并发标记阶段,并行执行的线程数 |
-XX:InitiatingHeapOccupancyPercent | 设置触发标记周期的 Java 堆占用率阈值。默认值是 45%。这里的 Java 堆占比指的是 non_young_capacity_bytes,包括old+humongous |
在G1中,还有一种特殊的区域,叫 Humongous区域。如果一个对象占用的空间达到或是超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象就会对垃圾收集器造成负面影响。为了解决这个问题, G1 划分了一个 Humongous 区,它用来专门存放巨型对象。如果一个H 区装不下一个巨型对象,那么 G1 会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动 Full GC
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zLsW6DSY-1578190259921)(JVM%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.resource/image-20191218205128114.png)]
在 CMS 中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用这是一种 point-out,在进行 Young Go时扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代
但在 G1 中,并没有使用 point-out,这是由于一个分区太小,分区数量太多,如果是用 point-out 的话,会造成大量的扫描浪费,有些根本不需要 GC 的分区引用也扫描了。
于是 G1中使用 point-in 来解决。 point-in 的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。
由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次 GC 时所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可
需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在 G1 中又引入了另外一个概念:卡表( Card table)。一个 Card table 将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于 128 到 512 字节之间。 Card Table通常为字节数组,由 Card 的索引(即数组下标)来标识每个分区的空间地址;
默认情况下,每个卡都未被引用,当一个地址空间被引用时候,这个地址空间对应的数组索引的值被标记为 0,即标记为脏被引用,此外 Rset 也将这个数组下标记录下来。一般情况下,这个 Rset 其实是一个 HashTable,Key 是别的 Region 的起始地址,Value 是一个集合,里面的元素是 Card Table 的 Index。
G1 Young GC 过程
阶段一:根扫描;
静态和本地对象被扫描
阶段二:更新 RS
处理 Dirty Card 队列,更新 RS
阶段三:处理 RS
检测从年轻代指向老年代的对象
阶段四:对象拷贝
拷贝存活的对象到 Survivor/old 区域
阶段五:处理引用队列
软引用、弱引用、虚引用处理
Mixed GC 不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程(即全局并发标记的线程)标记的老年代分区;
Mixed GC 步骤:
在 G1 GC 中, Global concurrent Marking 主要是为 Mixed GC 提供标记服务的,并不是一次 GC 过程中的一个必须环节。
提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有效的方法,利用它可以推演回收器的正确性,标记表示该对象是可达的,即不应该被当做垃圾回收
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理,如图:
这时候应用程序执行了以下操作: A.c=C B.c=null 这样,对象的状态图变成如下情形:
这时候垃圾收集器再标记扫描的时候就会变成下图这样
[
为老年代设置分区的目的是老年代里有的分区垃圾多,有的分区垃圾少,这样在回收的时候可以专注于收集垃圾多的分区这也是G1名称的由来不过这个算法并不适合新生代垃圾收集,因为新生代的垃圾收集算法是复制算法,但是新生代也使用了分区机制主要是因为便于代大小的调整。
mark的过程就是遍历heap标记live object的过程,采用的是三色标记算法,这三种颜色为white(表示还未访问到)、gray(访问到但是它用到的引用还没有完全扫描、black( 访问到而且其用到的引用已经完全扫描完)
整个三色标记算法就是从GCroots出发遍历heap,针对可达对象先标记white为gray,然后再标记gray为black;遍历完成之后所有可达对象都是black的,所有white都是可以回收的
SATB仅仅对于在marking开始阶段进行快照(“snapshot”(marked all reachable at markstart)),但是concurrent的时候并发修改可能造成对象漏标记
对于三色算法在concurrent的时候可能产生的漏标记问题,SATB在marking阶段中,对于从gray对象移除的目标引用对象将其标记为 gray,对于black引用的新产生的对象将其标记为black;由于是在开始的时候进行snapshot,因而可能存在Floating Garbage
Young GC 和 Mixed GC 是分代 G1 模式下选择 Cset 的两种子模式;
-XX:MaxGCPauseMillis=x
可以设置启动应用程序暂停的时间,G1 在运行的时候会根据这个参数选择 CSet 来满足响应时间的设置。一般情况下这个值设置到 100ms 或者 200ms 都是可以的(不同情况下会不一样),但如果设置成 50ms 就不太合理。暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。程序代码为:
VM 参数为:-verbose:gc -Xms10m -Xmx10m -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:MaxGCPauseMillis=200m
其中:* -XX:+UseG1GC
表示指定垃圾收集器使用G1;-XX:MaxGCPauseMillis=200m
表示设置垃圾收集最大停顿时间
package com.gjxaiou.gc.g1;
/**
* @Author GJXAIOU
* @Date 2019/12/20 20:59
*/
public class G1LogAnalysis {
public static void main(String[] args) {
int size = 1024 * 1024;
byte[] myAlloc1 = new byte[size];
byte[] myAlloc2 = new byte[size];
byte[] myAlloc3 = new byte[size];
byte[] myAlloc4 = new byte[size];
System.out.println("hello world");
}
}
日志结果为:
2019-12-20T21:02:10.163+0800: [GC pause (G1 Humongous Allocation【说明分配的对象超过了region大小的50%】) (young) (initial-mark), 0.0015901 secs]
[Parallel Time: 0.8 ms, GC Workers: 10【GC工作线程数】]
[GC Worker Start (ms): Min: 90.3, Avg: 90.4, Max: 90.4, Diff: 0.1]【几个垃圾收集工作的相关信息统计】
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.1, Sum: 2.1]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.4, Avg: 0.4, Max: 0.5, Diff: 0.1, Sum: 4.4]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 5.4, Max: 8, Diff: 7, Sum: 54]
【上面的几个步骤为YOUNG GC的固定执行步骤】
* 阶段1:根扫描
* 静态和本地对象被描
* 阶段2:更新RS
* 处理dirty card队列更新RS
* 阶段3:处理RS
* 检测从年轻代指向老年代的对象
* 阶段4:对象拷贝
* 拷贝存活的对象到survivor/old区域
* 阶段5:处理引用队列
* 软引用,弱引用,虚引用处理
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.3]
[GC Worker Total (ms): Min: 0.7, Avg: 0.7, Max: 0.7, Diff: 0.1, Sum: 6.9]
[GC Worker End (ms): Min: 91.1, Avg: 91.1, Max: 91.1, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]【 清楚 cardTable所花费时间】
[Other: 0.7 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 2048.0K(6144.0K)->0.0B(2048.0K) Survivors: 0.0B->1024.0K Heap: 3725.2K(10.0M)->2836.0K(10.0M)]
[Times: user=0.01 sys=0.00, real=0.00 secs]
2019-12-20T21:02:10.165+0800: [GC concurrent-root-region-scan-start]
2019-12-20T21:02:10.165+0800: [GC pause (G1 Humongous Allocation) (young)2019-12-20T21:02:10.165+0800: [GC concurrent-root-region-scan-end, 0.0006999 secs]
2019-12-20T21:02:10.165+0800: [GC concurrent-mark-start]
, 0.0013416 secs]
[Root Region Scan Waiting: 0.3 ms]
[Parallel Time: 0.5 ms, GC Workers: 10]
[GC Worker Start (ms): Min: 92.5, Avg: 92.6, Max: 92.6, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.2, Diff: 0.1, Sum: 1.0]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.3, Avg: 0.3, Max: 0.3, Diff: 0.0, Sum: 3.0]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 4.6, Max: 8, Diff: 7, Sum: 46]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 0.4, Avg: 0.4, Max: 0.5, Diff: 0.1, Sum: 4.1]
[GC Worker End (ms): Min: 93.0, Avg: 93.0, Max: 93.0, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.2 ms]
[Other: 0.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 1024.0K(2048.0K)->0.0B【新生代清理后】(1024.0K) Survivors: 1024.0K->1024.0K Heap: 3901.0K(10.0M)->4120.5K(10.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2019-12-20T21:02:10.166+0800: [GC concurrent-mark-end, 0.0012143 secs]
2019-12-20T21:02:10.167+0800: [Full GC (Allocation Failure) 4120K->3676K(10M), 0.0020786 secs]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->0.0B Heap: 4120.5K(10.0M)->3676.9K(10.0M)], [Metaspace: 3091K->3091K(1056768K)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2019-12-20T21:02:10.169+0800: [GC remark, 0.0000082 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2019-12-20T21:02:10.169+0800: [GC concurrent-mark-abort]
hello world
Heap
garbage-first heap total 10240K, used 4700K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
region size 1024K【说明region默认大小】, 1 young (1024K), 0 survivors (0K)
Metaspace used 3229K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0