【JVM故障问题排查心得】「内存诊断系列」JVM内存与Kubernetes中pod的内存、容器的内存不一致所引发的OOMKilled问题总结(下)

简介: 【JVM故障问题排查心得】「内存诊断系列」JVM内存与Kubernetes中pod的内存、容器的内存不一致所引发的OOMKilled问题总结(下)

承接上文


之前文章根据《【JVM故障问题排查心得】「内存诊断系列」JVM内存与Kubernetes中pod的内存、容器的内存不一致所引发的OOMKilled问题总结(上)》我们知道了如何进行设置和控制对应的堆内存和容器内存的之间的关系,所以防止JVM的堆内存超过了容器内存,导致容器出现OOMKilled的情况。但是在整个JVM进程体系而言,不仅仅只包含了Heap堆内存,其实还有其他相关的内存存储空间是需要我们考虑的,一边防止这些内存空间会造成我们的容器内存溢出的场景,正如下图所示。

image.png

接下来了我们需要进行分析出heap之外的一部分就是对外内存就是Off Heap Space,也就是Direct buffer memory堆外内存。主要通过的方式就是采用Unsafe方式进行申请内存,大多数场景也会通过Direct ByteBuffer方式进行获取。好废话不多说进入正题。




JVM参数MaxDirectMemorySize


我们先研究一下jvm的-XX:MaxDirectMemorySize,该参数指定了DirectByteBuffer能分配的空间的限额,如果没有显示指定这个参数启动jvm,默认值是xmx对应的值(低版本是减去幸存区的大小)。


DirectByteBuffer对象是一种典型的”冰山对象”,在堆中存在少量的泄露的对象,但其下面连接用堆外内存,这种情况容易造成内存的大量使用而得不到释放

-XX:MaxDirectMemorySize
复制代码



-XX:MaxDirectMemorySize=size 用于设置 New I/O (java.nio) direct-buffer allocations 的最大大小,size 的单位可以使用 k/K、m/M、g/G;如果没有设置该参数则默认值为 0,意味着JVM自己自动给NIO direct-buffer allocations选择最大大小。





-XX:MaxDirectMemorySize的默认值是什么?


在sun.misc.VM中,它是Runtime.getRuntime.maxMemory(),这就是使用-Xmx配置的内容。而对应的JVM参数如何传递给JVM底层的呢?主要通过的是hotspot/share/prims/jvm.cpp。我们来看一下jvm.cpp的JVM源码来分一下。


// Convert the -XX:MaxDirectMemorySize= command line flag
  // to the sun.nio.MaxDirectMemorySize property.
  // Do this after setting user properties to prevent people
  // from setting the value with a -D option, as requested.
  // Leave empty if not supplied
  if (!FLAG_IS_DEFAULT(MaxDirectMemorySize)) {
    char as_chars[256];
    jio_snprintf(as_chars, sizeof(as_chars), JULONG_FORMAT, MaxDirectMemorySize);
    Handle key_str = java_lang_String::create_from_platform_dependent_str("sun.nio.MaxDirectMemorySize", CHECK_NULL);
    Handle value_str  = java_lang_String::create_from_platform_dependent_str(as_chars, CHECK_NULL);
    result_h->obj_at_put(ndx * 2,  key_str());
    result_h->obj_at_put(ndx * 2 + 1, value_str());
    ndx++;
  }
复制代码



jvm.cpp 里头有一段代码用于把 - XX:MaxDirectMemorySize 命令参数转换为 key 为 sun.nio.MaxDirectMemorySize的属性。我们可以看出来他转换为了该属性之后,进行设置和初始化直接内存的配置。针对于直接内存的核心类就在www.docjar.com/html/api/su…

public class VM {
    // the init level when the VM is fully initialized
    private static final int JAVA_LANG_SYSTEM_INITED     = 1;
    private static final int MODULE_SYSTEM_INITED        = 2;
    private static final int SYSTEM_LOADER_INITIALIZING  = 3;
    private static final int SYSTEM_BOOTED               = 4;
    private static final int SYSTEM_SHUTDOWN             = 5;
    // 0, 1, 2, ...
    private static volatile int initLevel;
    private static final Object lock = new Object();
    //......
    // A user-settable upper limit on the maximum amount of allocatable direct
    // buffer memory.  This value may be changed during VM initialization if
    // "java" is launched with "-XX:MaxDirectMemorySize=<size>".
    //
    // The initial value of this field is arbitrary; during JRE initialization
    // it will be reset to the value specified on the command line, if any,
    // otherwise to Runtime.getRuntime().maxMemory().
    //
    private static long directMemory = 64 * 1024 * 1024;
复制代码



上面可以看出来64MB最初是任意设置的。在-XX:MaxDirectMemorySize 是用来配置NIO direct memory上限用的VM参数。可以看一下JVM的这行代码。

product(intx, MaxDirectMemorySize, -1,
        "Maximum total size of NIO direct-buffer allocations")
复制代码



但如果不配置它的话,direct memory默认最多能申请多少内存呢?这个参数默认值是-1,显然不是一个“有效值”。所以真正的默认值肯定是从别的地方来的。

// Returns the maximum amount of allocatable direct buffer memory.
    // The directMemory variable is initialized during system initialization
    // in the saveAndRemoveProperties method.
    //
    public static long maxDirectMemory() {
        return directMemory;
    }
    //......
    // Save a private copy of the system properties and remove
    // the system properties that are not intended for public access.
    //
    // This method can only be invoked during system initialization.
    public static void saveProperties(Map<String, String> props) {
        if (initLevel() != 0)
            throw new IllegalStateException("Wrong init level");
        // only main thread is running at this time, so savedProps and
        // its content will be correctly published to threads started later
        if (savedProps == null) {
            savedProps = props;
        }
        // Set the maximum amount of direct memory.  This value is controlled
        // by the vm option -XX:MaxDirectMemorySize=<size>.
        // The maximum amount of allocatable direct buffer memory (in bytes)
        // from the system property sun.nio.MaxDirectMemorySize set by the VM.
        // If not set or set to -1, the max memory will be used
        // The system property will be removed.
        String s = props.get("sun.nio.MaxDirectMemorySize");
        if (s == null || s.isEmpty() || s.equals("-1")) {
            // -XX:MaxDirectMemorySize not given, take default
            directMemory = Runtime.getRuntime().maxMemory();
        } else {
            long l = Long.parseLong(s);
            if (l > -1)
                directMemory = l;
        }
        // Check if direct buffers should be page aligned
        s = props.get("sun.nio.PageAlignDirectMemory");
        if ("true".equals(s))
            pageAlignDirectMemory = true;
    }
    //......
}
复制代码



