分类 Java 相关 下的文章

Java Metaspace 造成的full GC

老司机 Kyle 又一次问我有没有兴趣查一个问题, 直接扔过来一个app的监控 dashboard, 又是 GC overhead 100%. 不过这次有点曲折.


监控数据如下:
dashboard.png

解读:

  1. GC overhead 100%, 也就是Java 进程占用的CPU 全部用来做GC 了;
  2. JVM CPU usage 从20% 升到了35%, 也就是Java 进程使用的CPU 量增加了很多;
  3. OOM 的数量从0变成了每秒几个;
  4. 有意思的是 Java 的可用heap 从2G 突然变成了5.1G.

背景相关:
这个app 设置的Java heap 是5.5G.

从上面信息大致可以猜测, 可能遇到如下问题:

  1. 大量调用 System.gc() 或 Runtime.gc();
  2. Metaspace (或Java8之前永久代) 用尽;
  3. 或者 Native 内存被用尽;

于是使用 jstat 命令查看 gccause:

$ jdk/bin/jstat -gccause 1075  2000 5
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC
  0.00 100.00   0.00   4.14  69.08  32.72  48945 2441.067  1848  806.736 3247.803 Metadata GC Threshold Metadata GC Threshold
  0.00   0.00   0.00   4.55  69.08  32.72  48947 2441.076  1853  808.914 3249.990 Metadata GC Threshold Last ditch collection
  0.00 100.00   0.00   5.07  69.08  32.72  48950 2441.092  1858  811.101 3252.193 Metadata GC Threshold Metadata GC Threshold
  0.00 100.00   0.00   4.82  69.08  32.72  48952 2441.102  1862  812.887 3253.989 Metadata GC Threshold Metadata GC Threshold
  0.00   0.00   0.00   3.93  69.08  32.72  48954 2441.111  1867  815.072 3256.183 Metadata GC Threshold Last ditch collection

上面各列名的解释:
S0: Survive 0; S1: Survive 1; E: Eden; O: Old; M: Metaspace; CCS: Compressed class; YGC: Young GC Count; YGCT: Young GC Time; FGC: Full GC Count; FGCT: Full GC Time; GCT: GC Time; LGCC: Last GC Cause; GCC: GC Cause this time.

从上面统计数据可以看到: Full GC(FGC) 的数量在持续的10s 内持续增加, 造成的原因就是: Metadata GC Threshold 和 Last ditch collection.

在解释上面2个原因之前, 我们可以看看 Metaspace 的使用策略. Metaspace 是用来替换之前的永久代的, 并且从Native 内存申请, 用来存放类的元数据. 从操作系统申请的native 内存被分成 chunks, 每个chunk 被分配给每个 ClassLoader, 这个chunk里面的class 如果全部被回收的时候, 这个 chunk 也会被回收. 另外这些Native 内存使用mmap分配, 不是使用malloc.

从网上找来一张好图:
concepts.png

上面图上有几个概念:

  1. reserved: Java 应用虚拟内存里面保留的大小, 这个这是虚拟内存申请量, 和实际使用量没关系;
  2. committed: 从操作系统(OS)里面申请的 native 内存的大小, 被分为若干个不同大小的 chunks;
  3. capacity: 被分配给 ClassLoader 的所有 chunk 内存之和, 不包含没被分配的 free chunk;
  4. chunk: 分成2种: 被分配给classLoader的和未被分配的;
  5. used: 所有被分配的 chunk 里面的使用量之和. 一个被分配的chunk 也是慢慢被使用完的.
    上面图里面没有涉及到的概念:
  6. Metaspace occupied threshold(high watermark), 它一般是committed 的一个百分比, 由 -XX:MaxMetaspaceFreeRatio 和 -XX:MinMetaspaceFreeRatio 这个2个参数决定.

