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

继续阅读

Java Flight Recordings (JFR) — Java 飞行记录器 – part 1

一、JFR 飞行记录器

Java Flight Recorder(JFR)是一个商业特性,用在生产服务器上是需要商业许可的。

JFR 记录了关于 Java 运行时及运行在其内的 Java 应用程序的详细信息,记录用少量的开销完成。数据是作为时间上的数据点(称为事件)记录的。典型的事件可以是线程等待锁、GC、CPU 周期使用数据等。

在创建飞行记录时,你可以选择哪些事件应当保存,这叫做记录模板。有些模板只保存基本事件,对性能几乎没有影响。其他模板可能有轻微的性能开销,还可能触发 GC 来收集更多信息。通常,超过百分之几的开销是很罕见。

飞行记录可用于调试很大范围的问题,从性能问题到内存泄漏或严重的锁竞争。

1、记录类型

1.1、连续录制

一个连续记录是指记录总是开着并保存,例如,过去六小时的数据。如果应用程序陷入问题,你可以转储(dump)这些数据,例如,从过去一小时的,看看出生问题时发生了什么。

连续记录的默认设置是使用记录 profile,开销极低。这个 profile 不收集堆统计信息或(内存)分配性能分析,但仍然收集了很多有用数据。

保持持续录制一直允许是很好的,对于调试非常罕见的问题时非常有用。记录可以用 jcmdJMC 手工转储。你也可以在 JMC 里设置触发器在一些特定的条件被满足后转储飞行记录。

1.2、性能分析录制

性能分析录制是指记录开着,运行一定时间,然后停止。通常, 性能分析录制允许更多的时间,有可能对性能有较大的冲击。被开启的时间可以被修改,取决于你的 性能分析录制。

使用 性能分析录制 的典型场景如下:

  • 剖析运行最频繁的方法和创建对象最多的地方。
  • 查找使用了越来越多内存的类,暗示着内存泄漏。
  • 查找因为同步而导致的瓶颈,更多类型的情景。

性能分析录制 将给出很多信息,即使你不是在定位特定的问题。 性能分析录制 将给你关于应用程序的很好的视图,可以帮助你找出瓶颈或需要提高的地方。

继续阅读

JVM CPU 使用率高 问题两则

最近有两个系统先后被恶意扫描,出现 CPU 使用率高的现象。很好,把问题暴露出来解决掉。

CPU 使用率很高,首先是要找出 CPU 在执行什么样的代码,然后在分析这些代码有什么问题。

一、问题定位

1、 用命令 “ps aux | grep APP” 找出应用的进程 id:

801       84703  5.6 28.8 4627436 1132100 pts/2 Sl   11:08  11:19 /usr/jdk1.6.0_38/bin/java APP

2、 找出耗CPU的线程,在Linux系统下用命令:“ top –H –p pid ”, pid 就是前面找出来的应用进程 ID 。这个命令会显示出当前占用CPU高的线程。

Tasks: 426 total,   0 running, 426 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.5%us,  0.6%sy,  0.0%ni, 72.3%id, 26.6%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   3924912k total,  3308088k used,   616824k free,      768k buffers
Swap:  8388600k total,  3236720k used,  5151880k free,    12304k cached

   PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
84784 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:00.85 java
84903 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:34.66 java
84983 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:00.99 java
85091 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:02.69 java
85164 appdeplo  20   0 4518m 1.1g 3816 S  0.4 29.0   0:04.92 java
84703 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:00.00 java
84704 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:00.42 java
84705 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:02.52 java
84706 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:02.64 java
84707 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:02.46 java
84708 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:02.39 java
84709 appdeplo  20   0 4518m 1.1g 3816 S  0.0 29.0   0:33.99 java

这里的 PID 比如 84784 是十进制的,需要转换为十六进制,用windows的计算器就可以转换了,转换为十六进制是:14B30

继续阅读

Java SE 6 故障排除指南 – 4、系统崩溃故障排除

崩溃或致命错误导致进程异常终止。有各种可能的理由导致崩溃。例如,崩溃可能是由于HotSpot VM、系统库、Java SE 库或API、程序本地代码、甚至操作系统里的 bug。极端因素如操作系统资源耗尽也可以导致崩溃。

因 HotSpot VM 或 Java SE库代码导致的崩溃是罕见的。本章提供如何检查崩溃的建议。有时候可以变通崩溃直到导致崩溃的源被诊断和修复(也就是可以避开崩溃)。

通常,崩溃的第一步是定位致命错误日志。这是HotSpot VM生成的文本文件。附录C-致命错误日志 解释了如何定位文件和文件的详细描述。

4.1 崩溃样本

本节展示一些样本来说明错误日志是如何用于启发崩溃原因的。

4.1 测定哪里发生崩溃

错误日志头显示了有问题的帧。见 C.3 格式头。

如果顶层帧是本地帧且不是操作系统本地帧,这表明问题可能发生在本地库,而不是在JVM里。解决崩溃的第一步是研究本地库发生崩溃的源。有三个选择,取决于本地库的源。

如果本地库是由你的程序提供,研究你的本地库的源代码。选项 -Xcheck:jni 可以帮助查找本地 bug。见 B.2.1 -Xcheck:jni 选项。

如果你的程序使用的本地库是由其他供应商提供,报告bug,提供致命错误日志信息。

继续阅读

Java SE 6 故障排除指南 – 5、挂起或循环进程故障排除

本章为挂起或循环进程的故障排除在特定程序上提供了信息和指导。

问题在涉及挂起或循环进程时发生。挂起可能因为多种原因发生,但经常是源于程序代码、API代码或库代码里的死锁。挂起甚至是因为 HotSpot VM的bug。

有时候,一个表面上是挂起的可能是个循环。例如,VM进程里的bug导致一个或多个线程进入死循环,会消耗掉所有可得CPU周期。

诊断挂起的最初步骤是找出VM进程是空闲还是消耗了所有可得CPU周期,为做这个要求使用操作系统工具。如果进程表现为繁忙且消耗了所有可得CPU周期,那么问题很可能是循环线程而不是死锁。

继续阅读