查看JVM进程的内存情况
// 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
再贴一张图
相关阅读
-
世界热推荐:今晚7:00直播丨下一个突破...
今晚19:00,Cocos视频号直播马上点击【预约】啦↓↓↓在运营了三年... -
NFT周刊|Magic Eden宣布支持Polygon网...
Block-986在NFT这样的市场,每周都会有相当多项目起起伏伏。在过去... -
环球今亮点!头条观察 | DeFi的兴衰与...
在比特币得到机构关注之后,许多财务专家预测世界将因为加密货币的... -
重新审视合作,体育Crypto的可靠关系才能双赢
Block-987即使在体育Crypto领域,人们的目光仍然集中在FTX上。随着... -
简讯:前端单元测试,更进一步
前端测试@2022如果从2014年Jest的第一个版本发布开始计算,前端开发... -
焦点热讯:刘强东这波操作秀
近日,刘强东发布京东全员信,信中提到:自2023年1月1日起,逐步为...