当 ClassLoader 要分配一个新的class空间的时候, 它检查自己拥有的chunk, 如果当前拥有的chunk内部空间不足的时候, 就会去free chunk 里申请一个新的chunk. JVM 内部维护一个metaspace 已经被占用的 threshold (high watermark), 当已经占用的量(capacity) 触及这个 threshold 的时候, 就会触发一个 Full GC (Metadata GC Threshold) GC. 当这个Full GC 还不能回收到足够的空间时, 就会看到 Full GC (Last ditch collection).

verbose GC log 里面的日志:

2022-10-01T18:11:27.974-0700: 407174.877: [Full GC (Metadata GC Threshold) 2022-10-01T18:11:27.974-0700: 407174.877: [GC concurrent-root-region-scan-end, 0.0001638 secs]
2022-10-01T18:11:27.974-0700: 407174.877: [GC concurrent-mark-start]
 189M->189M(5120M), 0.4585377 secs]
   [Eden: 0.0B(390.0M)->0.0B(390.0M) Survivors: 2048.0K->0.0B Heap: 189.4M(5120.0M)->189.4M(5120.0M)], [Metaspace: 362167K->362167K(1525760K)]
Heap after GC invocations=50066 (full 1356):
 garbage-first heap   total 5242880K, used 193982K [0x00000006a0800000, 0x00000006a0a05000, 0x00000007e0800000)
  region size 2048K, 0 young (0K), 0 survivors (0K)
 Metaspace       used 362167K, capacity 367544K, committed 524288K, reserved 1525760K
  class space    used 15775K, capacity 16721K, committed 48228K, reserved 1048576K
}
 [Times: user=0.65 sys=0.01, real=0.46 secs]
{Heap before GC invocations=50066 (full 1356):
 garbage-first heap   total 5242880K, used 193982K [0x00000006a0800000, 0x00000006a0a05000, 0x00000007e0800000)
  region size 2048K, 0 young (0K), 0 survivors (0K)
 Metaspace       used 362167K, capacity 367544K, committed 524288K, reserved 1525760K
  class space    used 15775K, capacity 16721K, committed 48228K, reserved 1048576K
2022-10-01T18:11:28.433-0700: 407175.336: [Full GC (Last ditch collection)  189M->189M(5120M), 0.4538734 secs]

-- 未完待续

解密 Java 的 https 流量

最近有个同事发现生产环境中有些服务比较慢, 检查一番后, 发现一个服务调用在tcp 3次握手和 ssl client hello 包之间竟然有200ms 左右的延迟. 接下来, 该同事就想弄清楚: 为什么在3次握手和 client hello之间有200ms 延迟. 他知道有些语言是直接调用的 OpenSSL 的库, 于是问我 Java 里面是不是也是使用 OpenSSL的封装?

我之前写过2篇 Windows 上解密 chrome 的 https 流量 & MAC 上解密 chrome 的 https 流量, 当时就想: 是不是Java 也可以自动把 ssl key 劫持下来, 然后后边自动揭秘. 当时就找到了这个已经持续好多年的项目: jsslkeylog, 这个项目就是通过 Java Agent的方式, 注入相关的代码, 把ssl key 劫持下来.

所以, 当同事问我是不是 OpenSSL 封装的时候, 我立马想到这个项目. 在这个项目里面, 它通过注入的方式把 ssl key 记录下来, 那么肯定是Java 自己原生写的 SSL 的代码处理.

一些核心的代码类: https://github.com/jsslkeylog/jsslkeylog/tree/master/src/main/java/net/sf/jsslkeylog

关于 NSS(Network Security Services)

NSS (Network Security Services) 是由 Mozilla 项目开发的一个模块化、可复用的网络安全服务库,它提供了一系列的安全协议、密钥管理、证书管理、加密算法等安全功能的实现。NSS 被广泛地应用于多种网络应用程序中,包括 Web 浏览器、电子邮件客户端、虚拟专用网络 (VPN) 和移动设备等。

NSS 的功能涵盖了多种协议和技术,包括 SSL、TLS、PKCS#11、S/MIME、X.509 证书、公钥基础设施 (PKI)、安全套接字层 (SSL) 和传输层安全 (TLS) 等。NSS 在这些领域是具有广泛应用的库之一,是开源社区中信任和可靠的安全库。

NSS 还为应用程序提供了一些基本的加密功能,包括随机数生成器、哈希函数、密钥交换、数字签名、加密和解密、证书签名和认证等。这些功能被设计成易于使用和可扩展的,帮助应用程序开发者更快速、更容易地构建安全的网络应用程序。

需要注意的是,NSS 是一个独立的项目,并不仅限于 Mozilla 项目中。它在其他开源项目中也被广泛使用,例如 Red Hat、Fedora、OpenSSL 和 OpenSSH 等。
项目地址: https://github.com/nss-dev/nss
wiki: https://en.wikipedia.org/wiki/Network_Security_Services

关于 NSS 的 key log 格式

官方文档已经找不到了, 这是一个缓存: https://udn.realityripple.com/docs/Mozilla/Projects/NSS/Key_Log_Format
你可以认为这是 NSS 暴露出的一个接口, 这样使用 Wireshark 就能够解密加密传输.
NSS_Key_Log_Format.png

key log 的最新格式

这是 Mozilla 提交给 IEEE 的最新格式, 可以认为之前都不是经过认证, 只是 NSS 和 wireshark 之间商量的一个格式.
https://datatracker.ietf.org/doc/draft-thomson-tls-keylogfile/

wireshark 支持的格式

https://github.com/boundary/wireshark/blob/d029f48e4fd74b09848fc309630e5dfdc5d602f2/epan/dissectors/packet-ssl-utils.c#L4164-L4182
https://github.com/boundary/wireshark/blob/07eade8124fd1d5386161591b52e177ee6ea849f/epan/dissectors/packet-ssl-utils.c#L4204-L4207

两个揭秘 Java TLS 的工具

https://github.com/neykov/extract-tls-secrets/
https://github.com/jsslkeylog/jsslkeylog/tree/master/src/main/java/net/sf/jsslkeylog

使用 -Djavax.net.debug=all 生成的日志直接提取key的工具(TLS 1.3不适用):
https://dimosr.github.io/decrypting-tls-traffic-with-wireshark/
https://gist.github.com/tsaarni/14cc3341d0996e25671f5ca894842ec9

JDK 关于 javax.net.debug 的说明

https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/ReadDebug.html

JDK 使用到的关于TLS 版本的启动参数

  1. -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3
    jdk.tls.client.protocols 是一个 JDK (Java Development Kit) 的系统属性,这个属性可以用来指定与客户端建立安全连接时所支持的 TLS 版本的范围。具体来讲,这个属性可以用来限制客户端连接到服务端时可用的 TLS 协议版本。jdk.tls.client.protocols 属性的值是一个以逗号分隔的 TLS 协议版本列表.
  2. -Dhttps.protocols=TLSv1.2,TLSv1.3
    https.protocols 是一个 Java 系统属性, 可以用来指定在执行 HTTPS 请求时支持的协议版本。特别是当我们使用 Java 编写的程序向使用 HTTPS 存储在远程服务器上的资源发出请求时,这个属性就非常有用了。参数的值表示 Java 程序只会使用 TLS 1.2 和 TLS 1.3 协议版本来发起 HTTPS 请求。如果远程服务器不支持这些协议,则连接将失败。
  3. -Djavax.net.debug=ssl,keygen,handshake

Netty 抛出的 io.netty.util.internal.OutOfDirectMemoryError

最近遇到一个奇怪的Java direct memory 导致的 OutOfDirectMemoryError. 我们的应用程序在启动的时候, 明明设置了-XX:MaxDirectMemorySize=1G, 并且我们有持续的监控数据看到 direct memory 才用了不到10M, 就怎么爆出了io.netty.util.internal.OutOfDirectMemoryError 呢?

症状

日志里看到很多这样的错误:

msg=Root cause=OutOfDirectMemoryError: failed to allocate 4194304 byte(s) of direct memory (used: 1069547527, max: 1073741824)&st=io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 4194304 byte(s) of direct memory (used: 1069547527, max: 1073741824)
    at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:802)
    at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:731)
    at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:649)
    at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:624)
    at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:203)
    at io.netty.buffer.PoolArena.tcacheAllocateSmall(PoolArena.java:173)
    at io.netty.buffer.PoolArena.allocate(PoolArena.java:134)
    at io.netty.buffer.PoolArena.allocate(PoolArena.java:126)
    at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:394)
    at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:188)
    at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:179)
    at io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:140)
    at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:120)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:150)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:748)

