jvm(三)——jvm垃圾回收算法以及实现 - Go语言中文社区

jvm(三)——jvm垃圾回收算法以及实现


一、概述

java中,垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了。

jvm 中,程序计数器、虚拟机栈、本地方法栈都是都是线程私有的,随线程而生随线程而灭,栈帧(栈中的对象)随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的.

二、对象存活判断

判断对象是否存活一般有两种方式:

引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

对象循环引用问题,即对象A引用对象B的,而在对象B中又引用了对象A,那么对于对象A和对象B来说,其引用计数器都为1,难以判断其是否存活。

可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,即为不可达对象。

在Java语言中,GC Roots包括:

虚拟机栈中引用的对象。

方法区中类静态属性实体引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI引用的对象。

总结 :GC root包括栈中引用对象和方法区中引用对象。

三、垃圾收集算法

1、标记 -清除算法

标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

wpsA73E.tmp

2、复制算法

“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,降低了内存的利用率,持续复制长生存期的对象则导致效率降低,还有在分配对象较大时,该种算法也存在效率低下的问题。

wps9D31.tmp

3、标记-整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法(老年代一般是存活时间较长的大对象)。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,这种算法克服了复制算法的低效问题,同时克服了标记清除算法的内存碎片化的问题;

wps3952.tmp

4、分代收集算法

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

“分代收集”(Generational Collection)算法,是一种划分的策略,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

四、垃圾收集器

​ 收集算法是jvm内存回收过程中具体的、通用的方法,垃圾收集器是jvm内存回收过程中具体的执行者,即各种GC算法的具体实现。

1、Serial收集器

串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)

参数控制:-XX:+UseSerialGC 串行收集器

wpsA77.tmp

2、ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

参数控制:-XX:+UseParNewGC ParNew收集器

-XX:ParallelGCThreads 限制线程数量参数

wps6A83.tmp

3、Parallel收集器

Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩

参数控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行

4、Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供

参数控制:-XX:+UseParallelOldGC使用Parallel收集器+ 老年代并行

5、CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
​ 由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

优点:并发收集低停顿

缺点:产生大量空间碎片、并发阶段会降低吞吐量

参数控制:

-XX:+UseConcMarkSweepGC 使用CMS收集器

**-XX:+ UseCMSCompactAtFullCollection **Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

wpsCA6E.tmp

6、G1 收集器

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

  1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

wps3B4C.tmp

G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。

收集步骤

1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)

2、Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。

3、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

wps93E7.tmp

4、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

wps47EC.tmp

6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。

wpsEAB1.tmp

常用的收集器组合

新生代GC策略 年老代GC策略 说明
组合1 Serial Serial Old
组合2 Serial CMS+Serial Old
组合3 ParNew CMS
组合4 ParNew Serial Old
组合5 Parallel Scavenge Serial Old
组合6 Parallel Scavenge Parallel Old
组合7 G1GC G1GC

五、ZGC

最近一个新的GC收集器概念比较火,JDK团队在JDK 11中即将迎来ZGC(The Z Garbage Collector),这是一个处于实验阶段的,可扩展的低延迟垃圾回收器,本章将对其实现以及性能进行大致介绍,首先说明以下一个指标。

  • 每次GC STW的时间不超过10ms
  • 能够处理从几百M到几T的JAVA堆
  • 与G1相比,吞吐量下降不超过15%
  • 为未来的GC功能和优化利用有色对象指针(colored oops)和加载屏障(load barriers)奠定基础
  • 初始支持Linux/x64

描述

ZGC的特点:

  • 并发
  • 基于Region的
  • 标记整理
  • NUMA感知
  • 使用colored oops
  • 使用load barrier
  • 仅root扫描时STW,因此GC暂停时间不会随堆的大小而增加。

ZGC的核心原则是将load barrier与colored oops结合使用。这使得ZGC能够在Java应用程序线程运行时执行并发操作,例如对象迁移时。
从Java线程的角度来看,在Java对象中加载引用字段的行为受到load barrier的影响。除了对象地址之外,colored oops还包含load barrier使用的信息,以确定在允许Java线程使用指针之前是否需要采取某些操作。
例如,对象可能已迁移,在这种情况下,load barrier将检测情况并采取适当的操作。

与其他替代技术相比,colored oops提供了如下非常有吸引力的特性:

  • 它允许ZGC在对象迁移和整理阶段回收和重用内存。这有助于降低一般堆开销。这也意味着不需要为Full GC实现一个单独的标记整理算法。
  • 目前在colored oops中仅存储标记和对象迁移相关信息。然而,这种方案的通用性使我们能够存储任何类型的信息(只要我们可以将它放入指针中)并让load barrier根据该信息采取它想要的任何动作。比如,在异构内存环境中,这可以用于跟踪堆访问模式,以指导GC对象迁移策略,将很少使用的对象移动到冷存储。

