JVM(和Spark)性能优化:使用Java Mission Control (7)

举报
大数据小粉 发表于 2016/11/11 14:27:27 2016/11/11
【摘要】 JVM(和Spark)性能优化:使用Java Mission Control

4 JVM和垃圾收集器

4.1 垃圾收集器

Java垃圾回收器是一种“自适应的、分代的、停止-复制、标记-清扫”式的垃圾回收器。在基于分代的内存回收策略中,堆空间通常都被划分为3个代,年轻代,年老代(或者tenured代-终身代),永生代。在年轻代中又被划分为三个小的区域,分别为:Eden(伊甸)区,S0区(survivor 0),S1区(survivor 1),如下图所示:

其中,新的对象总被分配到年轻代中,当年轻代空间被填满时,这时需要执行一次垃圾回收,即执行 minor GC,回收不再被引用的对象,并同时提升幸存的对象其年龄,年轻代中的幸存对象都有年龄标识字段,一旦其达到一定的阈值,则仍然幸存的对象将被提升到年老代空间中。

年老代的空间用于存放长时间幸存的对象,即生命周期较长的对象,一旦年轻代空间的幸存对象达到一定的年龄阈值后,将被自动提升到年老代,当年老代空间被对象填满(达到限额)时,这时执行一次Major GC。相较于minor GC, Major GC的执行次数要比minor GC要少很多,同时,Major Gc 执行的时间较Minor Gc要长。因为其涉及到更多的对象扫描。这种分代的思想,也是基于在实践中,对于新分配的对象具有更短的生命周期,年老的对象具有更长的生命周期所作出的较佳的选择。

与此同时,Minor Gc 和 Major Gc 在执行垃圾收集时,采取的是stop the world (STW) ,即终止正在运行的线程,等GC执行完毕在恢复所有的线程。

JVM在Old区申请不到内存,会进行Full GC。对于永生代的内存,主要是用来存放元数据的相关信息,类及其方法的信息。当一个类不再使用时将会被回收,当执行Full GC时,将会扫描永生代内存,对其进行垃圾回收。

4.1.1 Scavenge GC(年轻代内存分配失败触发)

采用复制收集器(Copying)。

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

4.1.2 Full GC(年老代内存分配失败触发)

采用标记-整理收集器(Mark-Compact)。

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

年老代(Tenured)被写满(达到限额)
永生代(Perm)被写满
System.gc()被显示调用
上一次GC之后,Heap的各域分配策略有变更
注:Minor GC=Young GC= Scavenge GC,Major GC≈Full GC,参见《Java性能优化权威指南》第4章和这个博客

Java G1 GC 是分区的(regionalized)、分代的,它把堆heap切分为多个大小相等的区块regions,这个区块的大小可以从1 MB到32 MB,但总数不会超过2048个区块。eden, survivor,和 old generations 是这些区块的逻辑集合logical sets且不是连续的。

看起来像这种样子:

下图是HotSpot的几种收集器(不含G1):

4.1.3 GC调优原则和实践

性能优化的三个指标:吞吐量、延迟、内存占用。

JVM垃圾收集三个基本原则:
Ø Minor GC最多原则
Ø GC内存最大化原则
Ø GC调优的3选2原则

Oracle官方建议,heap大于16GB,就用G1垃圾收集器!替换掉Parallel Collector(默认的)和CMS Collector的长时暂停“Stop The World”。【注:基本上确定了在Java 9中G1将是默认的GC了。】G1不要设置年轻代的大小-Xmn。

The Garbage First Garbage Collector (G1 GC) is the low-pause, server-style generational garbage collector

• G1 GC uses concurrent and parallel phases to achieve its target:pause time and to maintain good throughput

• When G1 GC determines that a garbage collection is necessary, it collects the regions with the least live data first (garbage first)

JVM的长时暂停应用程序,可能会导致网络ack超时或executor lost。可以尝试下如下的优化设置选项:


-XX:+UseG1GC
-XX:MaxGCPauseMillis=150~500
-XX:GCPauseIntervalMillis=200
-XX:ParallelGCThreads=8 + ((N - 8) * 5 / 8)
-XX:ConcGCThreads= {ParallelGCThreads} / 4
-XX:+ParallelRefProcEnabled
-XX:-ResizePLAB
-XX:+UnlockExperimentalVMOptions -XX:G1MaxNewSizePercent=75 -XX:G1NewSizePercent=3 //如果heap>100GB,则设为1

更多的G1细节参看官方文档:http://www.oracle.com/technetwork/articles/java/g1gc-1984535.html

注意,在Java 8中永生代PermGen已经被删除了,取而代之的是Metaspace用来保存类的元数据,它位于本地(native)内存,而不是堆上。PermSize 和 MaxPermSize选项相应也从JDK中删除了。但可以通过如下参数控制non-heap内存的最大值:


