JVM内存
先复习一下JVM内存划分图,基于Java8
堆(Heap)
虚拟机从操作系统那里申请来的的内存空间,是Java虚拟机所管理的内存中最大的一块,并且是所有线程共享的一块内存区域。主要用来为类实例对象和数组分配内存。可以通过-Xmx
和-Xms
来控制堆的最大可扩展大小(默认情况下-Xmx
为物理内存的1/4,-Xms
为物理内存的1/64)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
堆由新生代(Young)和老年代(Old)组成,java8字符串常量池也放在堆中了(可能是由于字符串常量池编译时不好确认大小,所以放在最大区域的堆中更合适一些)。新生代又被划分为三个区:Eden、From Survivor、To Survivor。
另:JDK1.8之前,堆中还存在永久代。1.8之后,使用元数据空间替代了。元数据空间使用的是物理内存
虚拟机栈(JVM Stack)
每个方法被执行时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息. 每个方法被调用至返回的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程(VM提供了-Xss
来指定线程的最大栈空间, 该参数也直接决定了函数调用的最大深度)。这里局部变量表,就是我们定义的方法内部变量,包括8大基本类型和对象引用。
- 线程私有
- 使用
-Xss
设置每个线程栈的大小。一般情况下256K是足够了。 - 如果被实现为固定大小内存,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。
- 如果被实现为动态扩展内存大小,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
本地方法栈(Native Stack)
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务(Native方法简单点来说就是一个java调用非java代码的接口。一个Native 方法由非java语言实现),普通开发基本无需关心。
程序计数器(PC Register)
- 线程私有。
- 执行java方法时,记录的是正在执行的字节码指令的地址。
- 执行native本地方法时,程序计数器的值为空(Undefined)。
- 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
- 是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域
元数据空间(MetaSpace)
在JDK1.8之前,堆中存在着叫方法区(Method Area)的区域,位于永久代,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据(就是说常量池在这里)。
在JDK1.8,改名为元数据空间(Metaspace),位置从堆中移动到了本地内存。但本质功能没变,都是用于存储已被虚拟机加载的类信息,常量、静态变量,还包括在类、实例、接口初始化时用到的特殊方法。(字符串常量池还在堆中,见堆中描述)
直接内存(Direct)
不是JVM运行时数据区的一部分,是堆(Heap)外的直接向系统申请的内存空间。所以通常读写性能会优于堆内存。java的NIO库允许java程序使用直接内存。不受JVM内存回收管理,大小可以通过MaxDirectMemorySize设置,默认与-Xmx参数值一致。
一般通过ByteBuffer.allocateDirect或者FileChannel.map来创建
总结下来,一个java进程占用的物理内存:
Total Memory = Heap(eden + survivor + old + String Constant Pool)+ MetaSpace + Thread stack(*thread num) + Direct + JVM + Native Memory
内存的几个状态概念
- init
JVM在启动时从操作系统申请内存管理的初始内存大小。
-
used
表示当前使用的内存量。
-
committed
可供 Jvm使用的内存大小。已提交内存的大小可能随时间而变化(增加或减少)。 JVM也可能向系统释放内存,导致已提交的内存可能小于 init,但是committed永远会大于等于used。
-
max
表示可用于内存管理的最大内存
内存监控
NMT追踪
NMT(Native Memory tracking)是一种Java HotSpot VM功能,可跟踪Java HotSpot VM的内部内存使用情况(jdk8+)
开启
在启动参数中添加-XX:NativeMemoryTracking=detail
可以设置summary、detail来开启;开启的话,大概会增加5%-10%的性能消耗
注意:参数需要添加到最前面,否则可能会报错“Native memory tracking is not enabled”
正确如 java -XX:NativeMemoryTracking=detail -jar
查看结果
|
|
|
|
结果信息中有两个值,reserved和committed,解释如下:
reserved
是指JVM 通过mmaped PROT_NONE 申请的虚拟地址空间,在页表中已经存在了记录(entries)。说白了,就是已分配的大小
在堆内存下,就是xmx值,jvm申请的最大保留内存。
committed
是JVM向操做系统实际分配的内存(malloc/mmap),mmaped PROT_READ | PROT_WRITE,相当于程序实际申请的可用内存。
在堆内存下,当xms没有扩容时就是xms值,最小堆内存,扩容后就是扩容后的值,heap committed memory。
注意,committed申请的内存并不是说直接占用了物理内存,由于操作系统的内存管理是惰性的,对于已申请的内存虽然会分配地址空间,但并不会直接占用物理内存,真正使用的时候才会映射到实际的物理内存。所以committed > res也是很可能的
VisualVM监控
能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈。
启动
JVisualVM是JDK自带的工具,因此在shell窗口中输入jvisualvm即可运行。(注意PATH环境变量)
开启监控
启动参数增加
|
|
其中,hostname需要设置本机IP,1099代表用于远程监听的端口号(注意确认防火墙是否开启)。authenticate=false代表不认证,ssl=false代表不适用ssl。
- 本地监控
本地进程会直接显示在“本地”列表中,直接点击即可
- 远程监控
- 先添加远程主机
- 打开JMX链接,输入配置好的主机名和端口号即可
安装插件
默认监控项有CPU、堆、元数据空间、类、线程,如果想要看直接内存,需要安装BufferMonitor插件。工具-插件-可用插件中,找到VisualVM-BufferMonitor进行安装,安装好后就有了Buffer Pools选项卡。其中可以看到直接内存的使用情况。
PMAP
Linux下的命令, 用于报告进程的内存映射关系。
|
|
一般情况下使用-x即可
显示格式
-
Address(虚拟地址):表示每个映射的起始虚拟地址。虚拟地址是以升序显示的。
-
Kbytes(虚拟映射大小):每个映射的虚拟大小(以千字节K为单位)。
-
RSS(驻留物理内存):每个映射驻留的物理内存量(即实际占用内存),包括与其他地址空间共享的物理内存。
-
Anon(匿名内存):使用系统页面大小计数的与指定映射相关联的匿名内存的页面数。
JVM内存与系统内存
我们经常会发现,我们的一个java进程运行时间久了以后,会出现JVM所有区使用的内存加起来,比使用top/pmap监控到的内存要大(JVM中不包括Native Memory,也不显式调用JNI)。那多余的那部分内存去哪了?
当Java程序启动后,会根据Xmx为堆预申请一块保留内存,并不会直接使用,也不会占用物理内存,然后申请(malloc之类的方法)Xms大小的虚拟内存,但是由于操作系统的内存管理是惰性的,有一个内存延迟分配的概念。malloc虽然会分配内存地址空间,但是并没有映射到实际的物理内存,只有当对该地址空间赋值时,才会真正的占用物理内存,才会影响RES(Resident Set Size 常驻内存)的大小。所以可能会出现进程所用内存大于当前堆+非堆的情况。
比如说该Java程序在5分钟前,有一定活动,占用了2.6G堆内存(无论堆中的什么代),经过GC之后,虽然堆内存已经被回收了,堆占用很低,但GC的回收只是针对Jvm申请的这块内存区域,并不会调用操作系统释放内存。所以该进程的内存并不会释放,这时就会出现进程内存远远大于堆+非堆的情况。
至于Oracle文档上说的,Jvm可能会向操作系统释放内存,经过测试没有发现释放的情况。不过就算有主动释放的情况,也不太需要我们程序关心了。
记录一次内存优化特例
情况大概是,一个专门用来录制RTMP视频流到本地的java服务,使用了javacv这个库。硬件环境为2核2G
各个阶段内存情况:
-
初始化启动时,占用内存150M左右。
-
启动一次录制任务时,top监控内存会升到450M,JVM监控内存为180M。
-
完成录制后,top监控内存为360M,JVM监控内存为160M。
在录制过程中,jvm有正常进行GC,但实际使用内存(如top监控)一直都是比JVM监控内存(VisualVM监控和PMAP监控)要高。并且任务结束后,多的内存也不会释放。
总结一下就是,JVM监控是没有覆盖javacv底层申请的内存的。因为在javacv中,是基于javaCPP(JNI)调用opencv和ffmpeg等库的,这些库均为c或c++所写。实际内存为这些库所申请调用,所以就没有体现在JVM的监控中。至于如何针对javacv的内存优化,还需要再学习😂