JVM优化

312 阅读11分钟

垃圾回收算法

标记清除(Mark Sweep)

  • 速度较快
  • 会产生内存碎片

image.png 标记整理(Mark Compact)

  • 速度慢
  • 没有内存碎片

image.png 复制(Copy)

不会有内存碎片 需要占用两倍内存空间

image.png

分代垃圾回收

image.png 新创建的对象首先分配在 eden 区 新生代空间不足时,触发 minor gc ,eden 区 和 from 区存活的对象使用 - copy 复制到 to 中,存活的对象年龄加一,然后交换 from to minor gc 会引发 stop the world,暂停其他线程,等垃圾回收结束后,恢复用户线程运行 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit) 当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full fc ,停止的时间更长!

相关 JVM 参数

含义参数
堆初始大小-Xms
堆最大大小--Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC-XX:+ScavengeBeforeFullGC

GC 分析


    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
    }

}

通过上面的代码,给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,使用前需要设置 jvm 参数。

垃圾回收器

相关概念:

  • 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
  • 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
  • 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。

串行

  • 单线程
  • 堆内存较少,适合个人电脑

image.png

-XX:+UseSerialGC=serial + serialOld

安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象 因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial 收集器

Serial 收集器是最基本的、发展历史最悠久的收集器 特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!

ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本

特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本

特点:同样是单线程收集器,采用标记-整理算法

吞吐量优先

多线程 堆内存较大,多核 cpu 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4

image.png

  1. -XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
  2. -XX:+UseAdaptiveSizePolicy
  3. -XX:GCTimeRatio=ratio // 1/(1+radio)
  4. -XX:MaxGCPauseMillis=ms // 200ms
  5. -XX:ParallelGCThreads=n

Parallel Scavenge 收集器

  • 与吞吐量关系密切,故也称为吞吐量优先收集器
  • 特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)

GC自适应调节策略:

Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。 当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、 晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。

Parallel Scavenge 收集器使用两个参数控制吞吐量:

  • XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)
  • XX:GCTimeRatio=rario 直接设置吞吐量的大小 Parallel Old 收集器 是 Parallel Scavenge 收集器的老年代版本 特点:多线程,采用标记-整理算法(老年代没有幸存区)

响应时间优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让 STW 的单次时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5 (STW Stop the World )指的是垃圾回收时的暂停时间

image.png

  1. -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
  2. -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
  3. -XX:CMSInitiatingOccupancyFraction=percent
  4. -XX:+CMSScavengeBeforeRemark

CMS 收集器

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器

特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务 CMS 收集器的运行过程分为下列4步:

  • 初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
  • 并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
  • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
  • 并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!

CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。

G1 收集器

定义: Garbage First

适用场景:

同时注重吞吐量和低延迟(响应时间) 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域 整体上是标记-整理算法,两个区域之间是复制算法 相关参数: JDK8 并不是默认开启的,所需要参数开启

  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=size
  • -XX:MaxGCPauseMillis=time

G1 垃圾回收阶段

image.png

  • Young Collection:对新生代垃圾收集
  • Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。
  • Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。

Young Collection

新生代存在 STW: 分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间!

  • E:eden
  • S:幸存区
  • O:老年代 新生代收集会产生 STW !

image.png Young Collection + CM

在 Young GC 时会进行 GC Root 的初始化标记 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的 JVM 参数决定 -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

image.png Mixed Collection 会对 E S O 进行全面的回收

  • 最终标记会 STW
  • 拷贝存活会 STW

-XX:MaxGCPauseMills=xxms 用于指定最长的停顿时间!

问:为什么有的老年代被拷贝了,有的没拷贝?

因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

image.png Full GC

G1 在老年代内存不足时(老年代所占内存超过阈值) 如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理 如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。

Young Collection 跨代引用

  • 新生代回收的跨代引用(老年代引用新生代)问题

image.png

  • 卡表 与 Remembered Set
  1. Remembered Set 存在于E中,用于保存新生代对象对应的脏卡
  2. 脏卡:O 被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过 post-write barried + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

image.png

Remark 重新标记阶段 在垃圾回收时,收集器处理对象的过程中

  • 黑色:已被处理,需要保留的
  • 灰色:正在处理中的
  • 白色:还未处理的

image.png 但是在并发标记过程中,有可能 A 被处理了以后未引用 C ,但该处理过程还未结束,在处理过程结束之前 A 引用了 C ,这时就会用到 remark 。 过程如下

  • 之前 C 未被引用,这时 A 引用了 C ,就会给 C 加一个写屏障,写屏障的指令会被执行,将 C 放入一个队列当中,并将 C 变为 处理中状态。
  • 在并发标记阶段结束以后,重新标记阶段会 STW ,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它,由灰色变成黑色。

image.png JDK 8u20 字符串去重

过程

  • 将所有新分配的字符串(底层是 char[] )放入一个队列
  • 当新生代回收时,G1 并发检查是否有重复的字符串
  • 如果字符串的值一样,就让他们引用同一个字符串对象
  • 注意,其与 String.intern() 的区别
  1. String.intern() 关注的是字符串对象
  2. 字符串去重关注的是 char[]
  3. 在 JVM 内部,使用了不同的字符串标

优点与缺点

  • 节省了大量内存
  • 新生代回收时间略微增加,导致略微多占用 CPU
  1. -XX:+UseStringDeduplication

JDK 8u40 并发标记类卸载

在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类

JDK 8u60 回收巨型对象

  • 一个对象大于region的一半时,就称为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

image.png

JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FulGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
  1. -XX:InitiatingHeapOccupancyPercent 用来设置初始值
  2. 进行数据采样并动态调整
  3. 总会添加一个安全的空挡空间

垃圾回收调优 查看虚拟机参数命令

D:\JavaJDK1.8\bin\java -XX:+PrintFlagsFinal -version | findstr "GC"

可以根据参数去查询具体的信息

调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io
  • gc

确定目标

低延迟/高吞吐量? 选择合适的GC

  • CMS G1 ZGC
  • ParallelGC
  • Zing 最快的 GC
  • 首先排除减少因为自身编写的代码而引发的内存问题
  • 查看 Full GC 前后的内存占用,考虑以下几个问题
  • 数据是不是太多?
  • resultSet = statement.executeQuery(“select * from 大表 limit n”)
  • 数据表示是否太臃肿
  • 对象图
  • 对象大小 16 Integer 24 int 4
  • 是否存在内存泄漏
  • static Map map …
  • 第三方缓存实现

新生代调优

  • 新生代的特点
  1. 所有的 new 操作分配内存都是非常廉价的
  2. TLAB thread-lcoal allocation buffer
  3. 死亡对象回收零代价
  4. 大部分对象用过即死(朝生夕死)
  5. Minor GC 所用时间远小于 Full GC

新生代内存越大越好么?

  • 不是
  • 新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降
  • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长
  • 新生代内存设置为内容纳[并发量*(请求-响应)]的数据为宜

  • 幸存区需要能够保存 当前活跃对象+需要晋升的对象

  • 晋升阈值配置得当,让长时间存活的对象尽快晋升

  1. -XX:MaxTenuringThreshold=threshold
  2. -XX:+PrintTenuringDistrubution

老年代调优

以 CMS 为例:

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经,否者先尝试调优新生代。
  • 观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
  1. -XX:CMSInitiatingOccupancyFraction=percent

例如

  1. Full GC 和 Minor GC 频繁
  2. 请求高峰期发生 Full GC,单次暂停时间特别长(CMS)
  3. 老年代充裕情况下,发生 Full GC(jdk1.7)