新一代 GC 神器 ZGC

0. 标记-复制算法

标记-复制算法分三个阶段:

  • 标记阶段:从 GC Roots 出发,标记存活对象。
  • 转移阶段:把存活对象复制到新的内存地址,原来的内存空间变成可回收的。
  • 重定位阶段:存活对象被复制到其他地方后,所有指向对象旧地址的指针都要调整到对象新的地址上。

1. 概述

Z Garbage Collector,即ZGC,是一个可伸缩的、低延迟的垃圾收集器。只支持 64 位的系统。

ZGC 未分代,每次 GC 都是 FullGC 。

1.1 ZGC 的设计目标

  • 亚毫秒级的最大暂停时间。
  • 暂停时间不随堆、存活对象数量、根集合大小的增长而增长。
  • 可处理的堆大小从 8MB 到 16TB。

总的来说,ZGC 具有如下特性:

  • 并发
  • 基于 Region
  • 压缩
  • NUMA 友好
  • 使用着色指针
  • 使用读屏障 load barriers

截至 JDK 14,ZGC 已在主流的操作系统上获得支持。

继续阅读

CMS GC

1. 概述

CMS,Most concurrently,是一款 采用 并发-标记-清除(Concurrent-Mark-Sweep)算法、针对老年代的 GC,其目的是最小化暂停时间。

CMS GC 在 JDK 9 被标记为 deprecated。

2. 执行过程

CMS 的标记过程采用指针可达性分析方法,对于存活的对象,在对象头上进行标记。

2.1 初始标记

从 GC Roots、新生代(Eden、From、To)出发标记直接关联的对象。

需要 STW 。

2.2 并发标记

该阶段进行 GC Root Tracing,从初始标记阶段 标记过的对象出发,标记所有可达的对象。

与应用线程并发执行,可以采用多个标记线程进行并发标记。

2.3 预清理

此阶段从新生代晋升的对象、新分配到老年代的对象以及在并发阶段修改了的对象 出发进行标记。
在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的 card 标记为 dirty,通过扫描这些 card,重新标记那些在并发阶段引用被更新的对象。

通过 -XX:-CMSPrecleaningEnabled 参数选择关闭该阶段,默认开启。

目的是减少重新标记阶段的暂停时间。

CMS 提供了参数 CMSScavengeBeforeRemark 在 重新标记之前强制进行一次 minor GC,这样做的好处是缩短了重新标记阶段的暂停时间,坏处是 Minor GC 后紧跟着一个 重新标记的 暂停,这样使停顿时间也比较久。

继续阅读

《垃圾回收算法手册》–第2章 标记-清扫回收

1、垃圾回收基础

垃圾回收器的安全性是指:在任何时候都不能回收存活对象。

浮动垃圾(floating garbage):如果某个对象在回收过程启动之后才变成垃圾,那么该对象只能在下一个回收周期内得到回收。

回收器在标记对象时可以使用对象中的域,也可以使用额外的数据结构如位图 bitmap;对于并发回收器或其他需要将堆分为数个独立区域的回收器,其需要额外的记忆集(remembered set)来保存赋值器所修改的指针值或者跨区域指针的位置。

赋值器:应用线程。

在类型安全的语言中,一旦某个对象在堆中不可达,赋值器在没有外部系统协助的情况下是无法访问到不可达对象的。

赋值器在工作过程中会执行三种与回收器相关的操作:创建(New)、读(Read)、写(Write)。某些特殊的内存管理器会为基本操作增加一些额外功能,即屏障(barrier),屏障操作会同步或异步地与回收器产生交互。

在新分配的对象可被赋值器操作之前,回收器都需要先对其元数据进行初始化。

任何自动内存管理系统都面临三个任务:
1、为新对象分配空间。
2、确定存活对象。
3、回收死亡对象所占用的空间。

回收空间的方法影响这分配新空间的方法。

基于指针可达性分析得到的存活对象集合中可能包含一些永远不会再被赋值器访问的对象,但是死亡对象集合中的对象却必定是死亡的。