ZGC可以并发执行下面的任务:

  • 标记
  • 引用处置
  • relocation集选择
  • 迁移和整理

性能

以下是基于同一基准的GC暂停时间。请注意,确切的数字取决于所使用的确切机器和设置。

ZGC
avg: 1.091ms (+/-0.215ms)
95th percentile: 1.380ms
99th percentile: 1.512ms
99.9th percentile: 1.663ms
99.99th percentile: 1.681ms
max: 1.681ms

G1
avg: 156.806ms (+/-71.126ms)
95th percentile: 316.672ms
99th percentile: 428.095ms
99.9th percentile: 543.846ms
99.99th percentile: 543.846ms
max: 543.846ms

限制

  • 当前版本不支持类卸载
  • 当前版本不支持JVMCI
    JVMCI是JDK 9 引入的JVM编译器接口。这个接口允许用Java编写的编译器被JVM用作动态编译器。JVMCI的API提供了访问VM结构、安装编译代码和插入JVM编译系统的机制。现有支持Java编译器的项目主要是 Graal 和 Metropolis 。

如何工作的

指针标记

在x64系统上,引用是64位的, ZGC重新定义了引用结构

  +-------------------+-+----+-----------------------------------------------+
  |00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
  +-------------------+-+----+-----------------------------------------------+
  |                   | |    |
  |                   | |    * 41-0 Object Offset (42-bits, 4TB address space)
  |                   | |
  |                   | * 45-42 Metadata Bits (4-bits)  0001 = Marked0      (Address view 4-8TB)
  |                   |                                 0010 = Marked1      (Address view 8-12TB)
  |                   |                                 0100 = Remapped     (Address view 16-20TB)
  |                   |                                 1000 = Finalizable  (Address view N/A)
  |                   |
  |                   * 46-46 Unused (1-bit, always zero)
  |
  * 63-47 Fixed (17-bits, always zero)

如上表所示, ZGC使用41-0存储对象实际地址的前42位, 42位地址为应用程序提供了理论4TB的堆空间; 45-42位为metadata比特位, 对应于如下状态: finalizable,remapped,marked1和marked0; 46位为保留位,固定为0; 63-47位固定为0.

在引用中添加元数据, 使得解除引用的代价更加高昂, 因为需要操作掩码以获取真实的地址, ZGC采用了一种有意思的技巧, 读操作时是精确知道metadata值的, 而分配空间时, ZGC映射同一页到3个不同的地址,而在任一时间点,这3个地址中只有一个正在使用中。

for marked0: (0b0001 << 42) | x
for marked1: (0b0010 << 42) | x
for remapped: (0b0100 << 42) | x

实现代码如下:

void ZPhysicalMemoryBacking::map(ZPhysicalMemory pmem, uintptr_t offset) const {
  if (ZUnmapBadViews) {
    // Only map the good view, for debugging only
    map_view(pmem, ZAddress::good(offset), AlwaysPreTouch);
  } else {
    // Map all views
    map_view(pmem, ZAddress::marked0(offset), AlwaysPreTouch);
    map_view(pmem, ZAddress::marked1(offset), AlwaysPreTouch);
    map_view(pmem, ZAddress::remapped(offset), AlwaysPreTouch);
  }
}


void ZPhysicalMemoryBacking::unmap(ZPhysicalMemory pmem, uintptr_t offset) const {
  if (ZUnmapBadViews) {
    // Only map the good view, for debugging only
    unmap_view(pmem, ZAddress::good(offset));
  } else {
    // Unmap all views
    unmap_view(pmem, ZAddress::marked0(offset));
    unmap_view(pmem, ZAddress::marked1(offset));
    unmap_view(pmem, ZAddress::remapped(offset));
  }
}

采用此方法后, ZGC堆空间结构如下:

// Address Space & Pointer Layout
// ------------------------------
//
//  +--------------------------------+ 0x00007FFFFFFFFFFF (127TB)
//  .                                .
//  .                                .
//  .                                .
//  +--------------------------------+ 0x0000140000000000 (20TB)
//  |         Remapped View          |
//  +--------------------------------+ 0x0000100000000000 (16TB)
//  |     (Reserved, but unused)     |
//  +--------------------------------+ 0x00000c0000000000 (12TB)
//  |         Marked1 View           |
//  +--------------------------------+ 0x0000080000000000 (8TB)
//  |         Marked0 View           |
//  +--------------------------------+ 0x0000040000000000 (4TB)
//  .                                .
//  +--------------------------------+ 0x0000000000000000

如此带来一个副作用, ZGC无法兼容指针压缩.

分页

