JAVA实现公园地图,JVM 解剖公园(12): 本地内存跟踪

1. 写在前面

“JVM 解剖公园”是一个持续更新的系列迷你博客,阅读每篇文章一般需要5到10分钟。限于篇幅,仅对某个主题按照问题、测试、基准程序、观察结果深入讲解。因此,这里的数据和讨论可以当轶事看,不做写作风格、句法和语义错误、重复或一致性检查。如果选择采信文中内容,风险自负。

Aleksey Shipilёv,JVM 性能极客

推特 @shipilev

问题、评论、建议发送到 aleksey@shipilev.net"">aleksey@shipilev.net

2. 问题

有 512MB 可用内存,参数设为-Xms512m -Xmx512m,为什么虚拟机会报告“内存不足无法继续”启动失败?

3. 理论

JVM 是本地应用程序,维持和存储内部数据结构需要占用内存,包括程序代码、生成的机器代码、堆元数据、类元数据、内部统计数据等等。这些没有包括在 Java 堆中,因为大多数是本地程序,用 C 语言在堆中申请或者通过 mmap 进行内存映射。JVM 做了很多工作,期望应用程序在长时间运行过程中能加载更多类、支持更多的生成代码。在内存受限的情况下,这些默认值对运行时间较短的应用显得过高。

从 OpenJDK 8 开始加入了一个很好的虚拟机内部特性,称为“本地内存跟踪(Native Memory Tracking NMT)”:监测所有虚拟机内部分配的内存,包括分类信息、内存分配者等等。对于理解虚拟机如何使用内存非常有价值。

配置 -XX:NativeMemoryTracking=summary 选项可以启用 NMT。可以使用 jcmd 转储当前 NMT 数据,也可以使用 -XX:+PrintNMTStatistics 在 JVM 中止时请求数据转储。加上 -XX:NativeMemoryTracking=detail 选项可以获取 mmap 内存映射和 malloc 回调。

大多数时候summary信息就足够了。当然也可以通过detail查看详细日志,了解由谁分配了内存以及用途,阅读虚拟机源代码,配合虚拟机不同选项查看带来的影响。以 “Hello World” 为例:

public class Hello {

public static void main(String... args){

System.out.println("Hello");

}

}

很明显,在已分配的内存中 Java 堆占了很大一部分,让我们配置 -Xmx16m -Xms16m 查看具体使用情况:

Native Memory Tracking:

Total: reserved=1373921KB, committed=74953KB

-                 Java Heap (reserved=16384KB, committed=16384KB)

(mmap: reserved=16384KB, committed=16384KB)

-                     Class (reserved=1066093KB, committed=14189KB)