继续阅读

CAS/volatile 原理

1. 指令的 lock 前缀

intel手册对 lock 前缀的说明如下:

  1. 确保被修饰指令执行的原子性。
  2. 禁止该指令与前面和后面的读写指令重排序。
  3. 指令执行完后把写缓冲区的所有数据刷新到内存中。(这样这个指令之前的其他修改对所有处理器可见。)

在 Pentium 及之前的处理器中,带有 lock 前缀的指令在执行期间会声言 LOCK# 信号以锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel 使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低 lock 前缀指令的执行开销。如果访问的内存区域已经缓存在处理器内部,则不会声言 LOCK# 信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据

继续阅读

JVM 堆外内存泄漏分析(二)

关于 堆外内存的组成可以看上一篇文章 JVM 堆外内存泄漏分析(一)

1. NMT

NMT(Native Memory Tracking)是 HotSpot JVM 引入的跟踪 JVM 内部使用的本地内存的一个特性,可以通过 jcmd 工具访问 NMT 数据。NMT 目前不支持跟踪第三方本地代码的内存分配和 JDK 类库。

NMT 不跟踪非 JVM 代码的内存分配,本地代码里的内存泄露需要使用操作系统支持的工具来定位。

1.1 开启 NMT

启用 NMT 会带来 5-10% 的性能损失。NMT 的内存使用率情况需要添加两个机器字 word 到 malloc 内存的 malloc 头里。NMT 内存使用率也被 NMT 跟踪。

启动命令: -XX:NativeMemoryTracking=[off | summary | detail]

off:NMT 默认是关闭的;
summary:只收集子系统的内存使用的总计数据;
detail:收集每个调用点的内存使用数据。

1.2 jcmd 访问 NMT 数据

命令: jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]

option desc
summary 按分类打印汇总数据
detail 按分类打印汇总数据
打印虚拟内存映射
按调用点打印内存使用汇总
baseling 创建内存使用快照用于后续对比
summary.diff 基于最新的基线打印一份汇总报告
detail.diff 基于最新的基线打印一份明细报告
shutdown 关闭 NMT

在 NMT 启用的情况下,可以通过下面的命令行选项在 JVM 退出时输出最后的内存使用数据:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics

继续阅读

JVM 堆外内存泄漏分析(一)

1. JVM 感知容器资源

Java 应用部署在 Kubernetes 集群里,每个容器只运行一个进程, JVM 的启动命令是打包在镜像文件里的。

常规的方式是采用 -Xmx4g -Xms2g 这样的参数来指定 JVM 堆的最大、最小尺寸,如果需要调整堆大小就需要重新打包镜像。

为了避免因为修改堆大小而重新打包,从 JDK 8u191 版本开始支持 JVM 感知容器资源限制,这样在调整 JVM 内存分配时就不需要重新打包镜像文件,采用下面的参数来使 JVM 在启动时感知到容器的资源限制,并设定堆的大小:

-XX:+UseCGroupMemoryLimitForHeap
-XX:InitialRAMPercentage=60.00
-XX:MaxRAMPercentage=80.00
-XX:MinRAMPercentage=60.00 

假如分配给容器的内存上限是 4G,那么上述配置,JVM 堆的初始大小和最小尺寸是 4G * 0.6 即 2.4G,最大尺寸是 4G * 0.8 即 3.2G。

2. JVM 被 oomkill

上面的配置运行一段时间后发现容器自动重启了,在 linux 下通过 dmesg 命令查看系统日志,可以看到类似下面的日志:

Aug  8 15:32:40 H-LDOCKER-01 kernel: [ pid ]   uid   tgid   total_vm      rss        nr_ptes   nr_pmds   swapents   oom_score_adj  name
Aug  8 15:32:40 H-LDOCKER-01 kernel: [33775]   1001  33775  7624373       2036828    4476      32        0          -998           java
Aug  8 15:32:40 H-LDOCKER-01 kernel: Memory cgroup out of memory: Kill process 33775 (java) score 0 or sacrifice child
Aug  8 15:32:40 H-LDOCKER-01 kernel: Killed process 33775 (java) total-vm:30497492kB, anon-rss:8134056kB, file-rss:13256kB

