目录
-
一 垃圾回收算法梳理
1. 如何计算对象已死
1.1 引用计数器算法
引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。
引用计数器实现简单,效率高;但是不能解决循环引用问问题(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1之后,这个算法已经不再使用了。
1.2 可达性分析算法
可达性分析算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain),当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的,如下图所示。
GC Roots对象包括:
-
虚拟机栈(栈帧中的本地变量表)中的引用的对象。
-
方法区域中的类静态属性引用的对象。
-
方法区域中常量引用的对象。
-
本地方法栈中JNI(Native方法)的引用的对象。
上面只是标记了对象是否可以被回收,实际上在java中首先会标记下对象,会调用对象里面的
protected void finalize()
这个方法,这个时候对象还有救,只要在这个方法把该对象和引用链对接上,其实可以逃脱被回收
2. 回收的区域
- 新生代
- 永久代
新生代,这个很容易理解,一般来说永久带是最不好回收的。永久代主要回收以下部分内容:
- 废弃常量
- 无用的类
3. 垃圾回收算法
3.1 标记—清除算法
标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。
标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。如下图所示:
3.2 复制算法
复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。这种方式听上去确实是非常不错的方案,但是总的来说对内存的消耗十分高。
复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是1:1(大概是8:1),也就是常提到的一块Eden(80%)和两块Survivor(20%)。当然也会存在10%不够用的情况,这个后面在进行梳理,会有一个补偿机制,也就是分配担保
3.3 标记—整理算法
复制收集算法会存在一种极端情况,就是对象都没死。这种情况会在老年代有几率的出现,所以根据老年代的特点提出了标记—整理算法
。 标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存,如下图所示:
3.4 分代收集
分代收集是根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用标记—复制算法,老年代采用标记—整理算法。
垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。上面介绍的只不过是基本思想。
-
二 垃圾收集器
上面是目前比较常用的垃圾收集器,和他们直接搭配使用的情况,上面是新生代收集器,下面则是老年代收集器,这些收集齐都有自己的特点,根据不同的业务场景进行搭配使用。
收集器
1.1 Serial收集器
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)也就是传说中的Stop The World
。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
1.2 ParNew收集器
ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。 使用方式可以使用-XX:+UseConcMarkSweepGC
,或者是使用-XX:+UseParNewGC
来强制开启,可以通过-XX:ParallelGCThreads
来调整或者限制垃圾收集的线程数量。
1.3 Parallel Scavenge收集器
Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
Parallel Scavenge提供了两个参数用来精确控制,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis
参数以及直接设置吞吐量大小的-XX:GCTimeRatio
参数
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间
Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy
值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)
等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)
1.4 Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中详细讲解。
1.5 Parallel Old 收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(还记得上面说过Parallel Scavenge收集器无法与CMS收集器配合工作吗?)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
1.5CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:
-
初始标记(CMS initial mark)
-
并发标记(CMS concurrent mark)
-
重新标记(CMS remark)
-
并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”
失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction
的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”
失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction
设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
CMS是一款基于“标记—清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection
开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction
,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。
1.6 G1收集器
G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点。
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
2 总结
各个收集器都有自己适应的业务场景,一般来说在生产中都会使用ParNew+CMS
的方式分代收集回收。针对于新生代来说,根据其特性,总的来说还是使用复制算法。而针对于老年代来说算法一般会使用两种,标记-清除和标记-整理,每一种都有自己适用的场景。但是在目前我遇到的生产中还是使用CMS 标记清除的这种比较多,也是比较常用的,虽然标记-清除这种算法会导致大量的内存碎片,但是也是可以通过一种暴力的方式解决,就是FULL GC,G1收集器是比较完美的一种收集器,但是在目前的生产中依旧用得很少。
-
三 jvm参数详解
内存相关
选项 | 参数详解 | 默认值 |
---|---|---|
-Xms | 初始堆大小 | -- |
-Xmx | 最大堆大小 | -- |
-Xmn | 年轻代大小(1.4or lator)整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 | -- |
-XX:newSize | 表示新生代初始内存的大小,应该小于 -Xms的值 | -- |
-XX:NewRatio | 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 | -- |
-XX:MaxNewSize | 年轻代最大值(for 1.3/1.4) | -- |
-XX:PermSize | 设置持久代(perm gen)初始值 | -- |
-XX:MaxPermSize | 设置持久代最大值 | -- |
-Xss | 每个线程的堆栈大小 | -- |
-XX:ThreadStackSize | -- | -- |
-XX:SurvivorRatio | Eden区与Survivor区的大小比值, 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 | -- |
-XX:LargePageSizeInBytes | 内存页的大小不可设置过大, 会影响Perm的大小,基本没用过 | -- |
-XX:+UseFastAccessorMethods | 原始类型的快速优化 1.7以后不建议使用,1.6之前默认打开的 | -- |
-XX:+UseFastEmptyMethods | 优化空方法,1.7以后不建议使用,1.6之前默认打开的 | -- |
-XX:+DisableExplicitGC | 关闭System.gc() | -- |
-XX:MaxTenuringThreshold | 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率 | -- |
-XX:+AggressiveOpts | 加快编译 | -- |
-XX:+UseBiasedLocking | 锁机制的性能改善, 有偏见的锁是使得锁更偏爱上次使用到它线程。在非竞争锁的场景下,即只有一个线程会锁定对象,可以实现近乎无锁的开销。 | 默认开启 |
-Xnoclassgc | 禁用类垃圾回收 | -- |
-XX:SoftRefLRUPolicyMSPerMB | 每兆堆空闲空间中SoftReference的存活时间 | 默认是1S |
-XX:PretenureSizeThreshold | 对象超过多大是直接在旧生代分配,单位字节 新生代采用Parallel Scavenge GC时无效另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象. | -- |
-XX:+CollectGen0First | FullGC时是否先YGC | false |
收集器相关
选项 | 参数详解 | 默认值 |
---|---|---|
-XX:+UseParallelGC | 选择垃圾收集器为并行收集器。此配置仅对年轻代有效。可以同时并行多个垃圾收集线程,但此时用户线程必须停止。 | -- |
-XX:+UseParNewGC | 设置年轻代收集器ParNew | -- |
-XX:ParallelGCThreads | Parallel并行收集器的线程数 | -- |
-XX:+UseParallelOldGC | 设置老年代的并行收集器是ParallelOld | -- |
-XX:+UseG1GC | 使用G1收集器 | -- |
-XX:MaxGCPauseMillis | 每次年轻代垃圾回收的最长时间(最大暂停时间) | -- |
-XX:+UseAdaptiveSizePolicy | 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开. | -- |
-XX:GCTimeRatio | 设置垃圾回收时间占程序运行时间的,百分比公式为1/(1+n) | -- |
-XX:+ScavengeBeforeFullGC | Full GC前调用YGC | true |
-XX:+UseConcMarkSweepGC | 使用CMS内存收集 | -- |
-XX:+AggressiveHeap | 试图是使用大量的物理内存长时间大内存使用的优化,能检查计算资源(内存, 处理器数量)至少需要256MB内存大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升) | -- |
-XX:CMSFullGCsBeforeCompaction | 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理 | -- |
-XX:+CMSParallelRemarkEnabled | 降低CMS标记停顿 | -- |
-XX+UseCMSCompactAtFullCollection | 在FULL GC的时候, 对年老代的压缩,CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片 | -- |
-XX:+UseCMSInitiatingOccupancyOnly | 使用手动定义初始化定义开始CMS收集,禁止hostspot自行触发CMS GC | -- |
-XX:CMSInitiatingOccupancyFraction=70 | 使用cms作为垃圾回收使用70%后开始CMS收集 | -- |
-XX:CMSInitiatingPermOccupancyFraction | 设置Perm Gen使用到达多少比率时触发 | -- |
-XX:+CMSIncrementalMode | 设置为增量模式 | -- |
-XX:CMSTriggerRatio | CMSInitiatingOccupancyFraction = (100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100) 处罚cms收集的比例 | -- |
-XX:MinHeapFreeRatio | java堆中空闲量占的最小比例 | -- |
-XX:+CMSClassUnloadingEnabled | 如果你启用了CMSClassUnloadingEnabled ,垃圾回收会清理持久代,移除不再使用的classes。这个参数只有在 UseConcMarkSweepGC 也启用的情况下才有用。参数如下: | -- |
辅助信息
选项 | 参数详解 | 默认值 |
---|---|---|
-XX:+PrintGC | 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]Full GC 121376K->10414K(130112K), 0.0650971 secs] | -- |
-XX:+PrintGCDetails | -- | -- |
-XX:+PrintGCTimeStamps | -- | -- |
-XX:+PrintGC:PrintGCTimeStamps | -- | -- |
-XX:+PrintGCApplicationStoppedTime | 打印垃圾回收期间程序暂停的时间.可与上面混合使用 | -- |
-XX:+PrintGCApplicationConcurrentTime | 打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用 | -- |
-XX:+PrintHeapAtGC | 打印GC前后的详细堆栈信息 | -- |
-Xloggc:filename | 把相关日志信息记录到文件以便分析.与上面几个配合使用 | -- |
-XX:+PrintClassHistogram | 遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同 | -- |
-XX:+PrintTenuringDistribution | 查看每次minor GC后新的存活周期的阈值 | -- |
-XX:PrintHeapAtGC | 打印GC前后的详细堆栈信息 | -- |
-- | -- | -- |
-
四 jvm调优
1. 常见问题
1.1 内存泄漏
内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小
,目前来说,常遇到的泄漏问题如下:
年老代堆空间被占满
年老代堆空间被占满
异常: java.lang.OutOfMemoryError: Java heap space
这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。这种情况一般来说是因为内存泄漏或者内存不足造成的。某些情况因为长期的无法释放对象,运行时间长了以后导致对象数量增多,从而导致的内存泄漏。另外一种就是因为系统的原因,大并发加上大对象,Survivor Space
区域内存不够,大量的对象进入到了老年代,然而老年代的内存也不足时,从而产生了Full GC,但是这个时候Full GC也无发回收。这个时候就会产生java.lang.OutOfMemoryError: Java heap space
解决方案如下:
- 代码内的内存泄漏可以通过一些分析工具进行分析,然后找出泄漏点进行改善。
- 第二种原因导致的OutOfMemoryError可以通过,优化代码和增加
Survivor Space
等方式去优化。
持久代被占满
持久代被占满
异常:java.lang.OutOfMemoryError: PermGen space
Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。 解决方案:
- 增加持久代的空间 -XX:MaxPermSize=100M。
- 如果有自定义类加载的需要排查下自己的代码问题。
堆栈溢出
堆栈溢出
异常:java.lang.StackOverflowError
一般就是递归没返回,或者循环调用造成
线程堆栈满
线程堆栈满
异常:Fatal: Stack size too small
java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。 解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
系统内存被占满
系统内存被占满
异常:java.lang.OutOfMemoryError: unable to create new native thread
这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。 分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。 解决:
1. 重新设计系统减少线程数量。
2. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。
2 优化方法
2.1 优化目标
优化jvm其实是为了满足高吞吐,低延迟需求来优化GC,之前遇到的情况中,其实不优化GC也是可以正常于行的,只不过偶尔会因为高并发给压垮,但是也可以通过其他方式来解决这个问题。
2.2 优化GC步骤
- 首先需要观察目前垃圾回收的情况,分析出老年代和年轻代回收的情况,适当的去调整内存大小和-XX:SurvivorRatio的比例。
- 根据垃圾收集器的特性,选择适合自己业务的垃圾收集器,一般来说现在的WEB服务都是CMS+ParNew收集器。根据CMS收集器一般来说就会产生大量碎片,根据自己的需求悬着相应的压缩频率即可。
- 不断的调整jvm内存比例,老年代、年轻代、以及持久代的比例,直到测试出一个比较满意的值。
2.3 优化总结
总的来说GC优化不仅仅是加大内存可以解决的。需要综合下业务特征和GC的时间,减少新生代大小可以缩短新生代GC停顿时间,因为这样被复制到survivor区域或者被提升的数据更少,但是这样一来yangGC的频率就会很高,而且会有更多的垃圾进入到了老年代。如果增加新生代大小又会导致回收的时间和复制的时间变高,所以一般来说需要在这个中间进行折中。
像是针对大部分数据都在Eden区域被回收,并且几乎没有对象在survivor区域死亡的场景,完全可以减少-XX:MaxTenuringThreshold
这个参数,让数据提早进入Old,减少survivor区域的复制,来提高效率。
3.案例分析
3.1 案例1 Intellij IDEA 2016优化
公司系统参数
-server
-Xms2g
-Xmx2g
-Xmn768m
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:CMSInitiatingOccupancyFraction=60
-XX:CMSTriggerRatio=70
-Xloggc:/data/bpm.coffee.session/logs/gc_20160704_110306.log
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/bpm.coffee.session/tmp/heapdump_20160704_110306.hprof
网上给的配置参数
-server
-Xms6000M
-Xmx6000M
-Xmn500M
-XX:PermSize=500M
-XX:MaxPermSize=500M
-XX:SurvivorRatio=65536
-XX:MaxTenuringThreshold=0
-Xnoclassgc
-XX:+DisableExplicitGC
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSClassUnloadingEnabled
-XX:-CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=90
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log
Intellij IDEA 2016 默认参数
-Xms158m
-Xmx750m
-XX:MaxPermSize=350m
-XX:ReservedCodeCacheSize=240m
-XX:+UseCompressedOops
Intellij IDEA 2016
情景分析:
- -Xms2g -Xmx2g 这两个主要是设置初始堆大小和最大堆的大小,对比目前
公司系统
,网上参数
和Intellij IDEA
默认参数,总的来说在于场景问题,-Xms初始堆针对于服务器端的来说一般会设置跟最大堆一样大,为什么呢?因为对于服务器来说启动时间并不是最重要的,如果不够了再去申请,这个对于大并发的场景来说是不合适的。而针对于客户端Intellij IDEA
来说,重点是启动时间,所以初始堆设置比较小,这样启动的时间比较快。 - -server 这个参数主要是用来指定运行模式的,指定了-server那么启动的速度就会慢一些,所以针对于IDEA这种编辑器来说一般都使用默认的client模式
- -XX:+UseCompressedOops 使用compressed pointers。这个参数默认在64bit的环境下默认启动,但是如果JVM的内存达到32G后,这个参数就会默认为不启动,因为32G内存后,压缩就没有多大必要了,要管理那么大的内存指针也需要很大的宽度了
- -XX:ReservedCodeCacheSize=240m 这个主要是用来编译代码的,针对server来说用处不是很大.
- 针对于服务端来说
-XX:+UseConcMarkSweepGC
老年代基本都是使用cms收集器,而年轻代都是使用-XX:+UseParNewGC
进行收集的。 -XX:CMSInitiatingOccupancyFraction=60
这个参数主要是告诉cms收集器,内存到60%的时候进行回收,这个比例主要还是针对系统业务来决定的,太低容易内存溢出,太高容易内存浪费,而且老年代垃圾回收频率高。一般来说都是在70到90之间,60过低了。- -Xloggc 这个参数一般都会打开,能够很好的排查jvm的内存溢出.
- -Xnoclassgc 这个针对一般业务来说都会打开,不让class进行gc.
- -XX:+DisableExplicitGC web业务来说,由于大家开发水平不一致,难保有人会使用System.gc(),高并发的场景这个肯定会是一个很大的影响,所以一般都关掉
- -XX+UseCMSCompactAtFullCollection消除cms碎片
- -XX:CMSFullGCsBeforeCompaction 这个上面填的0,如果没有特殊场景,最好设置成1或者2.
- -XX:-CMSParallelRemarkEnabled 主要用来降低cms标记的停顿,但是这个参数会增加碎片化,
- SoftRefLRUPolicyMSPerMB 这个参数指定了软饮用停留的时间,一般来说设置为0就好了,感觉生产中这个参数影响并不大,如果是针对单机内存使用缓存比较多的场景,这个值可以设置到1s左右,增加加吞吐量。
总结
jvm的调优需要根据自己的业务场景进行调整,没有万能的参数,但是总的来说,在生产环境中都会打开这几个必须的参数
verbose: gc
Xloggc:<pathtofile>
XX:+PrintGCDetails
XX:+PrintTenuringDistribution