wittyResry / myIssue

My issue mark down^_^ 欢迎吐槽,讨论~~
https://github.com/wittyResry/myIssue/issues
The Unlicense
5 stars 1 forks source link

JVM性能优化 #87

Open wittyResry opened 6 years ago

wittyResry commented 6 years ago

JVM性能优化(一)JVM技术入门

从Java代码到字节码

做为一个Java程序员,你可能对编码、编译和执行Java应用很熟悉。例子:我们假设你有一个程序(MyApp.java),现在你想让它运行。去执行这个程序你需要先用javac(JDK内置的静态Java语言到字节码编译器)编译。基于Java代码,javac生成相应的可执行字节码,并保存在相同名字的class文件:MyApp.class中。在把Java代码编译成字节码后,你可以通过java命令(通过命令行或startup脚本,使用不使用startup选项都可以)来启动可执行的class文件,从而运行你的应用。这样你的class被加载到运行时(意味着Java虚拟机的运行),程序开始执行。 编译成与系统无关的"字节码",由java虚拟机来执行。jvm使得java程序可以一次编译,处处运行。

什么是JVM?

JVM是一个软件模块,用于执行Java应用的字节码,并且把字节码转化到硬件,操作系统的指令。通过这样做,JVM允许Java程序在第一次编写后,不需要更改原始的代码,就能在不同的环境中执行。Java的可移植性是通往企业应用语言的关键:开发者并不需要为不同平台重写应用代码,因为JVM负责翻译和平台优化。

动态编译器,例如Just-In-Time (JIT)编译器,把一种语言动态的转化为另一种,这意味着它们在运行时执行代码。一个JIT编译器让你收集或创建运行数据分析(通过插入性能计数的方式实现),并且编译器使用这些环境数据快速的做出决定。动态的编译器在编译的过程中,实现更好的指令序列,把一系列的指令替换成更有效的,并消除多余的操作。随着时间的增长,你将收集更多的代码生成数据,做更多更好的编译决定;整个过程就是我们通常称为的代码优化和重编译。 动态编译给了你根据行为进行动态调整的优势,或随着应用装载次数的增加从而进行新的优化。这就是为什么动态编译器非常适合Java运行。值得注意的是,动态编译器请求外部数据结构,线程资源,CPU周期分析和优化。越深层次的优化,你将需要越多的资源。然而在大多数环境中,顶层对执行性能的提升帮助非常小——比你纯粹的解释要快5到10倍的性能。

垃圾回收

每一个线程基于每个“Java进程分配内存地址空间” 完成内存分配,叫Java堆,或简称堆。在Java世界中单线程分配在客户端应用程序中很常见。然而,单线程分配在企业应用和工作装载服务端变的没有任何益处,因为它并没有使用现在多核环境的并行优势。

优化分配减少碎片的方法,就是创造所谓的新生代,这是一个专门用于分配新对象的专用堆空间。剩余的堆会成为所谓的老年代。老年代是用来分配长时间存在的对象的,被假定会存在很长时间的对象包括不被垃圾收集的对象或者大对象。

垃圾收集器的一个挑战是在分配内存时,需要尽量不影响运行着的应用。如果你不尽量垃圾收集,你的应用将耗近内存;如果你收集的太频繁,你将损失吞吐量和响应时间,这将对运行的应用产生坏的影响。

最后值的一提的算法是compaction,这是管理内存碎片的方法。Compaction基本来说就是把对象移动到一起,从来释放更大的连续内存空间。如果你熟悉磁盘碎片和处理它的工具,你会发现compaction跟它很像,不同的是这个运行在Java堆内存中。 Ref: JVM performance optimization JVM性能优化(一)JVM技术入门

wittyResry commented 5 years ago

GC日志

-XX:+PrintGC [GC 246656K->243120K(376320K), 0.0929090 secs] [Full GC 243120K->241951K(629760K), 1.5589690 secs] 第一行的意思就是GC将已使用的堆空间从246656K减少到243120K,当前的堆容量(译者注:GC发生时)是376320K,GC持续的时间是0.0929090秒。 简单模式的GC日志格式是与GC算法无关的,日志也没有提供太多的信息。在上面的例子中,我们甚至无法从日志中判断是否GC将一些对象从young generation移到了old generation。所以详细模式的GC日志更有用一些。

-XX:PrintGCDetails [GC [PSYoungGen: 142816K->10752K(142848K)] 246648K->243136K(375296K), 0.0935090 secs ] 这是一次在young generation中的GC,它将已使用的堆空间从246648K减少到了243136K,用时0.0935090秒。既然我们已经知道了young generation的大小,所以很容易判定发生了GC,因为young generation无法分配更多的对象空间:已经使用了142848K中的142816K。我们可以进一步得出结论,多数从young generation移除的对象仍然在堆空间中,只是被移到了old generation:通过对比绿色的和蓝色的部分可以发现即使young generation几乎被完全清空(从142816K减少到10752K),但是所占用的堆空间仍然基本相同(从246648K到243136K)。 [Times: user=0.55 sys=0.10, real=0.09 secs] 详细日志的“Times”部分包含了GC所使用的CPU时间信息,分别为操作系统的用户空间和系统空间所使用的时间。同时,它显示了GC运行的“真实”时间(0.09秒是0.0929090秒的近似值)。如果CPU时间(译者注:0.55秒+0.10秒)明显多于”真实“时间(译者注:0.09秒),我们可以得出结论:GC使用了多线程运行。这样的话CPU时间就是所有GC线程所花费的CPU时间的总和。实际上我们的例子中的垃圾收集器使用了8个线程。