继续阅读

容器下 -XX:+HeapDumpOnOutOfMemoryError 未生成 dump 文件的问题

JVM 的启动命令一般都会加上参数 -XX:+HeapDumpOnOutOfMemoryError=/path/to/save/dump.hprof,用于在 JVM 发生 OOM 时自动生成内存 dump 文件。

应用在生产环境是运行在 Docker 容器里、由 K8S 负责管理容器。

但是有的应用发生 OOM 时,在 /path/to/save/dump.hprof 路径下并没有生成对应的 dump 文件。

优秀的运维同事小伍在测试环境进行了各种测试,得出以下两种情况没法生产文件:
1. /path/to/save/ 如果中间层的目录没有提前建好,是没法生成 dump 文件的。
2. 堆外内存不足 800M 时,也没法生成 dump 文件。

第1点没啥问题,文件所在的目录没有提前建好是会报 java.io.FileNotFoundException 异常的。

第2点其实是因为 JVM 运行在容器里,容器允许使用的内存是有上限的,比如分配给容器的是 4G 内存,JVM 堆占用 80%,那么堆外内存就只能占用 20% 即 800M。

发生 OOM 时,JVM 占用了 3.2G;对于堆外内存,线程、JVM自身、应用申请的本地内存等都要在这里分配,OOM dump 也需要利用堆外内存,容器使用的总内存达到 4G 内存上限时,触发系统的 oomkiller 机制把容器进程杀死。

这带来一个小问题:如果要保证 JVM OOM 自动 dump 机制能顺利执行,我们就需要在容器里预留出足够的堆外内存,每个容器都得考虑预留,这就带来内存利用率的问题了。如果 JVM 直接运行在宿主操作系统,没有容器的限制,能申请的堆外内存是受限于系统能分配的内存的,不同应用的 JVM 可共享这个可分配内存空间。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

BTrace demo

不了解的 BTrace 的可以先看 BTrace 用户指南

被跟踪的程序

package net.coderbee.btrace;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author coderbee 2017年5月9日 下午9:50:18
 *
 */
public class BtraceObservable {
    private AtomicInteger counter = new AtomicInteger();

    public String targetMethod(int i) {
        try {
            Thread.sleep(2000);

            if (i % 10 == 0) {
                throw new IllegalStateException("测试抛异常状态。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return Thread.currentThread().getName() + "--" + i + " returned.";
    }

    private int tcount() {
        return counter.incrementAndGet();
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        BtraceObservable observable = new BtraceObservable();
        System.err.println(observable);

        ExecutorService service = Executors.newFixedThreadPool(2);
        service.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    int count = observable.tcount();
                    String string = observable.targetMethod(count);
                    System.out.println(string);
                }
            }
        });

        service.shutdown();
    }

}

跟踪脚本

package net.coderbee.btrace;

import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.BTrace;
import com.sun.btrace.annotations.Kind;
import com.sun.btrace.annotations.Location;
import com.sun.btrace.annotations.OnMethod;
import com.sun.btrace.annotations.ProbeClassName;
import com.sun.btrace.annotations.ProbeMethodName;
import com.sun.btrace.annotations.Return;
import com.sun.btrace.annotations.Self;
import com.sun.btrace.annotations.TargetInstance;
import com.sun.btrace.annotations.TargetMethodOrField;

@BTrace
public class BtraceScript {

    /**
     * 可以用正则表达式匹配多个类、多个方法,然后用注解 @ProbeClassName 获得被调用的类, @ProbeMethodName
     * 获得被调用的方法。
     * 
     * 注意正则表达式定义
     */
    // /java\\.io\\..*Input.*/
    @OnMethod(
            clazz = "net.coderbee.btrace.BtraceObservable",
            method = "/t.*/")
    public static void func(@ProbeClassName String className,
            @ProbeMethodName String methodName) {
        BTraceUtils.println("ProbeClassName:" + className + ", ProbeMethodName:"
                + methodName);
    }

