目录

一、JVM Optimization

1、Shenandoah

Shenandoah的使用方法

2、ZGC

ZGC的版本更迭

ZGC的使用方法

ZGC的参数设置

3、JMH测试GC性能


一、JVM Optimization

1、Shenandoah

        Shenandoah是由Red Hat开发的一款低延迟的垃圾收集器,Shenandoah并发执行大部分GC工作,包括并发的整理,堆大小对STW的时间基本没有影响

Shenandoah的使用方法

1、下载:Shenandoah只包含在OpenJDK中,默认不包含在内,需要单独构建,可以直接下载构建好的。

Shenandoah下载地址

下载选择方式如下: 

1、{aarch64,arm32-hflt,mipsel,mips64el,ppc64le,s390x,x86_32,x86_64}:架构,linux中使用arch命令选择对应的架构。

[root@localhost ~]# arch
x86_64

2、{server,zero}:虚拟机类型,选择server,包含所有GC功能

3、{release,fastdebug,Slowdebug,optimization}:不同的优化级别,选择release,性能最高。

4、{gcc*glibc*,msvc*}:编译期版本,选择较高的版本性能好一些,如果兼容性有问题,(无法启动)选择较低的版本。--glibc版本选择

[root@localhost ~]# ldd --version
ldd (GNU libc) 2.17
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
[root@localhost ~]#

我这里选择的版本:openjdk-jdk-shenandoah-linux-x86_64-server-release-gcc10-glibc2.31.tar.xz

2、配置。将OpenJDK配置到环境变量中,使用java -version进行测试。打印出如下内容代表成功。

3、添加参数,运行Java程序

-XX:+UseShenandoahGC 开启Shenandoah GC

-Xlog:gc 打印GC日志

2、ZGC

ZGC文章

The Z Garbage Collector (ZGC) is a scalable low latency garbage collector. ZGC performs 
all expensive work concurrently, without stopping the execution of application threads 
for more than a millisecond. It is suitable for applications which require low latency.
Pause times are independent of the heap size that is being used. ZGC works well with 
heap sizes from a few hundred megabytes to 16TB. 

ZGC was initially introduced as an experimental feature in JDK 11, and was declared 
Production Ready in JDK 15. In JDK 21 was reimplemented to support generations.

Z垃圾收集器(ZGC)是一种可扩展的低延迟垃圾收集器。ZGC执行所有昂贵的工作都是并发的,而不会停止
应用程序线程的执行持续超过一毫秒。它适用于需要低延迟的应用程序。暂停时间与正在使用的堆大小无关。
ZGC与堆大小从几百兆字节到16TB。
ZGC最初是作为JDK 11中的一个实验特性引入的,并声明JDK 15中的生产就绪。JDK中重新实现了21,以支持几代人。

        ZGC(Z Garbage Collector)是一款基于Region内存布局的,暂时不设分代的,使用了读屏障、颜色指针等技术来实现可并发的,采用标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

        ZGC是一种可扩展的低延迟垃圾回收器。ZGC在垃圾回收过程中,STW的时间不会超过一毫秒,适合需要低延迟的应用。支持几百兆到16TB的堆大小,堆大小对STW的时间基本没有影响。

        ZGC降低了停顿时间,能降低接口的最大耗时,提升用户体验。但是吞吐量不佳,所以如果Java服务比较关注QPS(每秒的查询次数),那么G1是比较不错的选择。

内存布局

        跟G1类似,ZGC的堆内存也是基于Region来分布。不同的是,ZGC的Region支持动态地创建和销毁,并且Region的大小不是固定的,包括三种类型的Region:

Smail Region:2MB,主要用于放置小于256KB的小对象。

Medium Region:32MB,主要用于放置大于等于256KB小于4MB的对象。

Large Region:N * 2MB,这个类型的Region是可以动态变化的,不过必须是2MB的整数倍,最小支持4MB,每个Large Region只放置一个大对象,并且是不会被重分配的。

颜色指针

颜色指针,如下图所示,是ZGC的核心设计之一。以前的垃圾回收器的GC信息都保存在对象头中,而ZGC的GC信息保存在指针中。

JVM Optimization Learning(六)-LMLPHP

每个对象有一个64位指针,这64位被分为:

        18位:预留给以后使用。

        1位:Finalizable标识。

        1位:Remapped标识,是否进入了重分配集(即被移动过)。

        1位:Marked1标识,简称M1。

        1位:Marked0标识,和上面的Marked1都是用于辅助GC。简称M0。

        42位:对象的地址(所以它可以支持2^42=4T内存)。

垃圾收集过程