初步诊断

上面的错误, 告诉我们以下一些细节:

  1. Java 应用程序的 direct memory 不够用了;
  2. direct memory 最大值是1G (1073741824), 已经使用1G (1069547527);
  3. 本次申请的用量是 4M (4194304 bytes);

大家知道 Java 提供了监控 direct memory 使用量的 MBean, 很多框架也支持持续导出这些数据形成时序 metrics. 下图是通过JDK 自带的 Jconsole 看到的 direct memory MBean 的数据: 6个 DirectByteBuffer, 共使用了49152 bytes.
mbeandirect.png
在服务器上使用 JConsole 不方便, 但是有很多命令行工具可以帮我们查看 MBean. 我们公司的框架暴露了这些数据为时序数据, 通过这些时序数据我们很容易看到, 我们的 direct memory 使用量不足10M. 所以这里矛盾的地方就是虽然我们设置了最大1G 的 direct memory, MBean 暴露的数据显示使用不到10M, 可是 Netty 却一直告诉我们1G 已经用完, 并且抛出 OutOfDirectMemoryError.

读 Netty 代码

根据出错栈, 我们找到了 Netty 的代码: io.netty.util.internal.PlatformDependent.incrementMemoryCounter.

    private static void incrementMemoryCounter(int capacity) {
        if (DIRECT_MEMORY_COUNTER != null) {
            long newUsedMemory = DIRECT_MEMORY_COUNTER.addAndGet(capacity);
            if (newUsedMemory > DIRECT_MEMORY_LIMIT) {
                DIRECT_MEMORY_COUNTER.addAndGet(-capacity);
                throw new OutOfDirectMemoryError("failed to allocate " + capacity
                        + " byte(s) of direct memory (used: " + (newUsedMemory - capacity)
                        + ", max: " + DIRECT_MEMORY_LIMIT + ')');
            }
        }
    }

