您的位置:首页 >聚焦 >

查看JVM进程的内存情况

2022-04-17 12:48:07    来源:程序员客栈

// DirectMemory.javapackage 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

文件结构如下图

run.py中的内容如下

其实就是调用了 javac 和 java 命令而已,

设置堆空间50M,-XX:MaxMetaspaceSize=16M

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

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

最后我们的目录结构如下

运行程序

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

使用 jps 查看进程的PID = 15933

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

./smem -t -k

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

USS=27.8M, PSS=28M,RSS=30.3M

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

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

如上图, 使用 top 和 ps 查看进程15933的RSS/RES,

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(已使用内存)的关系如下图, 更详细内容读者自行了解

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

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

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

进程15933当前时刻实际使用的内存54.2M,

虚拟内存1641572K=1603M,committed内存114552K=111.86M

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

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

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

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

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

进程的内存如下图所示

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

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

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

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

接下来

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

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

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

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

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

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

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

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

看一下内存的变化

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

输入2之后

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

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

我们所说的非堆内存包括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监控平台查看服务器的内存情况

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

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

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

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

接下来

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

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

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

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

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

【总结1】

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

【总结2】

如果要查看进程的committed内存, 使用pmap -d<进程ID>查看

如果要查看进程已使用的内存(USS,PSS,RSS), 使用smem工具查看, 使用top和ps命令也可以查看到RSS值,但这个RSS值包含共享的内存, 因此我们也要关注PSS,USS

如果要查看JVM的直接内存, 可以使用

jcmd <进程ID> VM.native_memory scale=KB

当使用unsafe.allocateMemory(30M)申请内存的时候,committed内存会增长30M, 但是已使用内存不会增长

当程序使用unsafe.allocateMemory(30M)申请到的内存时, 已使用内存会增长

jconsole ,jvisualvm, 阿里云arms 是监控不到unsafe.allocateMemory()方式申请的内存

关于如何监控远程Java进程可以查看我的这篇语雀文章

https://www.yuque.com/infuq/default/wwmdfk#rJSoP

关于JVM内存的布局图可以在下面这篇语雀文章中查找到

https://www.yuque.com/infuq/default/bzu9ef

再贴一张图

关键词: 操作系统 申请内存

相关阅读