论如何调配jvm

一些要交代的

我的研究生主题大概可以用两个字概括,那就是搞spark。两个字可能不太严谨,因为读出来感觉就是搞斯帕克。其历经之路也可以高度总结一下: 安装配置spark -> 写常用的spark应用 -> 读spark源码 -> 优化spark。当然优化spark,并不一定要从内核入手,有很多种方案,比如写出优雅的应用程序,调配置参数,官方还给出了一个重要的优化方案,那就是jvm优化,毕竟spark这种分布式系统很多都跑在jvm上。附上spark团队给的优化向导地址,里面有一部分讲的就是垃圾回收策略的选择与调控。

好的,让我们来快速聊下jvm^_^
jvm的构成: jvm区域总体分两类,heap区和非heap区。heap区又分:Eden Space(伊甸园)、Survivor Space(幸存者区)、TenuredGen(老年代-养老区)。非heap区又分:Code Cache(代码缓存区)、Perm Gen(永久代)、Jvm Stack(java虚拟机栈)、Local Method Statck(本地方法栈)。

曾经看过一个举例很形象,大致解释了下jvm对象的生与死。
假设你是一个普通的Java对象,你出生在Eden区,在Eden区有许多和你差不多的小兄弟、小姐妹,可以把Eden区当成幼儿园,在这个幼儿园里大家玩了很长时间。Eden区不能无休止地放你们在里面,所以当年纪稍大,你就要被送到学校去上学,这里假设从小学到高中都称为 Survivor区。开始的时候你在Survivor区里面划分出来的的“From”区,读到高年级了,就进了Survivor区的“To”区,中间由于学习成绩不稳定,还经常来回折腾。(这里指发生了18次young GC)直到18岁的时候,高中毕业了,该去社会上闯闯了。于是你就去了年老代,年老代里面人也很多。在年老代里,你生活了20年每次GC加一岁),最后寿终正寝,被GC回收。有一点没有提,你在年老代遇到了一个同学,他的名字叫海绵宝宝,他以及他的海绵家族永远不会死,那么他们就生活在永生代。
这里解释下永生代,这里面放着jvm不会去主动回收的东西,如类模版对象,方法模版对象,还有在JDK7之前的HotSpot虚拟机中,字符串常量池的字符串被存储在永久代中,因此导致了一系列的性能问题和内存溢出错误。所以在java8中移除了永生代,迎来了元空间。

让年轻代大一点

较大的年轻代

由于Full GC的成本远远高于MinorGC,因此某些情况下需要尽可能将对象分配在年轻代,这在很多情况下是一个明智的选择。虽然在大部分情况下,JVM会先尝试在Eden区分配对象,但是由于空间紧张等问题,很可能不得不将部分年轻对象提前向年老代压缩。因此,在JVM参数调优时可以为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的情况发生。而且如果
JVM参数可以设置-XX:+PrintGCDetails -Xmx20M -Xms20M,这时候指jvm堆空间设置的初始内存为20M,最大也为20M。个人认为大部分情况下(比如你大概可以预估到你的程序所占内存有多大)初始堆内存和最大堆内存设置得相同较为合理一点,这样减少了jvm堆空间拓展的开销,因为如果虚拟机启动时设置使用的内存比较小,这个时候又需要初始化很多对象,虚拟机就必须重复地增加内存。分配足够大的年轻代空间,使用JVM参数-XX:+PrintGCDetails -Xmx20M -Xms20M-Xmn6M,年轻代大小为6M。

较大较合理的幸存者区使用率

在jvm年轻代中也可以做一些配置比例上的文章,比如,设置合理的Survivor区并且提供Survivor区的使用率,可以将年轻对象保存在年轻代。一般来说,Survivor区的空间不够,或者占用量达到50%时,就会使对象进入年老代,不管它的年龄有多大.SurvivorRatio也可以优化survivor的大小,不过这对于性能的影响不是很大。SurvivorRatio是eden和survior大小比例。参数是-XX:SurvivorRatio=8

让大对象进入老年代

进入老年代你需要多大size

我们在大部分情况下都会选择将对象分配在年轻代。但是,对于占用内存较多的大对象而言,这就会落入比较尴尬的境地。因为大对象出现在年轻代很可能扰乱年轻代GC,并破坏年轻代原有的对象结构。因为尝试在年轻代分配大对象,很可能导致空间不足,为了有足够的空间容纳大对象,JVM不得不将年轻代中的年轻对象挪到年老代。因为大对象占用空间多,所以可能需要移动大量小的年轻对象进入年老代,这对GC相当不利。基于以上原因,可以将大对象直接分配到年老代,保持年轻代对象结构的完整性,这样可以提高GC的效率。如果一个大对象同时又是一个短命的对象,假设这种情况出现很频繁,那对于GC来说会是一场灾难。原本应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路。因此,在软件开发过程中,应该尽可能避免使用短命的大对象,这是开发者应该走到的。可以使用参数-XX:PetenureSizeThreshold设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。如果需要将1MB以上的对象直接在年老代分配,设置-XX:PetenureSizeThreshold=1000000,这里的单位是字节。

进入老年代你需要多老

堆中的每一个对象都有自己的年龄。一般情况下,年轻对象存放在年轻代,年老对象存放在年老代。为了做到这点,虚拟机为每个对象都维护一个年龄。如果对象在Eden区,经过一次GC后依然存活,则被移动到Survivor区中,对象年龄加1。以后,如果对象每经过一次GC依然存活,则年龄再加1。当对象年龄达到阈值时,就移入年老代,成为老年对象。这个阈值的最大值可以通过参数-XX:MaxTenuringThreshold来设置,默认值是15。虽然-XX:MaxTenuringThreshold的值可能是15或者更大,但这不意味着新对象非要达到这个年龄才能进入年老代。事实上,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的(比如说年轻代空间不够了,需要将很多对象提前移入老年代),这个参数指定的是阈值年龄的最大值。即,实际晋升年老代年龄等于动态计算所得的年龄与-XX:MaxTenuringThreshold中较小的那个。
设置-XX:MaxTenuringThreshold=1指年轻对象经过一次GC后便进入老年代.

其他的一些技巧

降低GC时的STW

STW即stop the world,垃圾回收时会暂停所有应用线程来用于垃圾回收,首先考虑的是使用关注系统停顿的CMS回收器,因为并发清理这个阶段收集器线程和应用程序线程会并发执行,–XX:+UseConcMarkSweepGC年老代使用CMS收集器降低停顿;

减少full GC

应尽可能将对象预留在年轻代,因为年轻代Young GC的成本远远小于年老代的Full GC。
–XX:ParallelGCThreads=20设置 20 个线程进行垃圾回收;
–XX:+UseParNewGC年轻代使用并行回收器;
–XX:+SurvivorRatio:设置Eden区和Survivor区的比例为 8:1。稍大的Survivor空间可以提高在年轻代回收生命周期较短的对象的可能性,如果Survivor不够大,一些短命的对象可能直接进入年老代,这对系统来说是不利的。
–XX:MaxTenuringThreshold设置年轻对象晋升到年老代的年龄。默认值是15次,即对象经过15次Minor GC依然存活,则进入年老代。这里设置为31,目的是让对象尽可能地保存在年轻代区域。

最后附上一个介绍jvm参数配置大全的文章:文章地址