根据这段代码, 我们注意到: DIRECT_MEMORY_LIMIT 在我们的情况下, 就是我们设置的 MaxDirectMemorySize 的值, 就是1G. DIRECT_MEMORY_COUNTER 就是 Netty 使用的 direct memory 的数量.
我们最初看到的那个 OutOfDirectMemoryError 就是这段代码抛出的. 很明显, 再加上那申请的4M, 就超过了1G 的最大限制. 为了验证我们说的情况, 我们事后做了一个 heap dump, 虽然当时没有抛出OutOfDirectMemoryError, MBean 显示的 direct memory 也只使用了4M多. 可是 DIRECT_MEMORY_COUNTER 的值, 却显示已经使用了490M 多:
heap.png

为什么MBean 报出的 direct memory 使用量和 Netty 的 DIRECT_MEMORY_COUNTER 的值不一样?

为了找出二者之间的差异, 我们要分别看下他们是怎么统计的.

MBean 的 direct memory 的统计方式

JDK 的 BufferPoolMXBean 统计 direct/mapper 的 memory 使用量, 它作为一个接口, 真正的实现类可以在 JDK 找到. sun.management.ManagementFactoryHelper (JDK8) 里面就有一个实现类:

private static BufferPoolMXBean
        createBufferPoolMXBean(final sun.misc.JavaNioAccess.BufferPool pool)
    {
        return new BufferPoolMXBean() {
            private volatile ObjectName objname;  // created lazily
            @Override
            public ObjectName getObjectName() {
                ObjectName result = objname;
                if (result == null) {
                    synchronized (this) {
                        result = objname;
                        if (result == null) {
                            result = Util.newObjectName(BUFFER_POOL_MXBEAN_NAME +
                                ",name=" + pool.getName());
                            objname = result;
                        }
                    }
                }
                return result;
            }
            @Override
            public String getName() {
                return pool.getName();
            }
            @Override
            public long getCount() {
                return pool.getCount();
            }
            @Override
            public long getTotalCapacity() {
                return pool.getTotalCapacity();
            }
            @Override
            public long getMemoryUsed() {
                return pool.getMemoryUsed();
            }
        };
    }

