JDK 11升级导致堆外内存占用升高问题排查

应用升级JDK 11后,堆外内存管理策略变化致使内存利用率升高。通过调整Netty参数,恢复原有策略,有效降低了内存占用。

原文标题:记一次内存利用率问题排查

原文作者:阿里云开发者

冷月清谈:

本文分析了一次应用升级到 JDK 11 后,由于堆外内存(Direct Memory)管理策略变化导致的内存利用率告警问题。升级后,内存利用率缓慢增长,最终触发告警。经排查,发现是堆外内存的 Resident 内存增长导致。JDK 8 使用 NoCleaner 策略管理堆外内存,只分配虚拟内存,不占用物理内存。而 JDK 11 默认使用 HasCleaner 策略,会将分配的虚拟内存加载到物理内存,导致 Resident 内存升高。

具体来说,JDK 11 中 Netty 的 `USE_DIRECT_BUFFER_NO_CLEANER` 参数默认为 false,导致 HasCleaner 策略被启用。HasCleaner 策略在分配内存后,会调用 `UNSAFE.setMemory` 方法,将虚拟内存加载到物理内存,从而增加 Resident 内存占用。

为了解决这个问题,可以在 Java 启动参数中添加 `-Dio.netty.tryReflectionSetAccessible=true`、`--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED` 和 `--add-opens=java.base/java.nio=ALL-UNNAMED` 等参数,强制 Netty 使用 NoCleaner 策略,避免堆外内存占用物理内存。添加参数后,Resident 内存占用显著下降,恢复到 JDK 11 升级前的水平。

怜星夜思:

1、文章提到JDK11升级后,堆外内存管理策略从NoCleaner变成了HasCleaner,除了文中提到的性能影响,还有其他潜在的影响吗?
2、如果我的应用也使用了Netty,该如何判断当前使用的是哪种堆外内存管理策略?
3、除了Netty,还有其他框架或技术也使用堆外内存吗?它们在JDK11中是否也存在类似问题?

原文内容

阿里妹导读


本文详细记录和分析了在应用升级到JDK 11后,由于堆外内存(Direct Memory)管理策略的变化导致的内存利用率告警问题。

背景

近期,我们应用开始出现sunfire内存利用率的告警,规律是应用重启后,内存利用率缓慢增长,一段时间不重启后,就会出现告警,一开始看到内存利用率第一反应是堆内存利用率的问题,走了一些弯路,最终发现是堆外内存的影响,本文主要记录和总结该问题的排查过程。






环境

  • JDK版本:ajdk11_11.0.14.13_fp2

  • netty版本:4.1.31.Final

问题分析

内存利用率

登陆机器,使用 free -m 查看内存使用情况,可以看到内存利用率为76.5% = 6269/8192,不过这里有一个问题,这个76.5%和sufire上的82%是对不上的,原因是我们登陆机器后看到的是业务容器内存利用率,在sunfire上面选择单机就能分别看到POD、业务容器、运维容器利用率。













业务容器内存利用率

通过sunfire观察到运维容器内存利用率一直是比较稳定,重点需要分析的业务内存利用率,使用top命名查看各进程的内存使用情况,可以看到JAVA应用就占了74.3%,接下来继续分析JAVA的内存分布了。





JAVA进程内存





可以看到,JAVA进程内存主要可以分为堆内存和非堆内存/堆外内存

Java 堆内存

1.定义:

  • Java 堆内存是 JVM 用来存储所有 Java 对象的内存区域,所有通过 new 关键字创建的对象以及数组都在此区域分配内存。

2.配置:

  • Java 堆内存由 JVM 的垃圾回收器(GC)自动管理,负责回收不再被引用的对象以释放内存。

  • 堆内存的使用情况可以通过 JVM 参数 -Xms 和 -Xmx 来配置,其中:

  • -Xms 设置初始堆大小。

  • -Xmx 设置最大堆大小。

3.构成:

  • 堆内存通常被分为两个主要部分:新生代(Young Generation)和老年代(Old Generation)。

  • 新生代:包含新创建的对象,消费垃圾回收频繁。由于新对象大多数是短命的,因此 GC 处理频率较高。

  • 老年代:存放长生命周期的对象,GC 处理不如新生代频繁。

非堆内存/堆外内存
非堆内存:
  • 非堆内存是指不受 Java 垃圾回收管理的内存区域,包括方法区、Java 方法栈区、native 堆(C heap)等。

  • 特别强调的是,方法区(Metaspace 区域在现代 JVM 中),存储类的元数据和静态信息,也被视为非堆内存。