大概分为4个阶段:并发标记,并发预备重分配,并发重分配,并发重映射。

        并发标记(Concurrent Mark):与G1一样,并发标记是遍历对象,做可达性分析的阶段。前后也要经过类似G1的初始标记、最终标记的短暂停顿。与G1不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新颜色指针的M0/M1标志位。

        并发预备重分配(Concurrent Prepare for Relocate):这个阶段根据特定的查询条件统计出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocate Set)。重分配集与G1收集器的回收集(Collection Set)还是有区别的,ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的Region。

        并发重分配(Concurrent Relocate):该阶段是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于颜色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将被读屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC的这种行为称作指针的“自愈”能力。

        并发重映射(Concurrent Remap):该阶段所做的主要是修正整个堆中指向重分配集中旧对象的所有引用。但是ZGC的并发重映射并不是必须要“迫切”去完成的任务。因为即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。因此,ZGC很巧妙的将并发重映射阶段要做的工作,合并到了下一次GC中的并发标记阶段里去完成,反正它们都是要遍历对象,这样就节省了一次遍历的开销。一旦所有的指针被修正之后,原来记录新旧关系的转发表就可以释放掉了。

ZGC的版本更迭

        2018年JDK11,实验版本。2019年JDK12-13,并行类的卸载AArch64架构支持。

        2020年JDK14-15,windows & MacOS支持第一个正式版本。

        2021年JDK16-17,亚毫秒级最大暂停时间并行线程数的自动计算。

        2023年JDK21,支持分代年龄。

ZGC的使用方法

        OracleJDK和OpenJDK中都支持ZGC,阿里的DragonWell龙井JDK也支持ZGC但属于其自行对OpenJDK11的ZGC进行优化的版本。

        建议使用JDK17之后的版本,延迟较低同时无需手动配置并行线程数

分代ZGC添加参数启用:-XX:+UseZGC -XX:+ZGenerational

非分代ZGC通过命令行选项启用:-XX:+UseZGC

ZGC的参数设置

需要设置的参数:

-Xmx值最大堆内存大小

        这是ZGC最重要的一个参数,必须设置。ZGC在运行过程中会使用一部分内存来处理垃圾回收,所以尽量保证堆中有足够的空间。设置多少值取决于对象分配的速度,根据测试情况来决定。

可以设置的参数

-XX:SoftMaxHeapSize=值

        ZGC会尽量保持堆内存小于该值,这样在内存靠近这个值时会尽早地进行垃圾回收,但是依然有可能会超过该值。例如:-Xmx5g -XX:SoftMaxHeapSize=4g 这个参数设置,ZGC会尽量保证堆内存大小4GB,最多不会超过5GB。

3、JMH测试GC性能

pom

<properties>
    <java.version>21</java.version>
</properties>

<dependency>
   <groupId>org.openjdk.jmh</groupId>
   <artifactId>jmh-core</artifactId>
   <version>1.37</version>
</dependency>
<dependency>
   <groupId>org.openjdk.jmh</groupId>
   <artifactId>jmh-generator-annprocess</artifactId>
   <version>1.37</version>
</dependency>

测试代码:

package com.lwz;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

//执行5轮预热,每次持续2秒
@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
//输出毫秒单位
@OutputTimeUnit(TimeUnit.MILLISECONDS)
//统计方法执行的平均时间
@BenchmarkMode(Mode.AverageTime)
//java -jar xxx.jar -rf json
@State(Scope.Benchmark)
public class JavaGc {

    //每次测试对象大小4KB和4M
    @Param({"4", "4096"})
    int perSize;


    private void test(Blackhole blackhole) {

        //每次循环创建堆内存60%对象 JMX获取到Java运行中的实时数据
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
        //获取到剩余的堆内存大小
        long heapSize = (long) ((heapMemoryUsage.getMax() - heapMemoryUsage.getUsed()) * 0.6);
        //计算循环的次数
        long size = heapSize / (1024L * perSize);
        for (int i = 0; i < 4; i++) {
            List<byte[]> list = new ArrayList<>((int) size);
            for (int j = 0; j < size; j++) {
                list.add(new byte[1024 * perSize]);
            }
            blackhole.consume(list);
        }
    }

    @Benchmark
    @Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseSerialGC"})
    public void serialGc(Blackhole blackhole){
        test(blackhole);
    }

    @Benchmark
    @Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseParallelGC"})
    public void parallelGc(Blackhole blackhole){
        test(blackhole);
    }

    @Benchmark
    @Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g"})
    public void g1(Blackhole blackhole){
        test(blackhole);
    }

    @Benchmark
    @Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseShenandoahGC"})
    public void shenandoahGc(Blackhole blackhole){
        test(blackhole);
    }

    @Benchmark
    @Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseZGC"})
    public void zGc(Blackhole blackhole){
        test(blackhole);
    }

    @Benchmark
    @Fork(value = 1,jvmArgsAppend = {"-Xms4g","-Xmx4g","-XX:+UseZGC","-XX:+ZGenerational"})
    public void zGcGenerational(Blackhole blackhole){
        test(blackhole);
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(JavaGc.class.getSimpleName())
                .forks(1)
                .build();
        new Runner(opt).run();
    }
}

JVM Optimization Learning(五)

再小的努力,乘以365都很明显!
一个程序员最重要的能力是:写出高质量的代码!!
有道无术,术尚可求也,有术无道,止于术。
无论你是年轻还是年长,所有程序员都需要记住:时刻努力学习新技术,否则就会被时代抛弃!

12-10 14:11