从上面代码可以看出, 真正的使用量, 都是从sun.misc.JavaNioAccess.BufferPool获得, 沿着这个线索找下去, 就在java.nio.Bits里面找到2个BufferPool的真正实现, 分别记录了 direct/mapper 的使用量. 其中, direct memory 的 count, totalCapacity 和 memory used 分别是由 Bits 的 count, totalCapacity 和 reservedMemory记录.

return new sun.misc.JavaNioAccess.BufferPool() {
    @Override
    public String getName() {
        return "direct";
    }
    @Override
    public long getCount() {
        return Bits.count.get();
    }
    @Override
    public long getTotalCapacity() {
        return Bits.totalCapacity.get();
    }
    @Override
    public long getMemoryUsed() {
        return Bits.reservedMemory.get();
    }
};                       

我们单追踪 Bits.reservedMemory 是怎么 track 已经使用量的:

DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        //省略其它无关代码
        Bits.reserveMemory(size, cap);
  }

从上面的代码可以看到, 每当 java.nio.DirectByteBuffer 初始化的时候, 它都会调用 Bits.reserveMemory 代码去 track 内存使用量. 这样当我们想查看 direct memory 使用量的时候, 只要查看 Bits 的记录数据就好了.

Netty 的 DIRECT_MEMORY_COUNTER 的统计方式

DIRECT_MEMORY_COUNTER 的统计方式比较直观, 它只在 PlatformDependent 内部被使用. 每当想要申请一块新的 direct 内存时候, 它就调用 incrementMemoryCounter 方法去增加 DIRECT_MEMORY_COUNTER 的值.

原因

所以, 虽然他们都以 MaxDirectMemorySize 为最大值, 但是都看不到对方的存在, 都只统计自己知道的使用量. 所以这导致我们看到 MBean 里面暴露的值和真正的使用量并不一样.

扩展 - 有关 ByteBuffer

JDK 里面的ByteBuffer 的数据结构有下图, Buffer 是一个抽象类, 定义了通用的一些方法, 比如 limit(), reset(), mark(), flip(), rewind(), isDirect(), 等. 然后 Buffer 下面定义了 primitive 类型对应的各种 Buffer 类. ByteBuffer 又分为在 heap 的 HeapByteBuffer 和在直接内存的 MappedByteBuffer & DirectByteBuffer. 下面以 R 结尾的 ByteBuffer 代表 read only 的 ByteBuffer.
ByteBuffer.png

扩展 - Netty 自己的 metrics 如何暴露这些数据的

Netty 本来提供了一个 metrics, 只是需要自己把这些 metrics 数据导出. metrics 数据通过PooledByteBufAllocatorMetric 暴露.

Java JDK UnSafe 类

JDK 里面有个类你可能没有直接用过, 但是阅读 JDK 源代码的时候, 到某些关键点上, 却经常会遇到, 就是 sun.misc.Unsafe. 这个类为什么不是一个名词呢? 为什么它就像一个警告一样, 告诉你: 不要用, 不要用.

- 阅读剩余部分 -