堆外内存(Direct Memory):
定义

堆外内存通常指直接内存(Direct Memory),可以通过 java.nio.ByteBufferallocateDirect() 方法分配,它包含Mapped Buffer pool和Direct Buffer pool。与 Java 堆内存相比,堆外内存不受垃圾回收的影响,因此可以减少 Full GC 对应用性能的影响,但需要手动管理内存生命周期。在sunfire中,堆外内存的数据来自JMX的接口,可通过java.nio:type=BufferPool,name=direct和java.nio:type=BufferPool,name=mapped查询出来。

配置

堆外内存可以通过 JVM 参数MaxDirectMemorySize来配置

  • -XX:MaxDirectMemorySize=

堆外内存的优势

1.降低延迟:

  • 使用堆外内存可以避免因 Full GC 导致的 Stop-The-World 现象,从而减少应用的暂停时间。

2.提高效率:

  • 通过减少 Java 堆和原生堆之间的数据拷贝,可以提高数据的读写效率。例如,在使用 NIO 进行大文件操作时,堆外内存可以直接进行内存映射,提高访问速度。许多大数据处理框架,如 Spark、Flink 和 Kafka,利用堆外内存以提高性能和资源利用率。例如,Netty 作为一套高性能网络通信框架,也大量使用了堆外内存来实现高效的数据传输。

Reserved (保留内存)/Committed (承诺内存)/Resident (常驻内存)

在分析下面的问题之前,我们先理解三个内存相关的名词,来帮助我们理解接下来的问题,在计算机系统中,特别是在涉及内存管理的上下文中,"Reserved"、"Committed" 和 "Resident" 是三个不同的术语,主要用于描述内存的使用情况。以下是对这三个术语的解释:

1.Reserved (保留内存):

  • 保留内存是指操作系统已经为某个应用程序预留的虚拟内存地址空间,但并没有实际分配物理内存。换句话说,保留的内存区域可以被应用程序使用,但在实际使用之前,操作系统不必立刻为其分配物理 RAM。保留内存的目的是为了保证应用程序可以在将来使用这些地址,而不会与其他应用程序发生冲突。

2.Committed (承诺内存):

  • 承诺内存是指已经分配并实际使用的内存。这部分内存可以被视为已承诺给应用程序使用的物理内存,操作系统为其分配了物理 RAM。简单来说,承诺内存就是已经被分配并实际存在于物理内存中的那部分内存。

3.Resident (常驻内存):

  • 常驻内存指的是当前在物理 RAM 中驻留的内存页。这部分内存是已经承诺并分配的内存,且确实加载到了物理内存中。常驻内存与承诺内存的主要区别在于,承诺内存不一定在物理内存中,可能会被交换到磁盘上,而常驻内存则永远是在物理内存中。

在本文中我们监控的内存利用率的指标,是统计的Resident (常驻内存)。

哪块内存区域的变化导致了内存利用率的增长

了解到了java进程的内存主要构成,那再回到一开始的问题,到底是什么原因导致了pod内存利用率的告警,通过每个指标的对比,就能很快的发现堆外内存的增长和内存利用率的陡升是同步的,查看当天做了什么变更,进行了JDK11升级的发布,那么问题就回到了为什么JDK11的升级会引发内存利用率的陡升呢?









为什么JDK11的升级会引发内存利用率的陡升

1.决策内存管理策略

之前JDK8时,USE_DIRECT_BUFFER_NO_CLEANER = true,走noCleaner(PlatformDependent.allocateDirectNoCleaner)的分支,升级到JDK11后,走hasCleaner(ByteBuffer.allocateDirect)的分支。

if (maxDirectMemory == 0
   || !hasUnsafe()
   || !PlatformDependent0.hasDirectBufferNoCleanerConstructor()) {
   USE_DIRECT_BUFFER_NO_CLEANER = false;
} else {
   USE_DIRECT_BUFFER_NO_CLEANER = true;
}
private static ByteBuffer allocateDirect(int capacity) {
return PlatformDependent.useDirectBufferNoCleaner() ?
PlatformDependent.allocateDirectNoCleaner(capacity) : ByteBuffer.allocateDirect(capacity);
}

2.NoCleaner策略(PlatformDependent.allocateDirectNoCleaner)

UNSAFE.allocateMemory这一行代码会调用native方法allocateMemory划分一块承诺内存 (Committed)

// 核心代码
newDirectBuffer(UNSAFE.allocateMemory(capacity), capacity)

