乐趣区

聊聊Java中的内存

JVM 的内存

先放一张 JVM 的内存划分图,总体上可以分为堆和非堆(粗略划分,基于 java8)

那么一个 Java 进程最大占用的物理内存为:

Max Memory = eden + survivor + old + String Constant Pool + Code cache + compressed class space + Metaspace + Thread stack(*thread num) + Direct + Mapped + JVM + Native Memory

堆和非堆内存

堆和非堆内存有以下几个概念:

init

表示 JVM 在启动时从操作系统申请内存管理的初始内存大小 (以字节为单位)。JVM 可能从操作系统请求额外的内存,也可以随着时间的推移向操作系统释放内存(经实际测试,这个内存并没有过主动释放)。这个 init 的值可能不会定义。

used

表示当前使用的内存量 (以字节为单位)

committed

表示保证可供 Jvm 使用的内存大小 (以字节为单位)。已提交内存的大小可能随时间而变化 (增加或减少)。JVM 也可能向系统释放内存,导致已提交的内存可能小于 init,但是 committed 永远会大于等于 used。

max

表示可用于内存管理的最大内存 (以字节为单位)。

NMT 追踪内存

NMT(Native Memory tracking) 是一种 Java HotSpot VM 功能,可跟踪 Java HotSpot VM 的内部内存使用情况(jdk8+)。

本文简单介绍下该工具的使用,主要用来解释 Java 中的内存

开启

在启动参数中添加 -XX:NativeMemoryTracking=detail

查看

jcmd 进程 id VM.native_memory summary scale=MB

输出结果

Native Memory Tracking:

Total: reserved=6988749KB, committed=3692013KB
                    堆内存
-                 Java Heap (reserved=5242880KB, committed=3205008KB)
                            (mmap: reserved=5242880KB, committed=3205008KB)
                    类加载信息
-                     Class (reserved=1114618KB, committed=74642KB)
                            (classes #10657)
                            (malloc=4602KB #32974)
                            (mmap: reserved=1110016KB, committed=70040KB)
                    线程栈
-                    Thread (reserved=255213KB, committed=255213KB)
                            (thread #248)
                            (stack: reserved=253916KB, committed=253916KB)
                            (malloc=816KB #1242)
                            (arena=481KB #494)
                    代码缓存
-                      Code (reserved=257475KB, committed=46551KB)
                            (malloc=7875KB #10417)
                            (mmap: reserved=249600KB, committed=38676KB)
                    垃圾回收
-                        GC (reserved=31524KB, committed=23560KB)
                            (malloc=17180KB #2113)
                            (mmap: reserved=14344KB, committed=6380KB)
                    编译器
-                  Compiler (reserved=598KB, committed=598KB)
                            (malloc=467KB #1305)
                            (arena=131KB #3)
                    内部
-                  Internal (reserved=6142KB, committed=6142KB)
                            (malloc=6110KB #23691)
                            (mmap: reserved=32KB, committed=32KB)
                    符号
-                    Symbol (reserved=11269KB, committed=11269KB)
                            (malloc=8544KB #89873)
                            (arena=2725KB #1)
                    nmt
-    Native Memory Tracking (reserved=2781KB, committed=2781KB)
                            (malloc=199KB #3036)
                            (tracking overhead=2582KB)

-               Arena Chunk (reserved=194KB, committed=194KB)
                            (malloc=194KB)

-                   Unknown (reserved=66056KB, committed=66056KB)
                            (mmap: reserved=66056KB, committed=66056KB)

nmt 返回结果中有 reserved 和 committed 两个值,这里解释一下:

reserved

reserved memory 是指 JVM 通过 mmaped PROT_NONE 申请的虚拟地址空间,在页表中已经存在了记录(entries),保证了其他进程不会被占用。
在堆内存下,就是 xmx 值,jvm 申请的最大保留内存。

committed

committed memory 是 JVM 向操做系统实际分配的内存(malloc/mmap),mmaped PROT_READ | PROT_WRITE,相当于程序实际申请的可用内存。

在堆内存下,就是 xms 值,最小堆内存,heap committed memory。

注意,committed 申请的内存并不是说直接占用了物理内存,由于操作系统的内存管理是惰性的,对于已申请的内存虽然会分配地址空间,但并不会直接占用物理内存,真正使用的时候才会映射到实际的物理内存。所以 committed > rss/res 也是很可能的

Linux 内存与 JVM 内存

再来说说 JVM 内存与该进程的内存。

现在有一个 Java 进程,JVM 所有已使用内存区域加起来才 2G(不包括 Native Memory,也没有显示调用 JNI 的地方),但从 top/pmap 上看该进程 res 已经 2.9G 了

#heap + noheap
Memory                         used       total     max        usage
heap                           1921M      2822M     4812M      39.93%
par_eden_space                 1879M      2457M     2457M      76.47%
par_survivor_space             4M         307M      307M       1.56% 
cms_old_gen                    37M        57M       2048M      1.84% 
nonheap                        103M       121M      -1         85.00%
code_cache                     31M        37M       240M       13.18%
metaspace                      63M        74M       -1         85.51%
compressed_class_space         7M         9M        1024M      0.75%
direct                         997K       997K      -          100.00
mapped                         0K         0K        -          NaN%

#top
top -p 6267
top - 17:39:40 up 140 days,  5:39,  5 users,  load average: 0.00, 0.01, 0.00
Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.2%us,  0.1%sy,  0.0%ni, 99.7%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   8059152k total,  5255384k used,  2803768k free,   148872k buffers
Swap:        0k total,        0k used,        0k free,  1151812k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 6267 root      20   0 8930m 2.9g  17m S  0.0 37.6   4:13.31 java

那么其余的 0.9G 内存去哪了呢?

这时候就要介绍下 JVM 与 Linux 内存的联系了

当 Java 程序启动后,会根据 Xmx 为堆预申请一块保留内存,并不会直接使用,也不会占用物理内存

然后申请(malloc 之类的方法)Xms 大小的虚拟内存,但是由于操作系统的内存管理是惰性的,有一个内存延迟分配的概念。malloc 虽然会分配内存地址空间,但是并没有映射到实际的物理内存,只有当对该地址空间赋值时,才会真正的占用物理内存,才会影响 RES/RSS 的大小。

所以可能会出现进程所用内存大于当前堆 + 非堆的情况。

比如说该 Java 程序在 5 分钟前,有一定活动,占用了 2.6G 堆内存(无论堆中的什么代),经过 GC 之后,虽然堆内存已经被回收了,堆占用很低,但 GC 的回收只是针对 Jvm 申请的这块内存区域,并不会调用操作系统回收内存。所以该进程的内存并不会释放,这时就会出现进程内存远远大于堆 + 非堆的情况。

至于 Oracle 文档上说的,Jvm 可能会向操作系统释放内存,经过测试没有发现释放的情况。不过就算有主动释放的情况,也不太需要我们程序关心了。

如果抛开 Native Memory 不看的话,堆最大内存(Xmx)+ 非堆内存总是会小进程所用内存的

RES/RSS(Resident Set Size)是常驻内存的意思,进程实际使用的物理内存

参考

  • https://zhanjindong.com/2016/…
  • https://docs.oracle.com/javas…
退出移动版