查看JVM进程的内存情况

// DirectMemory.java
package com.infuq.memory;

import org.jctools.util.UnsafeAccess;
import sun.misc.Unsafe;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import java.util.Scanner;

public class DirectMemory {

    public static void main(String[] args) throws Exception {

        Scanner scanner = new Scanner(System.in);

        long _30M = 30 * 1024 * 1024;
        long direct = 0;
        Unsafe unsafe = UnsafeAccess.UNSAFE;
        while (scanner.hasNext()) {
            String input = scanner.next();

            if (input.equals("1")) {
                System.out.println("malloc...");
                // 向操作系统申请内存,底层调用glibc库的malloc库函数
                direct = unsafe.allocateMemory(_30M);
            }
            else if (input.equals("2")) {
                System.out.println("init...");
                byte b = 6;
                // 使用上一步向操作系统申请的内存
                unsafe.setMemory(direct, _30M, b);
            }
        }
    }
}

上面这个程序的功能, 执行之后, 等待用户输入, 如果输入1,那么程序会向操作系统申请30M的内存, 如果输入2, 那么程序会初始化申请的30M内存.

这里说的初始化的言外之意是模拟程序使用向操作系统申请的内存

程序运行之后, 我们会通过使用JDK自带的jconsole(或jvisualvm)工具查看进程内存情况, 使用top,ps等命令查看进程内存情况, 使用JDK自带的jcmd命令查看进程内存情况, 使用pmap命令查看进程内存情况, 使用阿里云的arms查看进程的内存情况, 使用smem工具查看进程的内存情况. 从多维度查看内存情况 .

本次实验的环境: JDK1.8 Win10下的WSL2的Ubuntu20
还会使用2个三方包: jctools-core-2.1.2.jar jol-core-0.9.jar

文件结构如下图

图片.png

run.py中的内容如下

图片.png

其实就是调用了 javac 和 java 命令而已,
设置堆空间50M, -XX:MaxMetaspaceSize=16M

访问 https://www.selenic.com/smem/download/ 下载一个smem工具, 可以用于查看进程的内存

图片.png

解压下载的 smem-1.4.tar.gz

最后我们的目录结构如下


图片.png

运行程序


图片.png

运行之后, 程序阻塞, 等待用户的输入

使用 jps 查看进程的PID = 15933


图片.png

我们先使用 smem 工具查看下内存, 如下图

./smem -t -k


图片.png

以上输出当前系统所有进程的内存情况, 由于我实验使用的是Win10的WSL系统, 所以系统里的进程很少. 能够看出进程15933使用的内存, USS=27.8M, PSS=28M, RSS=30.3M

图片.png

USS,PSS,RSS都是表示进程实际使用的内存. 更多关于USS,PSS,RSS关系和区别, 读者自行了解.

我们经常听到RSS/RES, 在使用top和ps命令的时候会看到, 如下图

图片.png
图片.png

如上图, 使用 top 和 ps 查看进程15933的RSS/RES = 54552KB, 即53.27M, 约等于使用 smem 工具查看的RSS=54.2M内存.

RSS是常驻于内存的内存, RSS中还会包含与其他进程一起共享的内存.

我们使用如下shell命令可以每隔2秒打印进程15933的实际使用的内存情况
i=0;while true; do echo $((i++)) $(./smem -t -k | tail -3 | head -1); sleep 2; done

我们还会使用如下shell命令每隔2秒打印进程15933的committed内存
i=0;while true; do echo $((i++)) $(pmap -d 15933 | tail -1); sleep 2; done

关于reserved(预留内存), committed(提交内存), used(已使用内存)的关系如下图, 更详细内容读者自行了解


图片.png

比如我们向操作系统申请30M的内存, 则committed=30M. 但是操作系统并不会马上将真实的30M内存全部分配给进程, 只会先分配一小部分真实内存给进程使用, 当再次需要真实内存的时候再次分配. 因此一个进程的committed内存一定大于等于used的内存.

好了, 我们把上面两个shell命令运行起来

然后我们捕捉某一时刻的内存情况如下图


图片.png

进程15933当前时刻实际使用的内存54.2M, 虚拟内存1641572K=1603M, committed内存114552K=111.86M

接下来输入1,那么我们的程序会向操作系统申请30M的内存

图片.png

如上图, 我们向操作系统申请了30M的内存, 而进程的已使用内存并没有变化, 但是进程commited内存从114552K->145276K, 相差30724K=30M.

我们继续再输入1, 结果如下图


图片.png

如上图, 继续向操作系统申请30M内存, 进程已使用的内存也没有变化, 而进程committed内存又从145276增长到176000K, 又相差了30M.

我们不做任何操作, 时间过去了一会...

进程的内存如下图所示


图片.png

在这一段时间我们并没有任何操作, 内存有了一些小变化, 这很正常, 毕竟JVM进程里面还有一些JVM自身的线程也要随着程序的运行需要申请一些内存, 后面我们使用 jconsole 连接到进程, 内存也会发生一些增长, 这都是正常情况.

我们使用JDK自带的 jcmd 命令查看内存


图片.png

committed=173226KB 与上图使用pmap显示的176000KB有一些差. 毕竟它们是两个不同的命令, 统计的角度不一样.

pmap 命令统计的会比 jcmd统计的更准确. 查看man手册, pmap统计的是进程自身的smaps文件

图片.png
图片.png

接下来


图片.png

如上图, 重点需要关注Heap和Internal内存的情况