3.HasCleaner策略(ByteBuffer.allocateDirect)

可以看到HasCleaner策略,除了执行UNSAFE.allocateMemory外汇,还会执行UNSAFE.setMemory(base, size, (byte) 0)这一行代码,这就是堆外内存增长的核心原因了; 这个方法背后会调用native方法setMemory,找到承诺并分配的内存加载到RAM物理内存中成为Resident内存。

DirectByteBuffer(int cap) {                  

   super(-1, 0, cap, cap);
   boolean pa = VM.isDirectMemoryPageAligned();
   int ps = Bits.pageSize();
   long size = Math.max(1L, (long)cap + (pa ? ps : 0));
   Bits.reserveMemory(size, cap);

   long base = 0;
   try {
       base = UNSAFE.allocateMemory(size);
   } catch (OutOfMemoryError x) {
       Bits.unreserveMemory(size, cap);
       throw x;
   }
   UNSAFE.setMemory(base, size, (byte) 0);
   if (pa && (base % ps != 0)) {
       // Round up to page boundary
       address = base + ps - (base & (ps - 1));
   } else {
       address = base;
   }
   cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
   att = null;
}

为什么JDK8升级到JDK11之后USE_DIRECT_BUFFER_NO_CLEANER = false

可以看到USE_DIRECT_BUFFER_NO_CLEANER依赖于maxDirectMemory、hasUnsafe() 、PlatformDependent0.hasDirectBufferNoCleanerConstructor(),通过观察日志应用启动日志(其实在升级JDK11的时候就会新增这个debug级别的告警日志但被忽略了-.-)发现maxDirectMemory和hasUnsafe()在JDK8和JDK11是一致的,不一样的就是PlatformDependent0.hasDirectBufferNoCleanerConstructor这个方法的返回值,下面我们看下为什么hasDirectBufferNoCleanerConstructor返回值不一样。

if (maxDirectMemory == 0
   || !hasUnsafe()
   || !PlatformDependent0.hasDirectBufferNoCleanerConstructor()) {
   USE_DIRECT_BUFFER_NO_CLEANER = false;
} else {
   USE_DIRECT_BUFFER_NO_CLEANER = true;
}





当DirectBuffer构造器不为null时,hasDirectBufferNoCleanerConstructor返回true,就会走到else分支 设置USE_DIRECT_BUFFER_NO_CLEANER = true; 而当DIRECT_BUFFER_CONSTRUCTOR不为null,需要ReflectionUtil.trySetAccessible设置成功。

final Object maybeDirectBufferConstructor =
                       AccessController.doPrivileged(new PrivilegedAction<Object>() {
                           @Override
                           public Object run() {
                               try {
                                   final Constructor<?> constructor =
                                           direct.getClass().getDeclaredConstructor(long.class, int.class);
                                   Throwable cause = ReflectionUtil.trySetAccessible(constructor, true);
                                   if (cause != null) {
                                       return cause;
                                   }
                                   return constructor;
                               } catch (NoSuchMethodException e) {
                                   return e;
                               } catch (SecurityException e) {
                                   return e;
                               }
                           }
                       });

DIRECT_BUFFER_CONSTRUCTOR = directBufferConstructor

由于默认没设置io.netty.tryReflectionSetAccessible的值,当java版本低于JDK9时,返回了true,也就是说之前是JDK8,ReflectionUtil.trySetAccessible设置成功了,所以DIRECT_BUFFER_CONSTRUCTOR不为null,走到else分支 设置USE_DIRECT_BUFFER_NO_CLEANER = true,升级到JDK11后就走到了if分支USE_DIRECT_BUFFER_NO_CLEANER = false

public static Throwable trySetAccessible(AccessibleObject object, boolean checkAccessible) {
       if (checkAccessible && !PlatformDependent0.isExplicitTryReflectionSetAccessible()) {
           return new UnsupportedOperationException("Reflective setAccessible(true) disabled");
       }
       try {
           object.setAccessible(true);
           return null;
       } catch (SecurityException e) {
           return e;
       } catch (RuntimeException e) {
           return handleInaccessibleObjectException(e);
       }
   }
private static boolean explicitTryReflectionSetAccessible0() {
       // we disable reflective access
       return SystemPropertyUtil.getBoolean("io.netty.tryReflectionSetAccessible", javaVersion() < 9);
   }