在G1中,堆内存通常被分为几千个大小相同region。同样的,在ZGC中堆内存也被分成大量的区域,它们被称为page,不同的是,ZGC中page的大小是不同的。
ZGC有3种不同的页面类型:小型(2MB大小),中型(32MB大小)和大型(2MB的倍数)。
在小页面中分配小对象(最大256KB大小),在中间页面中分配中型对象(最多4MB)。大页面中分配大于4MB的对象。大页面只能存储一个对象,与小页面或中间页面相对应。
有些令人困惑的大页面实际上可能小于中等页面(例如,对于大小为6MB的大对象)。
这种分配方式让人想起操作系统的内存分配方式,有点相似

标记整理

void ZDriver::run_gc_cycle(GCCause::Cause cause) {
  ZDriverCycleScope scope(cause);
  // Phase 1: Pause Mark Start
  {
    ZMarkStartClosure cl;
    vm_operation(&cl);
  }
  // Phase 2: Concurrent Mark
  {
    ZStatTimer timer(ZPhaseConcurrentMark);
    ZHeap::heap()->mark();
  }
  // Phase 3: Pause Mark End
  {
    ZMarkEndClosure cl;
    while (!vm_operation(&cl)) {
      // Phase 3.5: Concurrent Mark Continue
      ZStatTimer timer(ZPhaseConcurrentMarkContinue);
      ZHeap::heap()->mark();
    }
  }
  // Phase 4: Concurrent Reference Processing
  {
    ZStatTimer timer(ZPhaseConcurrentReferencesProcessing);
    ZHeap::heap()->process_and_enqueue_references();
  }
  // Phase 5: Concurrent Reset Relocation Set
  {
    ZStatTimer timer(ZPhaseConcurrentResetRelocationSet);
    ZHeap::heap()->reset_relocation_set();
  }
  // Phase 6: Concurrent Destroy Detached Pages
  {
    ZStatTimer timer(ZPhaseConcurrentDestroyDetachedPages);
    ZHeap::heap()->destroy_detached_pages();
  }
  // Phase 7: Concurrent Select Relocation Set
  {
    ZStatTimer timer(ZPhaseConcurrentSelectRelocationSet);
    ZHeap::heap()->select_relocation_set();
  }
  // Phase 8: Prepare Relocation Set
  {
    ZStatTimer timer(ZPhaseConcurrentPrepareRelocationSet);
    ZHeap::heap()->prepare_relocation_set();
  }
  // Phase 9: Pause Relocate Start
  {
    ZRelocateStartClosure cl;
    vm_operation(&cl);
  }
  // Phase 10: Concurrent Relocate
  {
    ZStatTimer timer(ZPhaseConcurrentRelocated);
    ZHeap::heap()->relocate();
  }
}

ZGC包含10个阶段,但是主要是两个阶段标记和relocating。
GC循环从标记阶段开始,递归标记所有可达对象,标记阶段结束时,ZGC可以知道哪些对象仍然存在,哪些是垃圾。ZGC将结果存储在每一页的位图(称为live map)中。

在标记阶段,应用线程中的load barrier将未标记的引用压入线程本地的标记缓冲区。一旦缓冲区满,GC线程会拿到缓冲区的所有权,并且递归遍历此缓冲区所有可达对象。注意:应用线程负责压入缓冲区,GC线程负责递归遍历。

标记阶段后,ZGC需要迁移relocate集中的所有对象。relocate集是一组页面集合,包含了根据某些标准(例如那些包含最多垃圾对象的页面)确定的需要迁移的页面。对象由GC线程或者应用线程迁移(通过load barrier)。ZGC为每个relocate集中的页面分配了转发表。转发表是一个哈希映射,它存储一个对象已被迁移到的地址(如果该对象已经被迁移)。

GC线程遍历relocate集的活动对象,并迁移尚未迁移的所有对象。有时候会发生应用线程和GC线程同时试图迁移同一个对象,在这种情况下,ZGC使用CAS操作来确定胜利者。

一旦GC线程完成了relocate集的处理,迁移阶段就完成了。虽然这时所有对象都已迁移,但是旧地引用址仍然有可能被使用,仍然需要通过转发表重新映射(remapping)。然后通过load barrier或者等到下一个标记循环修复这些引用。

这也解释了为什么对象引用中有两个标记位(marked0和marked1)。标记阶段交替使用在marked0和marked1位。

load barrier

它的比较容易和CPU的内存屏障(memory barrier)弄混淆,但是它们是完全不同的东西。

从堆中读取引用时,ZGC需要一个所谓的load barrier(也称为read-barrier)。每次Java程序访问对象字段时,ZGC都会执行load barrier的代码逻辑,例如obj.field。访问原始类型的字段不需要屏障,例如obj.anInt或obj.anDouble。ZGC不使用存储/写入障碍obj.field = someValue。

如标记整理章节所说,根据GC当前所处的阶段,如果尚未标记或迁移引用,则屏障会标记对象或迁移它。

思考