(classes #391)

(malloc=9325KB #148)

(mmap: reserved=1056768KB, committed=4864KB)

-                    Thread (reserved=19614KB, committed=19614KB)

(thread #19)

(stack: reserved=19532KB, committed=19532KB)

(malloc=59KB #105)

(arena=22KB #38)

-                      Code (reserved=249632KB, committed=2568KB)

(malloc=32KB #297)

(mmap: reserved=249600KB, committed=2536KB)

-                        GC (reserved=10991KB, committed=10991KB)

(malloc=10383KB #129)

(mmap: reserved=608KB, committed=608KB)

-                  Compiler (reserved=132KB, committed=132KB)

(malloc=2KB #23)

(arena=131KB #3)

-                  Internal (reserved=9444KB, committed=9444KB)

(malloc=9412KB #1373)

(mmap: reserved=32KB, committed=32KB)

-                    Symbol (reserved=1356KB, committed=1356KB)

(malloc=900KB #65)

(arena=456KB #1)

-    Native Memory Tracking (reserved=38KB, committed=38KB)

(malloc=3KB #41)

(tracking overhead=35KB)

-               Arena Chunk (reserved=237KB, committed=237KB)

(malloc=237KB)

好吧,对于16MB Java 堆来说占用75MB内存显然是出乎意料之外。

4. 缩减内存:常见方法

让我们遍历 NMT 的输出结果,看看哪些部分否可以调优。

先从熟悉的部分下手:显,在已分配的内存中 Java 堆占了很大一部分,让我们配置 -Xmx16m -Xms16m 查看具体使用情况:

-                        GC (reserved=10991KB, committed=10991KB)

(malloc=10383KB #129)

(mmap: reserved=608KB, committed=608KB)

这里描述了 GC 本地结构。日志中可以看到,GCmalloc内存约10MBmmap内存约0.6MB。如果这些结构与堆相关,像 Marking Bitmap、Card Table、Remembered Set 等,可以预见随着堆大小增加,这部分占用的内存也会随之增大。实际结果也的确如此:

# Xms/Xmx = 512 MB

-                        GC (reserved=29543KB, committed=29543KB)

(malloc=10383KB #129)

(mmap: reserved=19160KB, committed=19160KB)

# Xms/Xmx = 4 GB

-                        GC (reserved=163627KB, committed=163627KB)

(malloc=10383KB #129)

(mmap: reserved=153244KB, committed=153244KB)

# Xms/Xmx = 16 GB

-                        GC (reserved=623339KB, committed=623339KB)

(malloc=10383KB #129)

(mmap: reserved=612956KB, committed=612956KB)

malloc分配的内存很可能是用 C 分配的堆空间,用作并行 GC 中任务队列;mmap分配的内存存储了 bitmap。随着堆大小增加,这部分占用的内存会占到配置堆大小的3%至4%。这会带来实际部署问题,就像文章一开始提出的:配置的堆大小会获取所有可用物理内存,超过内存限制。可能会触发内存交换,也可能会引起 OOM 异常 JVM 停止运行。

但是这种开销还依赖于使用的 GC,不同的 GC 会用不同的方式表示 Java 堆。例如,使用-XX:+UseSerialGC让 OpenJDK 切换到最轻量级 GC 后,在我们的测试用例上运行会得到戏剧化的结果:

-Total: reserved=1374184KB, committed=75216KB

+Total: reserved=1336541KB, committed=37573KB

--                     Class (reserved=1066093KB, committed=14189KB)

+-                     Class (reserved=1056877KB, committed=4973KB)

(classes #391)

-                            (malloc=9325KB #148)

+                            (malloc=109KB #127)

(mmap: reserved=1056768KB, committed=4864KB)

--                    Thread (reserved=19614KB, committed=19614KB)

-                            (thread #19)

-                            (stack: reserved=19532KB, committed=19532KB)

-                            (malloc=59KB #105)

-                            (arena=22KB #38)

+-                    Thread (reserved=11357KB, committed=11357KB)

+                            (thread #11)

+                            (stack: reserved=11308KB, committed=11308KB)

+                            (malloc=36KB #57)

+                            (arena=13KB #22)

--                        GC (reserved=10991KB, committed=10991KB)

-                            (malloc=10383KB #129)

-                            (mmap: reserved=608KB, committed=608KB)

+-                        GC (reserved=67KB, committed=67KB)

+                            (malloc=7KB #79)

+                            (mmap: reserved=60KB, committed=60KB)

--                  Internal (reserved=9444KB, committed=9444KB)

-                            (malloc=9412KB #1373)

+-                  Internal (reserved=204KB, committed=204KB)

+                            (malloc=172KB #1229)

(mmap: reserved=32KB, committed=32KB)

这里 “GC” 和 “Thread” 部分得到了改善。前者由于分配的元数据更少,后者是因为从默认的 Parallel GC 变为 Serial GC 后,需要的 GC 线程也随之减少。这意味着可以通过调整 Parallel、G1、CMS、Shenandoah 等 GC 的线程数量改进性能。接下来会讨论线程栈。注意,更换 GC 或者更改 GC 线程数量将影响性能,通过修改可以在时间、空间上找到合适的平衡点。

不仅如此,由于元数据表示方式少有不同 “Class” 部分也得到了改进。我们还能继续提升 “Class” 性能吗?可以试试类数据共享(Class Data Sharing CDS),使用-Xshare:on开启:

-Total: reserved=1336279KB, committed=37311KB

+Total: reserved=1372715KB, committed=36763KB

--                    Symbol (reserved=1356KB, committed=1356KB)

-                            (malloc=900KB #65)

-                            (arena=456KB #1)

-

+-                    Symbol (reserved=503KB, committed=503KB)

+                            (malloc=502KB #12)

+                            (arena=1KB #1)

在共享内存中加载预解析表示,这样又节省了0.5MB内部符号表空间。

接下来关注线程,相关日志如下:

-                    Thread (reserved=11357KB, committed=11357KB)

(thread #11)

(stack: reserved=11308KB, committed=11308KB)

(malloc=36KB #57)

(arena=13KB #22)

仔细查看日志内容,可以看到 “Thread” 大部分内存被线程堆栈占用。用 -Xss 选项可以把堆栈大小从默认值缩小到示例中的1M。注意:这样修改可能会产生 StackOverflowException 风险。如果确实需要修改,请务必测试软件各种配置检查可能带来的不良影响。试着用 -Xss256k 选项设为256KB,结果如下:

-Total: reserved=1372715KB, committed=36763KB

+Total: reserved=1368842KB, committed=32890KB

--                    Thread (reserved=11357KB, committed=11357KB)

+-                    Thread (reserved=7517KB, committed=7517KB)

(thread #11)

-                            (stack: reserved=11308KB, committed=11308KB)

+                            (stack: reserved=7468KB, committed=7468KB)

(malloc=36KB #57)

(arena=13KB #22)

还不错,又少了4MB。当然,对于线程多的应用效果更明显。线程很可能是仅次于 Java 堆的第二大内存消费者。

继续看线程部分,JIT 编译器本身也用到了线程。这就解释了为什么即使把堆栈大小设置为256KB,但上面的数据仍然显示堆栈大小是7517 / 11 = 683 KB。使用-XX:CICompilerCount=1减少编译器线程数,设置-XX:-TieredCompilation只启用最新编译层:

-Total: reserved=1368612KB, committed=32660KB

+Total: reserved=1165843KB, committed=29571KB

--                    Thread (reserved=7517KB, committed=7517KB)

-                            (thread #11)

-                            (stack: reserved=7468KB, committed=7468KB)

-                            (malloc=36KB #57)

-                            (arena=13KB #22)

+-                    Thread (reserved=4419KB, committed=4419KB)

+                            (thread #8)

+                            (stack: reserved=4384KB, committed=4384KB)

+                            (malloc=26KB #42)

+                            (arena=9KB #16)

喔,三个线程和它们的堆栈都没有了!同样,这种方式会影响性能:编译器线程越少意味着预热越慢。

在内存受限情况下,减少虚拟机占用内存的常见方法包括,减小 Java 堆大小、选择合适的 GC、减少虚拟机线程的数量、减小 Java 线程堆栈大小和减少线程数量。通过这些办法,16MB Java 堆测试的优化结果如下:

-Total: reserved=1373922KB, committed=74954KB

+Total: reserved=1165843KB, committed=29571KB

5. 缩减内存:大胆实验

这一部分提出的建议有点疯狂。使用有风险,请不要在家里自行尝试。

这一部分的优化,包括调整虚拟机内部设置,不保证优化有效,有可能会出现崩溃和其他意外。比如,虽然可以通过编码控制 Java 应用的堆栈大小,但我们并不知道 JVM 内部究竟发生了什么,因此减少虚拟机线程的堆栈大小是很危险的。不过,用-XX:VMThreadStackSize=256尝试一下还是很有意思的。

-Total: reserved=1165843KB, committed=29571KB

+Total: reserved=1163539KB, committed=27267KB

--                    Thread (reserved=4419KB, committed=4419KB)

+-                    Thread (reserved=2115KB, committed=2115KB)

(thread #8)

-                            (stack: reserved=4384KB, committed=4384KB)

+                            (stack: reserved=2080KB, committed=2080KB)

(malloc=26KB #42)

(arena=9KB #16)

是的,又少了2MB编译器和 GC 线程栈大小。

继续在编译器代码上下功夫:为什么不试着减少初始代码缓存(即生成的代码区域)大小呢?输入-XX:InitialCodeCacheSize=4096(字节):

-Total: reserved=1163539KB, committed=27267KB

+Total: reserved=1163506KB, committed=25226KB

--                      Code (reserved=49941KB, committed=2557KB)

+-                      Code (reserved=49941KB, committed=549KB)

(malloc=21KB #257)

-                            (mmap: reserved=49920KB, committed=2536KB)

+                            (mmap: reserved=49920KB, committed=528KB)

-                        GC (reserved=67KB, committed=67KB)

(malloc=7KB #78)

一旦编译的工作加重,占据的内存会马上膨胀起来,但目前为止还不错。

仔细看一下 “Class” 部分,可以看到 Hello World 程序占用的(Committed)4MB内存中大部分存储的是元数据。可以使用-XX:InitialBootClassLoaderMetaspaceSize=4096(字节)减少大小:

-Total: reserved=1163506KB, committed=25226KB

+Total: reserved=1157404KB, committed=21172KB

--                     Class (reserved=1056890KB, committed=4986KB)

+-                     Class (reserved=1050754KB, committed=898KB)

(classes #4)

-                            (malloc=122KB #83)

-                            (mmap: reserved=1056768KB, committed=4864KB)

+                            (malloc=122KB #84)

+                            (mmap: reserved=1050632KB, committed=776KB)

-                    Thread (reserved=2115KB, committed=2115KB)

(thread #8)

执行了所有这些大胆的优化后,Java 堆大小更接近16MB,最多只浪费8.5MB。

-Total: reserved=1165843KB, committed=29571KB

+Total: reserved=1157404KB, committed=21172KB

如果在构建 JVM 时进行优化应该可以更进一步。

6. 总结

有趣的是,可以看到本地开销如何随着 Java 堆大小发生变化:

8b0abed9ef6f4d729aceeda6340c8e57.png

上图证实了我们的直觉,即 GC 开销与 Java 堆大小之前比例为常数关系。当虚拟机成为内存总开销一部分时,虚拟机本地开销才会在小堆空间上起作用。上面的图片忽略了第二重要因素——线程栈。

7. 观察

JVM 的默认配置通常是为长时间运行的服务器应用准备的,包括 GC、内部数据结构的初始大小、堆栈大小等也是如此,因此可能不适用于短时间运行的内存受限应用程序。了解当前 JVM 配置条件下哪些部分占用内存最多,有助于在主机上加入更多 JVM。

用 NMT 探索虚拟机内存分配情况是一种非常具有启发性的练习,能让我们立刻知道从哪里入手优化应用占用的内存。在性能管理系统加入 NMT 在线监视,非常有助于在应用实际生产环境中调整 JVM 参数。比起解析 /proc/$pid/maps 中不透明的内存映射来确定 JVM 究竟在做什么要容易得多。

还可以参考阅读 Christine Flood 的 “OpenJDK 与容器”。

developers.redhat.com/blog/2017/04/04/openjdk-and-containers/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现简单的查询,各风景的查询,调用各函数,实现课程设计的目标。其中包含三个功能,一个是直接进入导航系统,利用主函数中已有的数据,进行查询:一个是进行创建数据,本程序中初始数据为农大的导航数据,如果需要也可以自己建立一个;最后一个是退出功能。设计该函数的目的是为了能够多次得应用dijkstra函数进行查询最短路径。同时该函数可以列出各景点的代号和对应的名称,这样大家只要输入代号就行了。方便进行查询。下面分别描述这些函数,建立它们函数原型。 1、主函数 函数原型:void main(void) 功 能:控制程序。 参 数:void 返 回 值:void 要 求:管理菜单命令并完成初始化。 2、菜单选择和处理函数 函数原型:int menu() 功 能:处理选择的菜单命令并接收用户选择的命令代码。 参 数:int 返 回 值:int 工作方式:返回命令代码的整数值,根据命令,调用相应函数。 要 求:只允许选择规定键,如果输入不合要求,则提醒用户重新输入。 3、建立邻接矩阵函数 函数原型:void createadj() 功 能:重新建立一个学生信息的记录。 参 数:void 返 回 值:void 工作方式:在需要的时候就可以有主菜单中调用 void createadj()函数。 要 求:必需输入信息记录,然后才能调用出search()函数进行查询。 4、dijkstra函数 函数原型:void dijkstra(intx,inty) 功 能:求两点间的最短路径 参 数:void 返 回 值:void 工作方式: 该函数被其它一些函数调用。 5、结束程序 函数原型:int Exit() 功 能:使程序正常结束运行 参 数:int 返 回 值:1 工作方式:在操作都完成后,可以调用int Exit()函数,使函数最终返回 1 运行exit(1),程序正常结束。 要 求:运行Exit()函数后可以选择是否要保存,选择y则先保存再返 回1值;如果选择n直接返回1值。详细的程序设计应从下到上,在本设计中就要先设计createadj函数;然后设计dijkstra函数;接着是search函数;menu函数;最后才是main函数。如此设计能大大提升设计速度,因为从下往上使编程时的高试过程简单许多,而做课程设计花费时间最多的就是调试过程。对于各函数的详细设计,各函数的N—S图如下: (1)Createadj函数 (2)Dijkstra函数          (3)Search函数          (4)Menu函数          (5)main函数          2.4 程序编码   把详细设计的结果进一步求精为程序设计语言程序。同时加入一些注解和断言,使程序中逻辑概念清楚;编写过程中参考各种的教材和材料,使程序编写的正确性大有提高,同时也许到许多实践知识,增加了实践经验。 2.5 程序调试与测试    程序编写总是出现各种各样的错误,但是难点不是修改错误,而是找出错误。在大量的源程序中找出错误难度很大,但有了一定的方法,就能节省大量的时间,在这次课程设计中我运用的调试方法主要有2种:     一是借助调试工具。利用Turbo C中提供的程序专门调试工具Debugger程序,可以很容易找出程序中的各种语法错误。但是遇到一些逻辑错误时却又无从着手。这时我就用下面一种方法。     二是在程序中插入打印语句。猜测出大致的错误位置,选则一些主要变量,在关键部位插入打印语句,打印出这个主要变量,看其是否与理论上的一样,在多个位置插入,如果有个位置的值与理论值一样,另一个位置与理论值不一样,则错误就落在这两个位置之间,然后再多测试几个位置缩小范围,直到找出错误。  例如;我在调试main()主函数时,程序能够运行,三个选项都能选择,创建函数能够正常运行,也能正常退出,但在选第一条进入校园导航后,打印出来的列表却是空的,源程序中的初始化数据没有显示出来,我又尝试输入两个结点进行查找,发现没有输出路线,所以我猜测初始化数据没有被正常写入。但不知道为何没有被正常写入,首先怀疑是初始化时附值发生错误,查阅各种资料进行校验,发现没有错误。后来经过综合分析,发现最有可能是n值在search()函数中发生错误,于是我在search()函数中插入打印n 的语句,运行后发现输出的n为0,初始化数据中有11个结点,n应该为11,所以n 在这个地方发生错误,我又在main()主函数中打印出n 的值,n=11,是正确的。所以错误就在search()函数中,也就说是当运行case1,运行到search()函数时,n从11变为0,针对这个错误,我把变量n改为宏定义,因为n 是代表结点个数,不管在哪个函数中它的值都是一样的才对。改完后运行程序,成功! 本设计文件的注释如上,已给出详细的说明,下面仅以文件为单位说明各个函数的作用和特点,在必要的地方给予一定说明。  3.1、guide.h文件 使用条件编译。以下是头文件中语句 /********************************************* *头文件(.h) ********************************************/ #include "stdio.h" #include "conio.h" #include "alloc.h" #define n0 100 #define infi 32767 /*“无穷大*/ int adjmatrix[n0+1][n0+1];     /*邻接矩阵*/ int n=11; struct node             /*表结点*/ { char name[20];       /*下一个表结点与它们之间的距离*/ }place[12]={{"ShiDiGongYuan"},   /*表结点初始化,即写各景点名称*/    {"CangRongGongYu"},    {"YinHuiLou"},    {"TuoHuanGuanChang"},    {"DiBaShiTang"},    {"XiaoYiYuan"},    {"TuShuGuan"},    {"TiYuGuan"},    {"ZhongHuaGuanChang"},    {"ChuangXinLou"},    {"YiFuTuShuGuan"},    {"BoXueLou"}};  void createadj()          /*建立邻接表*/  void dijkstra( int x,int y)     /*dijkstra求最小生树*/  void search()            /*搜索最短路径*/  menu()               /*菜单函数*/          /********************************************* *建立邻接表 ********************************************/ void createadj()  

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值