    /**
     * 用 @Return 获取方法的返回值
     */
    @OnMethod(
            clazz = "net.coderbee.btrace.BtraceObservable",
            method = "targetMethod",
            location = @Location(Kind.RETURN) )
    public static void retVal(@Return String retVal) {
        BTraceUtils.println("target method return:" + retVal);
    }

    /**
     * 获取调用对象、被调用对象、调用方法的信息
     */
    @OnMethod(clazz = "net.coderbee.btrace.BtraceObservable",
            method = "tcount",
            location = @Location(value = Kind.CALL, clazz = "/.*/",
                    method = "/.*/") )
    public static void call(@Self Object self, @TargetInstance Object target,
            @TargetMethodOrField String method) {
        BTraceUtils.println("on call, self:" + self + "\ntarget:" + target
                + ", method:" + method);
    }

    /**
     * 跟踪异常类的初始化。如果 类有多个构造函数,可以重载对应的方法。
     * 
     * @param self
     *            新创建的异常
     */
    @OnMethod(
            clazz = "java.lang.Throwable",
            method = "<init>")
    public static void onthrow(@Self Throwable self) {
        BTraceUtils.println(self);
    }
}


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

BTrace 用户指南

原文: BTrace-usersguide

BTrace 是用于 Java 的安全、动态跟踪工具。BTrace 插入跟踪动作到正在运行的 Java 程序类的字节码并热替换被跟踪程序的类。

BTrace 术语

  • probe point,探查点:一组位置或事件,跟踪语句执行的地方。位置或事件是我们希望执行一些跟踪语句的感兴趣的地方。
  • trace action,跟踪动作:跟踪语句是探查被触发时执行的。
  • action method,动作方法:当探查被触发时,被执行的 BTrace 跟踪语句定义在类的一个静态方法里。这样的方法被称为 动作 方法。

BTrace 程序结构

BTrace 程序是一个普通的 Java 类,有一个或多个标记有 BTrace 注解的 public static void 方法。注解用于指定跟踪程序的 位置。跟踪动作指定在静态方法体里。这些静态方法被称为 动作 方法。

BTrace 的限制

为了保证跟踪动作是 只读 且受限的,BTrace 只允许做一些严格受限的动作。通常,BTrace 类:

  • 不能 创建新对象。
  • 不能 创建新数组。
  • 不能 抛出异常。
  • 不能 捕获异常。
  • 不能 调用任意实例或静态方法。只能调用 com.sun.btrace.BTraceUtils 类的 public static 方法。
  • 不能 给目标程序的类或变量的静态或实例字段赋值。但是 BTrace 类可以给它自己的静态字段赋值(跟踪状态可以改变)。
  • 不能 有实例字段或方法。BTrace 类只能有 public static void 方法,所有的字段只能是静态的。
  • 不能 有外部、内部、嵌套类或本地类。
  • 不能 有循环(for, while, do ... while)。
  • 不能 继承任意类(父类必须是 java.lang.Object)。
  • 不能 实现接口。
  • 不能 包含 assert 语句。
  • 不能 使用类字面量。

继续阅读

Java Flight Recordings (JFR) — part 2 用 JFR 定位性能问题

内容目录

一、JFR 概览

二、找出瓶颈

不同的应用有不同的瓶颈。对于有些应用,瓶颈可能是等待 I/O 或网络,可能是线程之间的同步,或者是实际的 CPU 使用。对于其他,瓶颈可能是 GC 时间。很可能应用有不止一个瓶颈。

找出应用瓶颈的一个方法是查看 Events 选项卡。这是一个高级选项卡,可以做不少事情。如下图,可以选择自己感兴趣的事件:
jfr-event-types

继续阅读