从上面的源码可以读取 sun.nio.MaxDirectMemorySize 属性,如果为 null 或者是空或者是 - 1,那么则设置为 Runtime.getRuntime ().maxMemory ();如果有设置 MaxDirectMemorySize 且值大于 - 1,那么使用该值作为 directMemory 的值;而 VM 的 maxDirectMemory 方法则返回的是 directMemory 的值。


因为当MaxDirectMemorySize参数没被显式设置时它的值就是-1,在Java类库初始化时maxDirectMemory()被java.lang.System的静态构造器调用,走的路径就是这条:

if (s.equals("-1")) {  
    // -XX:MaxDirectMemorySize not given, take default  
    directMemory = Runtime.getRuntime().maxMemory();  
}
复制代码


而Runtime.maxMemory()在HotSpot VM里的实现是:

JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void))  
  JVMWrapper("JVM_MaxMemory");  
  size_t n = Universe::heap()->max_capacity();  
  return convert_size_t_to_jlong(n);  
JVM_END  
复制代码


这个max_capacity()实际返回的是 -Xmx减去一个survivor space的预留大小。




结论分析说明


MaxDirectMemorySize没显式配置的时候,NIO direct memory可申请的空间的上限就是-Xmx减去一个survivor space的预留大小。例如如果您不配置-XX:MaxDirectMemorySize并配置-Xmx5g,则"默认" MaxDirectMemorySize也将是5GB-survivor space区,并且应用程序的总堆+直接内存使用量可能会增长到5 + 5 = 10 Gb 。




其他获取 maxDirectMemory 的值的API方法


BufferPoolMXBean 及 JavaNioAccess.BufferPool (通过SharedSecrets获取) 的 getMemoryUsed 可以获取 direct memory 的大小;其中 java9 模块化之后,SharedSecrets 从原来的 sun.misc.SharedSecrets 变更到 java.base 模块下的 jdk.internal.access.SharedSecrets;要使用 --add-exports java.base/jdk.internal.access=ALL-UNNAMED 将其导出到 UNNAMED,这样才可以运行

public BufferPoolMXBean getDirectBufferPoolMBean(){
        return ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)
                .stream()
                .filter(e -> e.getName().equals("direct"))
                .findFirst()
                .orElseThrow();
}
public JavaNioAccess.BufferPool getNioBufferPool(){
     return SharedSecrets.getJavaNioAccess().getDirectBufferPool();
}
复制代码




内存分析问题


-XX:+DisableExplicitGC 与 NIO的direct memory


  • 用了-XX:+DisableExplicitGC参数后,System.gc()的调用就会变成一个空调用,完全不会触发任何GC(但是“函数调用”本身的开销还是存在的哦~)。


  • 做ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,但是无法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题,如果有大量的DirectByteBuffer对象移到了old,但是又一直没有做cms gc或者full gc,而只进行ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为heap明明剩余的内存还很多(前提是我们禁用了System.gc)。




相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
8月前
|
Kubernetes Docker Python
Docker 与 Kubernetes 容器化部署核心技术及企业级应用实践全方案解析
本文详解Docker与Kubernetes容器化技术,涵盖概念原理、环境搭建、镜像构建、应用部署及监控扩展,助你掌握企业级容器化方案,提升应用开发与运维效率。
1140 108
|
7月前
|
Kubernetes Devops Docker
Kubernetes 和 Docker Swarm:现代 DevOps 的理想容器编排工具
本指南深入解析 Kubernetes 与 Docker Swarm 两大主流容器编排工具,涵盖安装、架构、网络、监控等核心维度,助您根据团队能力与业务需求精准选型,把握云原生时代的技术主动权。
655 115
|
7月前
|
存储 Kubernetes 网络安全
关于阿里云 Kubernetes 容器服务(ACK)添加镜像仓库的快速说明
本文介绍了在中国大陆地区因网络限制无法正常拉取 Docker 镜像的解决方案。作者所在的阿里云 Kubernetes 集群使用的是较旧版本的 containerd(1.2x),且无法直接通过 SSH 修改节点配置,因此采用了一种无需更改 Kubernetes 配置文件的方法。通过为 `docker.io` 添加 containerd 的镜像源,并使用脚本自动修改 containerd 配置文件中的路径错误(将错误的 `cert.d` 改为 `certs.d`),最终实现了通过多个镜像站点拉取镜像。作者还提供了一个可重复运行的脚本,用于动态配置镜像源。虽然该方案能缓解镜像拉取问题,
761 2
|
9月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
2875 0
|
9月前
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
951 1
|
9月前
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
1130 0
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
1050 0

相关产品

  • 容器服务Kubernetes版