-XX:ReservedCodeCacheSize=100m
-XX:MaxMetaspaceSize=128m
-XX:CompressedClassSpaceSize=128m

Java 8中的G1,引入了一个强大的优化,字符串去重,因为String和它内部的char[]常常占用非常大的堆空间。G1就会识别出相同的字符串,并把指针指向同一个内部char[],从而避免相同字符串的多个副本在堆上。需要加上:


-XX:+UseStringDeduplication

Oracle官方也建议,当heap小于等于32GB时,也可以试试CMS垃圾收集器的优化配置:


-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseIncrementalMode
-XX:+UseIncrementalPacing
-XX:CMSInitiatingOccupancyFraction=70 //如果程序中维持了一个很大的长命对象的缓存,则可以加大此值为90
-XX:+CMSParallelRemarkEnabled
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:ParallelGCThreads=20

这些参数选项需要根据具体的业务不断的试验并测量,直到找到最合适的数值。观察GC中的年轻代、年老代的大小随GC的变化,适当调整试验不同代的大小。有人说,无测量无改进。有了JMC/JFR,就变得容易多了。

GC日志样例1 ,Minor GC :

GC日志样例2,Full GC:

当前Java 8的JVM有700多个Final参数(另外还有大量实验性的参数)。性能优化是多轮迭代的过程,需要人工反复收集大量的数据一轮一轮地进行。最好每次只优化一个方面。通过以下推荐的GC日志数据,可以得到许多的信息:

推荐选项 描述
-XX:+PrintGCDetails Print a detailed message after each GC
-XX:+PrintGCTimeStamps Print a time stamp relative to JVM start when GC occurs
-XX:+PrintGCDateStamps Print date/time stamp when GC occurs
-Xloggc: Force GC message to be logged to a file(例如/var/log/prs_gc.log)
-XX:+PrintReferenceGC Print times for special(weak, JNI, etc) reference processing during STW pause
-XX:+PrintJNIGCStalls Reports if GC is waiting fornative code to unpin object in memory
-XX:+PrintGCCause Add cause of GC in log
-XX:+PrintPromotionFailure Print additional information for promotion failure
-XX:+PrintTenuringDistribution Premature promotion information
-XX:+PrintGCApplicationStoppedTime 打印垃圾回收期间程序暂停的时间
-XX:+PrintGCApplicationConcurrentTime 打印每次垃圾回收前,程序未中断的执行时间
-XX:+PrintSafepointStatistics 垃圾收集的安全点或应用程序或系统中其它事件

Java堆大小的通用计算法则:

4.2 未来的JVM

Java 7和8中的JVM变化较大,功能越来越强。未来的JVM(可能在Java 9中)将在模块化上有突破性进展,模块化有几年曲折历史了,相关的JEP都有很多个。

【2015.07.29增加】最值得期待的是下一代Hotspot JVM,叫Graal(http://openjdk.java.net/projects/graal/ ),是动态的编译器,增加了一种高度可扩展的中间表示IR,可以做更多高级和低级优化:

基于此,Oracle还开发了个多语言的解释器框架Truffle(https://wiki.openjdk.java.net/display/Graal/Truffle+FAQ+and+Guidelines ),许多语言基于此速度将会更快,例如JRuby,Jython,FastR,Clojure等。基于Truffle实现新的语言将会更加容易。

还有个引人注目的是新一代GC,Shenandoah(http://openjdk.java.net/jeps/189 ): An Ultra-Low-Pause-Time Garbage Collector,超低暂停时间的GC。

当然,老牌项目Jigsaw将会在Java 9 中实现(http://openjdk.java.net/projects/jigsaw/),模块化带来的好处之一是性能优化。因为模块的导入导出包是明确的,JVM可以做很多Whole-Program Optimization Techniques(https://www.voxxed.com/blog/2015/07/the-features-project-jigsaw-brings-to-java-9/ )。

另外,还值得期待的有:
Value Objects 值对象,这样能方便地支持元组、记录、基本类型集合了。
http://openjdk.java.net/jeps/169
Project Sumatra: Java on GPU 在JVM级别支持GPGPU并行计算!
http://openjdk.java.net/projects/sumatra/
Project Panama: Native Interconnect for Java (JNI 2.0)
http://mail.openjdk.java.net/pipermail/discuss/2014-March/003306.html
JEP 197: Segmented Code Cache,把代码缓存根据不同的代码类型分为三个段,提高性能
http://openjdk.java.net/jeps/197

作者 | 孙奇辉

转载请注明出处:华为云博客 https://portal.hwclouds.com/blogs

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。