当Java程序性能达不到目标,且代码上的优化手段都已经穷尽时,通常需要调整垃圾回收器和JVM内存空间配置来进一步提高性能,这就是JVM调优。

在调优的时候,我们必须要知道调的是什么,为什么要这样调,这些跟基础知识密不可分。

比如要知道内存结构、垃圾收集机制、算法、收集器,还要知道如何从GC日志中得到调试的效果,那么本系列文章将带你回顾调优用到的JVM基础知识。然后实战调优一把,最后我会总结出一个调优的套路,不论在实战还是面试中都能用上

JVM内存结构

按线程是否共享分为以下区域:

线程共享

  • 方法区: 存储已被虚拟机加载的类信息、方法信息、常量、静态变量、字节码、JIT编译后的本地代码,并使用永久代来实现方法区。1.8中用元空间替代了永久代,元空间并不在虚拟机中,而是使用本地内存,元空间中可能还存在短指针数据区CCS
  • 堆区: 最大的一块区域,用于存放对象的区域,1.7之后常量池移到这里

线程私有

  • 虚拟机栈: 每个方法执行时在其中创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息
  • 本地方法栈: 功能与虚拟机栈相同,为native方法服务
  • 程序计数器: 存放当前正在执行的指令的地址

堆上的内存内配

图上是的不同区域的比例是可以配置的

新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短

新生代内又分三个区:一个Eden区,两个Survivor区,大部分对象在Eden区中生成。每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor

老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,或者大对象直接进入老年代。该区域中对象存活率高。

因为堆是几乎所有对象创建、销毁的地方,所以堆就是我们要调优的重点,比如设置合适的新生代与老年代的大小、设置进入老年代的条件。

GC回收机制

回收哪些对象

满足以下两点的对象将被回收:

  1. 通过GC Roots作为起点的向下搜索形成引用链,没有搜到该对象,这是第一次标记。
  2. 在finalize方法中没有逃脱回收(将自身被其他对象引用),这是第一次标记的清理。

GC Roots:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。

不可达对象:通过一系列的GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时则此对象是不可用的。

垃圾回收算法

  • 复制算法:将内存划分成大小相等的两块,每次只使用其中一块,但一块用完了触发GC,可达性分析完成之后,将活的对象复制到另外一块,然后把原来的一块清除掉。但空间利用率低
  • 标记-清除:可达性分析完成之后,首先标记出所有需要回收的对象,标记完成后统一回收被标记的对象。但会产生大量碎片,导致无法分配大对象从而导致频繁GC
  • 标记-整理:可达性分析完成之后,首先标记出所有需要回收的对象,让所有存活的对象向一端移动。但比较耗时。

而主流虚拟机(Hotspot VM)中使用"分带垃圾回收",因为对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率,比如:

新生代因为每次GC都有大批对象死去,只需要付出少量存活对象的复制成本且无碎片所以使用“复制算法”,将可用内存按容量划分为Eden、from survivor、to survivor,分配的时候使用Eden和一个survivor,Minor GC后将存活的对象复制到另一个survivor,然后将原来已使用的内存一次清理掉

老年代因为存活率高、没有分配担保空间,所以使用“标记-清理”或者“标记-整理”算法

垃圾收集器

分类

按执行方式分类:

  • 串行:Serial、Serial Old
  • 并行(关注吞吐量):Parallel Scavenge、Parallel Old、ParNew
  • 并发(关注停顿时间):CMS、G1

按收集区域分类:

  • 新生代收集器:Serial、ParNew、Parallel Scavenge
  • 老年代收集器:Serial Old、Parallel Old、CMS
  • 整堆收集器:G1

并行与并发

这里的并行与并发与程序执行的并行与并发是不同的。

程序执行中:

  • 并行:系统能处理多个任务,且同时还能处理多个的任务处理机制
  • 并发:系统能处理多个任务,但同时只能处理一个的任务处理机制

垃圾收集器中:

  • 并行:用户线程处于等待状态,多条垃圾收集线程同时工作。适合计算、后台处理这样的弱交互场景。
  • 并发:用户线程与垃圾收集线程同时工作(但也会出现用户线程等待的情况)。适合Web。

吞吐量与停顿时间

吞吐量 = 应用程序线程用时占程序总用时的比例。JVM参数:-XX:GCTimeRatio=n,设置吞吐量大小,它的值是一个 0-100 之间的整数。系统将花费不超过 1/(1+n) 的时间用于垃圾收集。

停顿时间 = 垃圾收集时中断应用的时间。JVM参数:-XX:MaxGCPauseMillis

关系:吞吐量越高,停顿时间越低。同时也是衡量垃圾收集器好坏的参数。

串行收集器

串行收集器Serial是最古老的收集器,只使用一个线程去回收,可能会产生较长的停顿

新生代使用Serial收集器复制算法、老年代使用Serial Old标记-整理算法

参数:-XX:+UseSerialGC,默认开启-XX:+UseSerialOldGC

并行收集器

并行收集器Parallel关注可控的吞吐量,能精确地控制吞吐量与最大停顿时间是该收集器最大的特点,也是1.8的Server模式的默认收集器,使用多线程收集。ParNew垃圾收集器是Serial收集器的多线程版本。

新生代复制算法、老年代标记-整理算法

参数:-XX:+UseParallelGC,默认开启-XX:+UseParallelOldGC

并发收集器

并发收集器CMS是以最短停顿时间为目标的收集器。G1关注能精确控制停顿时间且垃圾回收效率高。

CMS针对老年代,有初始标记、并发标记、重新标记、并发清除四个过程,标记阶段会Stop The World,使用标记-清除算法,所以会产生内存碎片。

参数:-XX:+UseConcMarkSweepGC,默认开启-XX:+UseParNewGC

G1将堆划分为多个大小固定的独立区域,根据每次允许的收集时间优先回收垃圾最多的区域,使用标记-整理算法,是1.9的Server模式的默认收集器

参数:-XX:+UseG1GC

垃圾收集器的配合使用方式

如何选择垃圾收集器

  1. 需要停顿时间能超过1秒且想要一个可控的吞吐量时,使用并行收集器
  2. 如果停顿时间重要且不超过1秒,使用并发收集器
  3. 如果内存小于100M,使用串行或者JVM自己选
  4. 如果是单核,且没有系统停顿要求,使用串行或者JVM自己选

Stop The World

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互

STW总会发生,不管是新生代还是老年代,比如CMS在初始标记和重复标记阶段会停顿,G1在初始标记阶段也会停顿,所以并不是选择了一款停顿时间低的垃圾收集器就可以避免STW的,我们只能尽量去减少STW的时间。

那么为什么一定要STW?因为在定位堆中的对象时JVM会记录下对所有对象的引用,如果在定位对象过程中,有新的对象被分配或者刚记录下的对象突然变得无法访问,就会导致一些问题,比如部分对象无法被回收,更严重的是如果GC期间分配的一个GC Root对象引用了准备被回收的对象,那么该对象就会被错误地回收。

参考

https://www.cnblogs.com/ityouknow/p/5614961.html
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28
https://stackoverflow.com/questions/16695874/why-does-the-jvm-full-gc-need-to-stop-the-world
周志明,深入理解Java虚拟机[M],机械工业出版社,2013.

号外号外

最近在总结一些针对Java面试相关的知识点,感兴趣的朋友可以一起维护~
地址:https://github.com/xbox1994/2018-Java-Interview

Comments