[Full GC [PSYoungGen: 10752K->9707K(142848K)] [ParOldGen: 232384K->232244K(485888K)] 243136K->241951K(628736K) [PSPermGen: 3162K->3161K(21504K)], 1.5265450 secs ] 除了关于young generation的详细信息,日志也提供了old generation和permanent generation的详细信息。对于这三个generations,一样也可以看到所使用的垃圾收集器、堆空间的大小、GC前后的堆使用情况。需要注意的是显示堆空间的大小等于young generation和old generation各自堆空间的和。以上面为例,堆空间总共占用了241951K,其中9707K在young generation,232244K在old generation。Full GC持续了大约1.53秒,用户空间的CPU执行时间为10.96秒,说明GC使用了多线程(和之前一样8个线程)。

对于Serial垃圾收集器,详细的GC日志和Throughput垃圾收集器是非常相似的。唯一的区别是不同的generation日志可能使用了不同的GC算法(例如:old generation的日志可能以Tenured开头,而不是ParOldGen)。使用垃圾收集器作为一行日志的开头可以方便我们从日志就判断出JVM的GC设置。

REF: https://blog.codecentric.de/en/2014/01/useful-jvm-flags-part-8-gc-logging/

wittyResry commented 5 years ago

CMS-低暂停时间

CMS收集器的过程

CMS收集器的GC周期由6个阶段组成。其中4个阶段(名字以Concurrent开始的)与实际的应用程序是并发执行的,而其他2个阶段需要暂停应用程序线程。

  1. 初始标记Initial Mark:为了收集应用程序的对象引用需要暂停应用程序线程,该阶段完成后,应用程序线程再次启动。(STW)
  2. 并发标记:从第一阶段收集到的对象引用开始,遍历所有其他的对象引用。
  3. 并发预清理:改变当运行第二阶段时,由应用程序线程产生的对象引用,以更新第二阶段的结果。
  4. 重标记Remark:由于第三阶段是并发的,对象引用可能会发生进一步改变。因此,应用程序线程会再一次被暂停以更新这些变化,并且在进行实际的清理之前确保一个正确的对象引用视图。这一阶段十分重要,因为必须避免收集到仍被引用的对象。(STW)
  5. 并发清理:所有不再被应用的对象将从堆里清除掉。
  6. 并发重置:收集器做一些收尾的工作,以便下一次GC周期能有一个干净的状态。

挑战

堆碎片 堆碎片是有可能的,CMS收集器并没有任何碎片整理的机制。即使总的堆大小远没有耗尽,但却不能分配对象——仅仅是因为没有足够连续的空间完全容纳对象。当这种事发生后,并发算法不会帮上任何忙,因此,万不得已JVM会触发Full GC。老年代将没有足够的可用空间来容纳一个从年轻代提升过来的对象。这种情况JVM会执行堆碎片整理:触发Full GC。

对象分配率高 如果获取对象实例的频率高于收集器清除堆里死对象的频率,并发算法将再次失败。

关于挑战的解决方法

当这些情形之一出现在实践中时(经常会出现在生产系统中),经常被证实是老年代有大量不必要的对象。一个可行的办法就是增加年轻代的堆大小,以防止年轻代短生命的对象提前进入老年代。另一个办法就似乎利用分析器,快照运行系统的堆转储,并且分析过度的对象分配,找出这些对象,最终减少这些对象的申请。

参数调整: XX:+UseConcMarkSweepGC 该标志首先是激活CMS收集器。默认HotSpot JVM使用的是并行收集器。

-XX:UseParNewGC 当使用CMS收集器时,该标志激活年轻代使用多线程并行执行垃圾回收。这令人很惊讶,我们不能简单在并行收集器中重用-XX:UserParNewGC标志,因为概念上年轻代用的算法是一样的。然而,对于CMS收集器,年轻代GC算法和老年代GC算法是不同的,因此年轻代GC有两种不同的实现,并且是两个不同的标志。

注意最新的JVM版本,当使用-XX:+UseConcMarkSweepGC时,-XX:UseParNewGC会自动开启。因此,如果年轻代的并行GC不想开启,可以通过设置-XX:-UseParNewGC来关掉。

-XX:+CMSConcurrentMTEnabled 当该标志被启用时,并发的CMS阶段将以多线程执行(因此,多个GC线程会与所有的应用程序线程并行工作)。该标志已经默认开启

-XX:CMSInitiatingOccupancyFraction=来设置,该值代表老年代堆空间的使用率。比如,value=75意味着第一次CMS垃圾收集会在老年代被占用75%时被触发。通常CMSInitiatingOccupancyFraction的默认值为68(之前很长时间的经历来决定的)。

REF

wittyResry commented 5 years ago

老年代垃圾收集-吞吐量收集器

HotSpot的并行面向吞吐量垃圾收集算法通常称为”吞吐量收集器”,因为它们旨在通过并行执行来提高吞吐量。从所谓的”GC根”(GC roots)开始,搜索堆中的可达对象并将其标记为活着的,之后,垃圾收集器将活着的对象移到年老代的一块无碎片(non-fragmented)内存块中,并标记剩余的内存空间是空闲的。 也就是说,我们不像复制策略那样移到一个不同的堆区域,像年轻代垃圾收集算法所做的那样。 相反地,我们把所有的对象放在一个堆区域中,从而对该堆区域进行碎片整理。

-XX:+UseSerialGC 我们使用该标志来激活串行垃圾收集器,例如单线程面向吞吐量垃圾收集器。 无论年轻代还是年老代都将只有一个线程执行垃圾收集。 该标志被推荐用于只有单个可用处理器核心的JVM。 在这种情况下,使用多个垃圾收集线程甚至会适得其反,因为这些线程将争用CPU资源,造成同步开销,却从未真正并行运行。

-XX:+UseParallelOldGC 当期望高吞吐量,并且JVM有两个或更多可用处理器核心时,我建议使用该标志。