JVM 优化经验总结
2017-07-11 00:00
309 查看
开始之前
Java虚拟机有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。注意:本文仅针对JDK7、HotSPOTJava虚拟机,对于JDK8引入的JVM新特性及其他Java虚拟机,本文不予关注。
我们以一个例子开始这篇文章。假设你是一个普通的Java对象,你出生在Eden区,在Eden区有许多和你差不多的小兄弟、小姐妹,可以把Eden区当成幼儿园,在这个幼儿园里大家玩了很长时间。Eden区不能无休止地放你们在里面,所以当年纪稍大,你就要被送到学校去上学,这里假设从小学到高中都称为Survivor区。开始的时候你在Survivor区里面划分出来的的“From”区,读到高年级了,就进了Survivor区的“To”区,中间由于学习成绩不稳定,还经常来回折腾。直到你18岁的时候,高中毕业了,该去社会上闯闯了。于是你就去了年老代,年老代里面人也很多。在年老代里,你生活了20年(每次GC加一岁),最后寿终正寝,被GC回收。有一点没有提,你在年老代遇到了一个同学,他的名字叫爱德华(慕光之城里的帅哥吸血鬼),他以及他的家族永远不会死,那么他们就生活在永生代。
之前的文章
如何将新对象预留在年轻代
众所周知,由于FullGC的成本远远高于MinorGC,因此某些情况下需要尽可能将对象分配在年轻代,这在很多情况下是一个明智的选择。虽然在大部分情况下,JVM会尝试在Eden区分配对象,但是由于空间紧张等问题,很可能不得不将部分年轻对象提前向年老代压缩。因此,在JVM参数调优时可以为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的情况发生。清单1所示代码尝试分配4MB内存空间,观察一下它的内存使用情况。清单1.相同大小内存分配
1 2 3 4 5 6 7 8 9 |
清单2.清单1运行输出
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 36 37 38 |
清单3.增大Eden大小后清单1运行输出
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 36 37 38 39 |
清单4.不同大小内存分配
1 2 3 4 5 6 7 8 9 10 |
清单5.清单4运行输出
1 2 3 4 5 6 7 8 9 10 |
清单6.修改运行参数后清单4输出
1 2 3 4 5 6 7 8 9 10 11 |
清单7.再次修改运行参数后清单4输出
1 2 3 4 5 6 7 8 9 10 11 |
如何让大对象进入年老代
我们在大部分情况下都会选择将对象分配在年轻代。但是,对于占用内存较多的大对象而言,它的选择可能就不是这样的。因为大对象出现在年轻代很可能扰乱年轻代GC,并破坏年轻代原有的对象结构。因为尝试在年轻代分配大对象,很可能导致空间不足,为了有足够的空间容纳大对象,JVM不得不将年轻代中的年轻对象挪到年老代。因为大对象占用空间多,所以可能需要移动大量小的年轻对象进入年老代,这对GC相当不利。基于以上原因,可以将大对象直接分配到年老代,保持年轻代对象结构的完整性,这样可以提高GC的效率。如果一个大对象同时又是一个短命的对象,假设这种情况出现很频繁,那对于GC来说会是一场灾难。原本应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路。因此,在软件开发过程中,应该尽可能避免使用短命的大对象。可以使用参数-XX:PetenureSizeThreshold设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。参数-XX:PetenureSizeThreshold只对串行收集器和年轻代并行收集器有效,并行回收收集器不识别这个参数。清单8.创建一个大对象
1 2 3 4 5 6 |
清单9.清单8运行输出
1 2 3 4 5 6 7 8 9 10 11 |
清单10.修改运行参数后清单8输出
1 2 3 4 5 6 7 8 9 10 11 |
如何设置对象进入年老代的年龄
堆中的每一个对象都有自己的年龄。一般情况下,年轻对象存放在年轻代,年老对象存放在年老代。为了做到这点,虚拟机为每个对象都维护一个年龄。如果对象在Eden区,经过一次GC后依然存活,则被移动到Survivor区中,对象年龄加1。以后,如果对象每经过一次GC依然存活,则年龄再加1。当对象年龄达到阈值时,就移入年老代,成为老年对象。这个阈值的最大值可以通过参数-XX:MaxTenuringThreshold来设置,默认值是15。虽然-XX:MaxTenuringThreshold的值可能是15或者更大,但这不意味着新对象非要达到这个年龄才能进入年老代。事实上,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,这个参数指定的是阈值年龄的最大值。即,实际晋升年老代年龄等于动态计算所得的年龄与-XX:MaxTenuringThreshold中较小的那个。清单11所示代码为3个对象申请了若干内存。清单11.申请内存
1 2 3 4 5 6 7 8 9 10 |
运行清单11所示代码,输出如清单12所示。
清单12.清单11运行输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
清单13.修改运行参数后清单11输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
稳定的Java堆VS动荡的Java堆
一般来说,稳定的堆大小对垃圾回收是有利的。获得一个稳定的堆大小的方法是使-Xms和-Xmx的大小一致,即最大堆和最小堆(初始堆)一样。如果这样设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少GC的次数。因此,很多服务端应用都会将最大堆和最小堆设置为相同的数值。但是,一个不稳定的堆并非毫无用处。稳定的堆大小虽然可以减少GC次数,但同时也增加了每次GC的时间。让堆大小在一个区间中震荡,在系统不需要使用大内存时,压缩堆空间,使GC应对一个较小的堆,可以加快单次GC的速度。基于这样的考虑,JVM还提供了两个参数用于压缩和扩展堆空间。-XX:MinHeapFreeRatio参数用来设置堆空间最小空闲比例,默认值是40。当堆空间的空闲内存小于这个数值时,JVM便会扩展堆空间。
-XX:MaxHeapFreeRatio参数用来设置堆空间最大空闲比例,默认值是70。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆。
当-Xmx和-Xms相等时,-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio两个参数无效。
清单14.堆大小设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
清单15.修改运行参数后清单14输出
1 2 3 4 5 6 7 8 9 10 11 |
清单16.再次修改运行参数后清单14输出
1 2 3 4 5 6 7 8 9 10 11 12 |
增大吞吐量提升系统性能
吞吐量优先的方案将会尽可能减少系统执行垃圾回收的总时间,故可以考虑关注系统吞吐量的并行回收收集器。在拥有高性能的计算机上,进行吞吐量优先优化,可以使用参数:1 2 |
-Xss128k:减少线程栈的大小,这样可以使剩余的系统内存支持更多的线程;
-Xmn2g:设置年轻代区域大小为2GB;
–XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能地减少GC时间。
–XX:ParallelGC-Threads:设置用于垃圾回收的线程数,通常情况下,可以设置和CPU数量相等。但在CPU数量比较多的情况下,设置相对较小的数值也是合理的;
–XX:+UseParallelOldGC:设置年老代使用并行回收收集器。
尝试使用大的内存分页
CPU是通过寻址来访问内存的。32位CPU的寻址宽度是0~0xFFFFFFFF,计算后得到的大小是4G,也就是说可支持的物理内存最大是4G。但在实践过程中,碰到了这样的问题,程序需要使用4G内存,而可用物理内存小于4G,导致程序不得不降低内存占用。为了解决此类问题,现代CPU引入了MMU(MemoryManagementUnit内存管理单元)。MMU的核心思想是利用虚拟地址替代物理地址,即CPU寻址时使用虚址,由MMU负责将虚址映射为物理地址。MMU的引入,解决了对物理内存的限制,对程序来说,就像自己在使用4G内存一样。内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页(page)和页帧(pageframe),并保证页与页帧的大小相同。这种机制,从数据结构上,保证了访问内存的高效,并使OS能支持非连续性的内存分配。在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是大家耳熟能详的虚拟内存。在Solaris系统中,JVM可以支持LargePageSize的使用。使用大的内存分页可以增强CPU的内存寻址能力,从而提升系统的性能。
1 2 |
过大的内存分页会导致JVM在计算Heap内部分区(perm,new,old)内存占用比例时,会出现超出正常值的划分,最坏情况下某个区会多占用一个页的大小。
使用非占有的垃圾回收器
为降低应用软件的垃圾回收时的停顿,首先考虑的是使用关注系统停顿的CMS回收器,其次,为了减少FullGC次数,应尽可能将对象预留在年轻代,因为年轻代MinorGC的成本远远小于年老代的FullGC。1 2 3 |
–XX:+UseParNewGC:年轻代使用并行回收器;
–XX:+UseConcMarkSweepGC:年老代使用CMS收集器降低停顿;
–XX:+SurvivorRatio:设置Eden区和Survivor区的比例为8:1。稍大的Survivor空间可以提高在年轻代回收生命周期较短的对象的可能性,如果Survivor不够大,一些短命的对象可能直接进入年老代,这对系统来说是不利的。
–XX:TargetSurvivorRatio=90:设置Survivor区的可使用率。这里设置为90%,则允许90%的Survivor空间被使用。默认值是50%。故该设置提高了Survivor区的使用率。当存放的对象超过这个百分比,则对象会向年老代压缩。因此,这个选项更有助于将对象留在年轻代。
–XX:MaxTenuringThreshold:设置年轻对象晋升到年老代的年龄。默认值是15次,即对象经过15次MinorGC依然存活,则进入年老代。这里设置为31,目的是让对象尽可能地保存在年轻代区域。
结束语
通过本文的学习,读者了解了如何将新对象预留在年轻代、如何让大对象进入年老代、如何设置对象进入年老代的年龄、稳定的Java堆VS动荡的Java堆、增大吞吐量提升系统性能、尝试使用大的内存分页、使用非占有的垃圾回收器等主题,通过实例及对应输出解释的形式让读者对于JVM优化有一个初步认识。如其他文章相同的观点,没有哪一条优化是固定不变的,读者需要自己判断、实践后才能找到正确的道路。相关文章推荐
- JVM 优化经验总结
- JVM 优化经验总结
- JVM 优化经验总结
- JVM 优化经验总结
- JVM 优化经验总结
- JVM 优化经验总结(原文已发表在IBM开发者论坛)
- JVM 优化经验总结
- JVM 优化经验总结
- JVM 优化经验总结
- JVM 优化经验总结
- JVM 优化经验总结
- JVM 优化经验总结
- JVM 优化经验总结
- 34条SQL优化经验总结
- 网页制作代码经验总结:JS代码优化
- SQL Server数据库优化经验总结
- 代码优化经验总结(1)
- SqlSever2005 一千万条以上记录分页数据库优化经验总结【索引优化 + 代码优化】一周搞定
- SQL 优化经验总结34条
- SqlSever2005 一千万条以上记录分页数据库优化经验总结【索引优化 + 代码优化】一周搞定