• 近期将进行后台系统升级,如有访问不畅,请稍后再试!
  • 极客文库-知识库上线!
  • 极客文库小编@勤劳的小蚂蚁,为您推荐每日资讯,欢迎关注!
  • 每日更新优质编程文章!
  • 更多功能模块开发中。。。

JVM内存占用情况深入分析

很多同学都问过这个问题,为什么我的 Xmx 设置 4g,但是 TOP 命令查询 RES 却占用 5G,6G,甚至 10G。这个正常吗?也可以说正常,也可以说不正常,怎么判断?笔者今天就要为你解答这个问题,叫你如何分析 JVM 占用的内存都分配到了哪里,哪些地方合理,哪些地方异常。

内存分布

首先,列举一下一个JVM进程主要占用内存的一些地方:
  • Young
  • Old
  • metaspace
  • java thread count * Xss
  • other thread count * stacksize (非 Java 线程)
  • Direct memory
  • native memory
  • codecache
说明:包括但不限于此。
接下来一步一步验证每个区域占用的内存。并且为了验证这个问题,写了一个工具类,里面有给每个区域分配内存的方法,源码在文末。

运行过程中的JVM参数如下:
-verbose:gc -XX:+PrintGCDetails -Xmx2g -Xms2g -Xmn1g 
-XX:PretenureSizeThreshold=2M -XX:+UseConcMarkSweepGC -XX:+UseParNewGC  
-XX:CMSInitiatingOccupancyFraction=90 -XX:+UseCMSInitiatingOccupancyOnly 
-XX:MaxDirectMemorySize=512m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m

Young+Old

我们先从最简单的堆占用内存开始,即 Xmx 和 Xms 参数申明,它包括 young 和 old 区。分别分配 800M 和 200M 内存,main 方法如下:
public static void main(String[] args) throws Exception{
    youngAllocate(800);
    oldAllocate(200);
    Thread.sleep(300000);

}

通过 TOP 命令查看,RES 为 1G:
   PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                   
 22481 afei      20   0 4366m 1.0g  11m S  0.5 27.0   0:02.41 java  
通过 jstat 命令也能看到,Old 和 Eden 分别占用 200M 和 800M。
这里再增加一个有趣的测试,young 和 old 区分别分配 1000M 和 1000M 内存,main 方法如下:
public static void main(String[] args) throws Exception{
    youngAllocate(1000);
    oldAllocate(1000);
    // 为了 CMS GC 顺利触发,这里需要 sleep 5s 以上,建议时间长一点,让整个 CMS GC 顺利完成。
    Thread.sleep(300000);

}

这样就会导致发生一次 YGC 和一个 CMS GC,那么你认为这时候通过 TOP 命令查看 RES 结果是多少呢?这时候应该是 1.8G,除了 S0/S1 两个区域,eden 和 Old 区域都写入过数据,而JVM 使用过的内存就不会归还给操作系统,除非JVM进程宕机或者重启,这个结论很重要:
   PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                   
 22707 afei      20   0 4366m 1.8g  11m S  0.0 48.7   0:00.90 java

Young+Old+Metaspace

接下来,我们再通过程序在 Metaspace 中重复加载 20w 个对象,即 metaspace 分配 200M 左右的内存,main 方法如下:
public static void main(String[] args) throws Exception{
    youngAllocate(1000);
    oldAllocate(1000);
    metaspaceAllocate(200000);
    Thread.sleep(60000);

}

通过 TOP 命令查看,RES 为 2.0G:
   PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                   
 22781 afei      20   0 4472m 2.0g  12m S  0.0 54.7   0:07.51 java 
即前面分析的 1.8G+208M(213822/1024),在JVM进程退出时有一行这样的日志:
 Metaspace       used 213822K, capacity 215618K, committed 215936K, reserved 1165312K

Young+Old+Metaspace+DirectMemory

接下来,我们再通过程序给堆外分配 400M,main 方法如下:
public static void main(String[] args) throws Exception{
    youngAllocate(1000);
    oldAllocate(1000);
    metaspaceAllocate(200000);
    directMemoryAllocate(400);
    Thread.sleep(60000);

}

通过 TOP 命令查看,RES 为 2.4G:
   PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                   
 23329 afei      20   0 4874m 2.4g  12m S  0.0 65.2   0:12.67 java 

Abount DirectMemory

在 Java 的上下文里,特指通过一组特定的 API 访问 native memory,这组 API 主要由 DirectByteBuffer 暴露出来,其底层是通过 c 的 malloc 分配内存,API 参考:ByteBuffer.allocateDirect(1024),可以通过MaxDirectMemory限制分配上限。
这部分分配的内存可以通过 VisualVM 的 MBeans 查看,但是 MBeans 默认没有安装,需要我们自己安装。但是由于 VisualVM 的 MBeans 默认从 https://github.com/visualvm/visualvm.src/releases/download/1.3.9/com-sun-tools-visualvm-modules-mbeans.nbm 中下载 visualvm 插件,而这个路径已经不存在。所以建议去 https://github.com/oracle/visualvm/releases 上下载对应的版本,然后手动安装这个插件:工具-插件-已下载-添加插件,选择本地已经下载的插件,最后点击安装即可。笔者的 JDK8 默认下载 1.3.9 版本,那么就去 github 上下载 1.3.9 版本,只需要 MBeans 这个模块即可:
visual vm
通过 MBeans 查看 Direct Memory 占用内存非常方便:
direct memory usage

Young+Old+Metaspace+DirectMemory+线程栈