我们使用JDK自带的 jconsole 工具查看内存

图片.png

上图查看的是堆空间的内存情况, committed=49152KB, 与使用 jcmd 命令查看的51200KB有一些差, 可以忽略, 毕竟是2个不同的工具统计的. 上图同时也说明了, 虽然向操作系统申请了50M的堆空间, 但是目前实际使用了Used=10578KB, 此时操作系统也只是把部分真实内存分配给进程, 只有随着进程的运行需要的内存越多, 操作系统才会分配更多的真实内存给进程, 当分配的真实内存一旦超过committed时, 也就会报OOM了.

我们再次捕捉某一时刻的内存情况


图片.png

接下来我们输入2, 我们写的程序就会使用申请到的内存

图片.png

如上图, 当我们真正使用内存的时候, committed(374664KB)内存没有发生变化, 而使用内存发生了变化, 增大了30M, 和我们之前申请的30M是一致的.

这个时候我们看一下通过 jconsole 统计的非堆内存的情况

图片.png

我们继续输入1, 再申请30M内存, 再输入2, 使用申请的内存,

看一下内存的变化

图片.png

和之前的实验一样, 当输入1申请内存时, committed内存发生了变化, 已经使用内存没有发生变化

输入2之后


图片.png

committed内存没有发生变化, 已使用内存增长了30M

而且我们再次看一下非堆内存, 与之前的统计几乎一样, 没变化.

图片.png

我们所说的非堆内存包括Metaspace, CodeCache, CCS, 使用ByteBuffer.allocateDirect(),使用unsafe.allocateMemory(), 使用FileChannel.map(), 使用FileChannel.transferTo()等申请的内存.其中重点要说的是ByteBuffer.allocateDirect()和unsafe.allocateMemory().虽然都是申请的直接内存, 也就是操作系统本地内存, 但是 jconsole 只能统计到使用ByteBuffer.allocateDirect()申请的直接内存, 它是无法统计到使用unsafe.allocateMemory()申请的直接内存. 我们使用的-XX:MaxMetaspaceSize也是控制ByteBuffer.allocateDirect()申请的直接内存大小, 无法控制unsafe.allocateMemory()申请的直接内存大小.比如我们使用的Dubbo, RocketMQ等底层网络通信都是使用Netty, Netty就是通过unsafe.allocateMemory()向操作系统申请内存并自己管理这块内存, Netty也会自己管理向操作系统申请内存的空间大小, 毕竟不能无限制向操作系统申请内存.

FileChannel.map() 和 FileChannel.transferTo() 涉及到零拷贝知识, 读者朋友可以去了解下, 在我的 https://www.yuque.com/infuq/others/miqbcc 文章也有记录

如果读者朋友所在公司的服务器部署在阿里云上, 通过阿里云的arms监控平台查看服务器的内存情况

图片.png

上图右下角的直接缓冲区与 jconsole 统计的直接内存一样, 它们都无法统计到使用unsafe.allocateMemory()申请的内存.

如果要查看堆内存的使用情况, 可以使用 jconsole 或者 arms 查看堆内存的情况, 它们的统计没问题.

如果要查看直接内存的情况, 或者查看进程的内存情况, 仅仅使用 jconsole 或者 arms 是不完全的, 看到的内存是比实际要少的.

上图并非此次实验程序的内存统计, 我是从线上找的一个服务器

接下来

当我一直输入1, 也就是一直向操作系统申请内存, 只能表明进程的committed内存一直在增长

图片.png

而且我的宿主机Win10的内存也不会随着committed内存增长而增长


图片.png

接下来我们输入2, 让进程使用申请到的内存


图片.png

进程已使用的内存到了1.5G


图片.png

宿主机的内存也从之前的6.6增长到了7.1G, 进程已使用内存也从828M增长到了1.5G, 两者增长量基本吻合的.

【总结1】
通过实验, 零零散散介绍了如何查看进程的内存, 包括committed内存, 已使用内存等. 进程使用unsafe.allocateMemory()申请内存只是属于committed内存, 只有在进程真正使用这块内存的时候, 操作系统才会一部分一部分的将真实的内存分配给进程使用. 通过实验也能知道, 使用unsafe.allocateMemory()方式申请内存是不受-XX:MaxMetaspaceSize参数控制的, 实验中设置-XX:MaxMetaspaceSize=16M , 但是我们程序已经申请使用了好几百M的内存.

【总结2】

1.如果要查看进程的committed内存, 使用pmap -d <进程ID>查看
2.如果要查看进程已使用的内存(USS,PSS,RSS), 使用smem工具查看, 使用top和ps命令也可以查看到RSS值, 但这个RSS值包含共享的内存, 因此我们也要关注PSS,USS
3.如果要查看JVM的直接内存, 可以使用 jcmd <进程ID> VM.native_memory scale=KB
4.当使用unsafe.allocateMemory(30M)申请内存的时候, committed内存会增长30M, 但是已使用内存不会增长
5.当程序使用unsafe.allocateMemory(30M)申请到的内存时, 已使用内存会增长
6.jconsole , jvisualvm, 阿里云arms 是监控不到unsafe.allocateMemory()方式申请的内存

关于如何监控远程Java进程可以查看我的这篇语雀文章
https://www.yuque.com/infuq/default/wwmdfk#rJSoP

关于JVM内存的布局图可以在下面这篇语雀文章中查找到
https://www.yuque.com/infuq/default/bzu9ef

再贴一张图


进程实际内存.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容