JVM内存

先复习一下JVM内存划分图,基于Java8

JAVA内存模型

堆(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

查看结果

1
2
3
4
5
6
7
jcmd 进程id VM.native_memory summary scale=MB
# summary: 分类内存使用情况.
# detail: 详细内存使用情况,除了summary信息之外还包含了虚拟内存使用情况。
# baseline: 创建内存使用快照,方便和后面做对比
# summary.diff: 和上一次baseline的summary对比
# detail.diff: 和上一次baseline的detail对比
# shutdown: 关闭NMT
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Native Memory Tracking:

Total: reserved=1287MB, committed=167MB
-            堆内存 Java Heap (reserved=128MB, committed=72MB)
                            (mmap: reserved=128MB, committed=72MB)

-            类加载信息 Class (reserved=1071MB, committed=54MB)
                            (classes #9891)
                            (malloc=5MB #13282)
                            (mmap: reserved=1066MB, committed=49MB)

-             线程栈 Thread (reserved=11MB, committed=11MB)
                            (thread #34)
                            (stack: reserved=11MB, committed=11MB)

-             代码缓存 Code (reserved=49MB, committed=2MB)
                            (mmap: reserved=49MB, committed=2MB)

-             垃圾回收  GC (reserved=8MB, committed=8MB)
                            (malloc=3MB #188)
                            (mmap: reserved=5MB, committed=4MB)

-             内部  Internal (reserved=5MB, committed=5MB)
                            (malloc=5MB #13595)

-             符号   Symbol (reserved=13MB, committed=13MB)
                            (malloc=11MB #105732)
                            (arena=2MB #1)
				NMT内存
-    Native Memory Tracking (reserved=2MB, committed=2MB)
                            (tracking overhead=2MB)

-               Arena Chunk (reserved=1MB, committed=1MB)
                            (malloc=1MB)

结果信息中有两个值,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环境变量)

开启监控

启动参数增加

1
2
3
4
5
-Djava.rmi.server.hostname=10.0.0.1 
-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=1099
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false

其中,hostname需要设置本机IP,1099代表用于远程监听的端口号(注意确认防火墙是否开启)。authenticate=false代表不认证,ssl=false代表不适用ssl。

  • 本地监控

​ 本地进程会直接显示在“本地”列表中,直接点击即可

  • 远程监控
    1. 先添加远程主机
    2. 打开JMX链接,输入配置好的主机名和端口号即可

安装插件

​ 默认监控项有CPU、堆、元数据空间、类、线程,如果想要看直接内存,需要安装BufferMonitor插件。工具-插件-可用插件中,找到VisualVM-BufferMonitor进行安装,安装好后就有了Buffer Pools选项卡。其中可以看到直接内存的使用情况。

PMAP

Linux下的命令, 用于报告进程的内存映射关系。

1
2
3
4
5
pmap (选项) (进程id)

-d 显示设备格式
-x 显示扩展信息
-q 不显示头尾行

一般情况下使用-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

各个阶段内存情况:

  1. 初始化启动时,占用内存150M左右。

  2. 启动一次录制任务时,top监控内存会升到450M,JVM监控内存为180M。

  3. 完成录制后,top监控内存为360M,JVM监控内存为160M。

​ 在录制过程中,jvm有正常进行GC,但实际使用内存(如top监控)一直都是比JVM监控内存(VisualVM监控和PMAP监控)要高。并且任务结束后,多的内存也不会释放。

​ 总结一下就是,JVM监控是没有覆盖javacv底层申请的内存的。因为在javacv中,是基于javaCPP(JNI)调用opencv和ffmpeg等库的,这些库均为c或c++所写。实际内存为这些库所申请调用,所以就没有体现在JVM的监控中。至于如何针对javacv的内存优化,还需要再学习😂