STW为什么这么短

仅root扫描时STW,其他标记、清理、迁移阶段,均通过colored oops和load-barrier配合使用,并发执行。

参考资料

JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental)
http://openjdk.java.net/jeps/333
http://hg.openjdk.java.net/jdk/jdk/rev/767cdb97f103
http://hg.openjdk.java.net/zgc/zgc/file/59c07aef65ac/src/hotspot/os_cpu/linux_x86/zGlobals_linux_x86.hpp#l59
http://hg.openjdk.java.net/zgc/zgc/file/59c07aef65ac/src/hotspot/share/gc/z/zPage.hpp#l34

https://blog.csdn.net/lirenzuo/article/details/81182686

附加内容

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
GC时的Stop the World(STW)是大家最大的敌人。但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象。
JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation,比如分派GC,thread dump等,这些任务,都需要整个Heap,以及所有线程的状态是静止的,一致的才能进行。所以JVM引入了安全点(Safe Point)的概念,想办法在需要进行VM Operation时,通知所有的线程进入一个静止的安全点。
除了GC,其他触发安全点的VM Operation包括:
1. JIT相关,比如Code deoptimization, Flushing code cache ;
2. Class redefinition (e.g. javaagent,AOP代码植入的产生的instrumentation) ;
3. Biased lock revocation 取消偏向锁 ;
4. Various debug operation (e.g. thread dump or deadlock check);
监控安全点看看JVM到底发生了什么?
最简单的做法,在JVM启动参数的GC参数里,多加一句:
-XX:+PrintGCApplicationStoppedTime
它就会把全部的JVM停顿时间(不只是GC),打印在GC日志里。
2016-08-22T00:19:49.559+0800: 219.140: Total time for which application threads were stopped: 0.0053630 seconds

这是个很有用的必配参数,可以打出几乎一切的停顿……
但是,在JDK1.7.40以前的版本,它居然没有打印时间戳,所以只能知道JVM停了多久,但不知道什么时候停的。此时一个土办法就是加多一句“ -XX:+PrintGCApplicationConcurrentTime”,打印JVM在两次停顿之间的正常运行时间(同样没有时间戳),但好歹能配合有时间戳的GC日志,反推出Stop发生的时间了。
2016-08-22T00:19:50.183+0800: 219.764: Application time: 5.6240430 seconds

如何打印出事哪种原因导致的停顿呢?
再多加两个参数:-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1

此时,在stdout中会打出类似的内容

vmop [threads: total initially_running wait_to_block]1913.425: GenCollectForAllocation [ 55 2 0 ] [time: spin block sync cleanup vmop] page_trap_count[ 0 0 0 0 6 ] 0

此日志分两段,第一段是时间戳,VM Operation的类型,以及线程概况
total: 安全点里的总线程数
initially_running: 安全点时开始时正在运行状态的线程数
wait_to_block: 在VM Operation开始前需要等待其暂停的线程数
第二行是到达安全点时的各个阶段以及执行操作所花的时间,其中最重要的是vmop
spin: 等待线程响应
safepoint号召的时间
block: 暂停所有线程所用的时间
sync: 等于 spin+block,这是从开始到进入安全点所耗的时间,可用于判断进入安全点耗时
cleanup: 清理所用时间
vmop: 真正执行VM Operation的时间
可见,那些很多但又很短的安全点,全都是RevokeBias,详见 偏向锁实现原理, 高并发的应用一般会干脆在启动参数里加一句”-XX:-UseBiasedLocking”取消掉它。另外还看到有些类型是no vm operation, 文档上说是保证每秒都有一次进入安全点(如果这秒已经GC过就不用了),给一些需要在安全点里进行,又非紧急的操作使用,比如一些采样型的Profiler工具,可用-DGuaranteedSafepointInterval来调整,不过实际看它并不是每秒都会发生,时间不定。
在实战中,我们利用安全点日志,发现过有程序定时调用Thread Dump等等情况。不过因为安全点日志默认输出到stdout,因为性能及stdout日志的整洁性等原因,我们平时默认没有开启它。只有在需要时才打开。
再再增加下面三个参数,可以知道更多VM里发生的事情。可惜JVM不会因为设了这三个参数,就把安全点日志转移到vm.log里面来,而是白白打印了两次。
-XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log

总结

本文关于快速理解Java垃圾回收和jvm中的stw的介绍就到这里,希望对大家有所帮助,感兴趣的朋友可以参阅:浅谈Java回收对象的标记和对象的二次标记过程 、Java虚拟机装载和初始化一个class类代码解析 、Java中map遍历方式的选择问题详解等,有什么问题可以随时留言,小编会及时回复大家的。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/wangxiaotongfan/article/details/82389881
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2019-08-27 13:28:45
  • 阅读 ( 1677 )
  • 分类:算法

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