最后就是线程栈,笔者试图通过启动 20 个线程,并且设置-Xss10240k,但是并没有达到预期,这里作为一个遗留问题。等笔者哪天搞懂了,再发文说明。
  • Xss 案例
曾经群里有一个朋友就是因为 Xss 配置相当大导致 RES 占用 13G 左右。大概情况是这样,-Xms4g,-Xss40940k,dubbo 的 provider 服务。熟悉 dubbo 服务同学知道,dubbo 服务 provider 默认采用固定 200 个线程处理的方式。所以 200 个线程占用 8G,加上 4G 堆,以及一些其他内存,导致 RSS 高达 13G,恐怖!!!

codecache

这部分内存一般占用比较少,在 JVM 崩溃的文件 hs_err_pid18480.log 中有其内存占用情况:
CodeCache: size=245760Kb used=47868Kb max_used=47874Kb free=197891Kb
 bounds [0x00007f00b4de4000, 0x00007f00b7d54000, 0x00007f00c3de4000]
 total_blobs=12973 nmethods=12383 adapters=500
 compilation: enabled

知识总结

HotSpot VM 自己在 JIT 编译器、GC 工作等的一些时候都会额外临时分配一些 native memory,在 JDK 类库也有可能会有些功能分配长期存活或者临时的 native memory,然后就是各种第三方库的 native 部分可能分配的 native memory。
总之,RES 占比异常时,一一排查,不要忽略任何一部分可能消耗的内存。
jvm 使用了的内存,即使 GC 后也不会还给操作系统。
Direct Memory 内存查看:如果是 JDK 7 及以上版本,可以用 jconsole 或者 VisualVM 的 MBeans 窗口查看 java.nio.BufferPool.direct 属性。

文末福利

最后笔者推荐一个 JVM 参数-XX:NativeMemoryTracking==[off|summary|detail],可以窥探一些我们平常不怎么关注的内存占用部分,配置 JVM 参数后,执行如下命令即可:
jcmd 23448 VM.native_memory summary
命令执行结果如下:
Native Memory Tracking

测试源码

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;

/**
 * 每个方法的参数 m 都是表示对应区间分配多少 M 内存
 * @author afei
 * @date 2018-09-28
 * @since 1.0.0
 */
public class MemoryTest {
    private static final int _1m = 1024*1024;

    private static final long THREAD_SLEEP_MS = 10*1000;

    public static void main(String[] args) throws Exception{
        youngAllocate(1000);
        oldAllocate(1000);
        metaspaceAllocate(200000);
        directMemoryAllocate(400);
        // threadStackAllocate(400);
        Thread.sleep(60000);
    }

    /**
     * @param count 重复定义的 MyCalc 对象数量
     */
    private static void metaspaceAllocate(int count) throws Exception {
        System.out.println(“metaspace object count: “ + count);

        Method declaredMethod = ClassLoader.class.getDeclaredMethod(“defineClass”,
                new Class[]{String.class, byte[].class, int.class, int.class});
        declaredMethod.setAccessible(true);

        File classFile = new File(“/app/afei/MyCalc.class”);
        byte[] bcs = new byte[(int) classFile.length()];
        try(InputStream is = new FileInputStream(classFile);){
            // 将文件流读进 byte 数组
            while (is.read(bcs)!=-1){
            }
        }

        int outputCount = count/10;
        for (int i=1; i<=count; i++){
            try {
                // 重复定义 MyCalc 这个类
                declaredMethod.invoke(
                        MemoryTest.class.getClassLoader(),
                        new Object[]{“MyCalc”, bcs, 0, bcs.length});
            }catch (Throwable e){
                // 重复定义类会抛出 LinkageError: attempted  duplicate class definition for name: “MyCalc”
                // System.err.println(e.getCause().getLocalizedMessage());
            }
            if (i>=outputCount && i%outputCount==0){
                System.out.println(“i = “+i);
            }
        }
        System.out.println(“metaspace end”);
    }

    /**
     * @param m 分配多少 M direct memory
     */
    private static void directMemoryAllocate(int m){
        System.out.println(“direct memory: “+m+“m”);
        for (int i = 0; i < m; i++) {
            ByteBuffer.allocateDirect(_1m);
        }
        System.out.println(“direct memory end”);
    }

    /**
     * @param m 给 young 区分配多少 M 的数据
     */
    private static void youngAllocate(int m){
        System.out.println(“young: “+m+“m”);
        for (int i = 0; i < m; i++) {
            byte[] test = new byte[_1m];
        }
        System.out.println(“young end”);
    }

    /**
     * 需要配置参数: -XX:PretenureSizeThreshold=2M, 并且结合 CMS
     * @param m 给 old 区分配多少 M 的数据
     */
    private static void oldAllocate(int m){
        System.out.println(“old:   “+m+“m”);
        for (int i = 0; i < m/5; i++) {
            byte[] test = new byte[5*_1m];
        }
        System.out.println(“old end”);
    }

    // 需要配置参数: -Xss10240k, 这里的实验以失败告终
    private static void threadStackAllocate(int m){
        int threadCount = m/10;
        System.out.println(“thread stack count:”+threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                System.out.println(“thread name: “ + Thread.currentThread().getName());
                try {
                    while(true) {
                        Thread.sleep(THREAD_SLEEP_MS);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        System.out.println(“thread stack end:”+threadCount);
    }

}


  • 参考
笨神的文章:https://mp.weixin.qq.com/s/3sb_ovHhhTXTid3G5iZUew
R 大的知乎:https://www.zhihu.com/question/55033583/answer/142577881


丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:JVM 内存占用情况深入分析
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

欢迎 注册账号 登录 发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00