public static boolean getBoolean(String key, boolean def) {
       String value = get(key);
       if (value == null) {
           return def;
       }

       value = value.trim().toLowerCase();
       if (value.isEmpty()) {
           return def;
       }

       if (“true”.equals(value) || “yes”.equals(value) || “1”.equals(value)) {
           return true;
       }

       if (“false”.equals(value) || “no”.equals(value) || “0”.equals(value)) {
           return false;
       }

       logger.warn(
               “Unable to parse the boolean system property ‘{}’:{} - using the default value: {}”,
               key, value, def
       );

       return def;
   }

应该选择HasCleaner还是NoCleaner策略

一般建议采用NoCleaner策略,即使当前应该还没有达到内存利用率瓶颈。

因为noCleaner是Netty在4.1引入的策略:创建不带Cleaner的DirectByteBuffer对象,这样做的好处是绕开带Cleaner的DirectByteBuffer执行构造方法和执行Cleaner的clean()方法中一些额外开销,一方面可以减少Resident (常驻内存)的使用,另外当堆外内存不够的时候,也不会触发System.gc(),提高性能。

JDK11如何和JDK 8一样采用NoCleaner策略

在Java启动参数增加如下部分:

  • 添加jvm参数 -Dio.netty.tryReflectionSetAccessible=true 参数

  • 添加jvm参数 --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED参数 打开Unsafe权限

  • 添加jvm参数 --add-opens=java.base/java.nio=ALL-ALL-UNNAMED打开nio的包访问限制

JVM添加以上参数后,我门再来看一下内存变化,java进程RES内存减少了500多MB,也就是对应堆外内存占用的Resident (常驻内存)。

添加参数前:
TOP监控

Resident内存4.8G





NMT监控
$jcmd 8607 VM.native_memory scale=MB
8607:

Native Memory Tracking:

Total: reserved=7359MB, committed=5538MB

  •                 Java Heap (reserved=3840MB, committed=3840MB)
                               (mmap: reserved=3840MB, committed=3840MB)

  •                     Class (reserved=1227MB, committed=527MB)
                               (classes #83975)
                               (  instance classes #80682, array classes #3293)
                               (malloc=23MB #393787)
                               (mmap: reserved=1204MB, committed=504MB)
                               (  Metadata:   )
                               (    reserved=444MB, committed=443MB)
                               (    used=432MB)
                               (    free=11MB)
                               (    waste=0MB =0.00%)
                               (  Class space:)
                               (    reserved=760MB, committed=61MB)
                               (    used=55MB)
                               (    free=6MB)
                               (    waste=0MB =0.00%)

  •                    Thread (reserved=1183MB, committed=134MB)
                               (thread #1173)
                               (stack: reserved=1178MB, committed=128MB)
                               (malloc=4MB #7040)
                               (arena=1MB #2345)

  •                      Code (reserved=256MB, committed=184MB)
                               (malloc=15MB #51500)
                               (mmap: reserved=242MB, committed=170MB)

  •                        GC (reserved=26MB, committed=26MB)
                               (malloc=15MB #13986)
                               (mmap: reserved=11MB, committed=11MB)

  •                  Compiler (reserved=6MB, committed=6MB)
                               (malloc=6MB #4322)

  •                  Internal (reserved=72MB, committed=72MB)
                               (malloc=72MB #94258)

  •                     Other (reserved=525MB, committed=525MB)
                               (malloc=525MB #447)

  •                    Symbol (reserved=73MB, committed=73MB)
                               (malloc=69MB #977115)
                               (arena=4MB #1)

  •    Native Memory Tracking (reserved=25MB, committed=25MB)
                               (tracking overhead=25MB)

  •               Arena Chunk (reserved=55MB, committed=55MB)
                               (malloc=55MB)

  •                    Module (reserved=8MB, committed=8MB)
                               (malloc=8MB #45253)

  •              Synchronizer (reserved=2MB, committed=2MB)
                               (malloc=2MB #15079)

  •                    (null) (reserved=60MB, committed=60MB)
                                (mmap: reserved=60MB, committed=60MB)

arthas监控

direct堆外内存526 MB = 551703298 B/(1024*1024)

[arthas@8607]$ mbean java.nio:name=direct,type=BufferPool

OBJECT_NAME java.nio:name=direct,type=BufferPool

NAME VALUE

TotalCapacity 551703289
MemoryUsed 551703298
Name direct
Count 202
ObjectName java.nio:type=BufferPool,name=direct

mapped堆外内存几乎没有:

OBJECT_NAME    java.nio:name=mapped,type=BufferPool                                                                                            
-----------------------------------------------------                                                                                            
NAME           VALUE                                                                                                                            
-----------------------------------------------------                                                                                            
TotalCapacity  1024                                                                                                                            
MemoryUsed     1024                                                                                                                            
Name           mapped                                                                                                                          
Count          1                                                                                                                                
ObjectName     java.nio:type=BufferPool,name=mapped

添加参数后:

TOP监控

Resident内存4.2G,对比添加参数前减少了近0.6G(实际为500多 MB,这里单位为GB小数位截断导致),刚好和添加参数前的堆外内存526 MB差不多。





NMT监控

可以看到NMT监控到的JVM内存几乎没有变化,原因在于NMT监控的是reserved和committed内存,netty无论用哪种方式管理内存,都会在初始化时执行UNSAFE.allocateMemory这一行代码划分一块承诺内存 (Committed)。

jcmd 9964 VM.native_memory scale=MB
9964:

Native Memory Tracking:

Total: reserved=7214MB, committed=5399MB

  •                 Java Heap (reserved=3840MB, committed=3840MB)
                               (mmap: reserved=3840MB, committed=3840MB)

  •                     Class (reserved=1202MB, committed=501MB)
                               (classes #81265)
                               (  instance classes #78038, array classes #3227)
                               (malloc=14MB #260100)
                               (mmap: reserved=1188MB, committed=486MB)
                               (  Metadata:   )
                               (    reserved=428MB, committed=428MB)
                               (    used=418MB)
                               (    free=10MB)
                               (    waste=0MB =0.00%)
                               (  Class space:)
                               (    reserved=760MB, committed=59MB)
                               (    used=54MB)
                               (    free=5MB)
                               (    waste=0MB =0.00%)

  •                    Thread (reserved=1154MB, committed=130MB)
                               (thread #1144)
                               (stack: reserved=1148MB, committed=124MB)
                               (malloc=4MB #6866)
                               (arena=1MB #2287)

  •                      Code (reserved=255MB, committed=165MB)
                               (malloc=13MB #47242)
                               (mmap: reserved=242MB, committed=152MB)

  •                        GC (reserved=26MB, committed=26MB)
                               (malloc=15MB #11967)
                               (mmap: reserved=11MB, committed=11MB)

  •                  Compiler (reserved=5MB, committed=5MB)
                               (malloc=4MB #3623)

  •                  Internal (reserved=45MB, committed=45MB)
                               (malloc=45MB #28840)

  •                     Other (reserved=525MB, committed=525MB)
                               (malloc=525MB #452)

  •                    Symbol (reserved=72MB, committed=72MB)
                               (malloc=68MB #959726)
                               (arena=4MB #1)

  •    Native Memory Tracking (reserved=21MB, committed=21MB)
                               (tracking overhead=21MB)

  •                    Module (reserved=7MB, committed=7MB)
                               (malloc=7MB #38082)

  •              Synchronizer (reserved=2MB, committed=2MB)
                               (malloc=2MB #12973)

  •                    (null) (reserved=60MB, committed=60MB)
                               (mmap: reserved=60MB, committed=60MB)

arthas监控

direct堆外内存10 MB = 10526012 B/(1024*1024)

[arthas@10089]$ mbean java.nio:name=direct,type=BufferPool
OBJECT_NAME    java.nio:name=direct,type=BufferPool                                                                                            
-----------------------------------------------------                                                                                            
NAME           VALUE                                                                                                                            
-----------------------------------------------------                                                                                            
TotalCapacity  10526004                                                                                                                        
MemoryUsed     10526012                                                                                                                        
Name           direct                                                                                                                          
Count          147                                                                                                                              
ObjectName     java.nio:type=BufferPool,name=direct

mapped堆外内存仍然几乎没有

OBJECT_NAME    java.nio:name=mapped,type=BufferPool
-----------------------------------------------------
NAME           VALUE
-----------------------------------------------------
TotalCapacity  1024
MemoryUsed     1024
Name           mapped
Count          1
ObjectName     java.nio:type=BufferPool,name=mapped
sunfire监控

堆外内存:添加参数后可以看到堆外内存明显下降,和arthas监控的mbean堆外内存数据一致。





内存利用率:内存利用率明显下降,恢复到JDK11升级前水位。





参考文档:

https://aliyuque.antfin.com/sunfire/manual/nle3ub
https://aliyuque.antfin.com/sigmahost/bdrtdk/sco1i3#sZ2dH
https://aliyuque.antfin.com/sunfire/manual/sg4g44oz4m0tr058#tIBNI
https://github.com/netty/netty/pull/7650
https://stackoverflow.com/questions/57885828/netty-cannot-access-class-jdk-internal-misc-unsafe/57892679
https://stackoverflow.com/questions/31173374/why-does-a-jvm-report-more-committed-memory-than-the-linux-process-resident-set
https://stackoverflow.com/questions/41468670/difference-in-used-committed-and-max-heap-memory
https://stackoverflow.com/questions/71366522/how-does-java-guarateee-reserved-memory
https://www.youtube.com/watch?v=c755fFv1Rnk&t=1615s
https://www.pengzna.top/article/Java-Memory/
https://www.cnblogs.com/stateis0/p/9062152.html

https://www.cnblogs.com/exmyth/p/14205361.htmls


云上经典架构serverless版


本方案采用云上的Serverless架构,原生支持弹性伸缩、按量付费和服务托管,减少企业手动资源管理和性能成本优化的工作,同时通过高可用的配置,避免可能遇到的单点故障风险。    


点击阅读原文查看详情。



我可以补充一些,像DirectMemory主要被一些需要高性能I/O操作的框架或技术使用,比如:大数据处理框架,如 Spark、Flink 等,会使用堆外内存来存储和处理数据,提高 I/O 效率;还有,高性能的网络通信框架,除了Netty,还有比如 gRPC,也可能会使用堆外内存来优化网络传输性能。至于是是否也存在类似问题?这个我不敢保证。

就这个问题,我想到一点,即使在 JDK 8 中,堆外内存的管理也需要注意。如果使用了 NoCleaner 策略,一定要手动管理堆外内存的生命周期,否则很容易出现内存泄漏。在 JDK 11 中,虽然 HasCleaner 策略可以自动释放堆外内存,但仍然需要注意 Cleaner 机制的开销,避免对 GC 造成过大的压力。

其实,还可以通过代码判断。Netty 提供了 PlatformDependent.useDirectBufferNoCleaner() 方法,可以用来判断当前使用的是哪种策略。不过,这种方法需要修改代码,不太方便。

关于“JDK11升级后,堆外内存管理策略从NoCleaner变成了HasCleaner,除了文中提到的性能影响,还有其他潜在的影响吗?”这个问题,我觉得吧,HasCleaner策略因为使用了Cleaner机制,会在DirectByteBuffer对象被GC时自动释放堆外内存,这可以有效地防止堆外内存泄漏,这算是一个好处吧。当然,由于Cleaner机制依赖于虚引用,其释放内存的时机是不确定的,可能导致堆外内存释放不及时,造成内存占用持续增长,这就要具体情况具体分析了。

对这个问题,我补充一点,NoCleaner策略虽然性能更好,但需要手动管理堆外内存的生命周期,容易出现内存泄漏问题,排查起来也比较麻烦,不像HasCleaner,更为省心。

关于“如果我的应用也使用了Netty,该如何判断当前使用的是哪种堆外内存管理策略?”,最直接的办法是查看 Netty 的启动日志。如果看到 io.netty.tryReflectionSetAccessible 相关的日志,并且值为 false,就说明 Netty 使用的是 HasCleaner 策略。反之,如果值为 true 或没有相关日志,则使用的是 NoCleaner 策略。

HasCleaner策略下,堆外内存的分配和释放都更加可控,可以更精细地管理堆外内存的使用,减少内存浪费。但同时,Cleaner机制本身也有一定的开销,在高并发场景下,大量的Cleaner对象可能会对GC造成一定的压力。

针对“除了Netty,还有其他框架或技术也使用堆外内存吗?它们在JDK11中是否也存在类似问题?”这个问题,我可以肯定的告诉你,很多框架和技术都使用堆外内存,例如:一些高性能的数据库连接池,像 HikariCP,为了提高性能,也会使用堆外内存来存储连接信息;还有,一些缓存框架,如 Ehcache,也可以配置使用堆外内存来存储缓存数据。至于在 JDK 11 中是否存在类似问题,需要具体情况具体分析,不同的框架和技术对堆外内存的管理方式可能不同,因此受到 JDK 11 升级的影响也可能不同。

除了看日志,还可以通过 arthas 等 Java 诊断工具查看堆外内存的使用情况。如果堆外内存的 Committed 内存和 Resident 内存相差不大,则很可能是使用了 HasCleaner 策略。如果相差很大,则可能是 NoCleaner 策略。当然,这只是一种推测,并不能完全确定。