当前位置: 首页>后端>正文

JDK21 虚拟线程和世代机制的ZGC

JDK 21是在2023/09/19发布GA(General Availability)版本。 相较于前一版本,完成预览(Preview)正式产品化(Production Ready)重磅功能有虚拟线程(Virtual Thread),增强世代特性的Z垃圾回收机制(Generational Z Garbage Collection-本文会简写成GZGC),这两大特性将为Java应用和JVM提供极大的性能调优空间。一起发布其他功能还有弥补集合库缺陷的有序集合(Sequenced Collections)接口及其实现,记录模式(Record Patterns)及其Switch模式匹配(Pattern Matching for switch),和Key封装机制接口(Key Encapsulation Mechanism API),还有一些其他在预览中的新功能,这些预览功能的使用需要在编译时的特殊配置(--enable-preview)才能启用,这些预览功能将不是本文介绍的重点。本文章主要是介绍JDK21中正式产品化的功能-虚拟线程和GZGC。

虚拟线程(Virtual Threads)

Java 21 正式引入了虚拟线程(Virtual Thread)。 相对于操作系统线程(OS/Platform Thread),虚拟线程是轻量级的线程,主要在于它的切换成本极低并且可以轻易扩展(scale-up)到千万级别数量而不感阻塞。它是被JDK而非操作系统调度的用户态线程(lightweight fibers)。虚拟线程更适合于执行大部分时间阻塞(blocked)的任务,比如等待网络数据到来或等待一个元素在队列(Queue)中出现,以及读写文件处理数据等。

Java的虚拟线程是项目Project Loom的基于Fibers,Continuation和Tail Call等概念设计的,类似于C++/Go/Python 协程(Coroutine) 但感觉明显封装的要更好一些,它是设计目标是:

  • 使服务器端每次请启用线程(thread-per-request)类型的应用可以极致优化并接近硬件最佳使用率。
  • 用最小的改变让已存在的代码(java.lang.Thread API)容易采用虚拟线程,方便代码移植。
  • 易于查找问题,调试和用现有的JDK工具进行分析程序

启动虚拟线程的方法,和OS Thread的启动方法有些不同,比如没有Pooling(线程池)方法,以下是启动虚拟线程的方法。

Thread.ofVitual().start(runnable)
ThreadFactory factory = Thread.ofVirtual().name("worker", 0).factory();factory.newThread(runnable)
var executor = Executors.newVirtualThreadPerTaskExecutor();executor.submit(runnable)

以下代码是用虚拟线程实现典型的生产和消费类型的应用,使用Thread.ofVirtual().start启动线程。

  var queue = new SynchronousQueue<String>();
    Runnable producer =() -> {
          try {
            queue.put("is it done?");
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        };
    Runnable consumer =() -> {
          try {
            String takeElement = queue.take();
            System.out.println(takeElement);
          } catch (InterruptedException ex) {
            ex.printStackTrace();
          }
        };
    Thread producerThread = Thread.ofVirtual().start(producer);
    Thread consumerThread = Thread.ofVirtual().start(consumer);
    producerThread.join();
    consumerThread.join();

由于虚拟线程廉价并且可以数量众多,所以不需要像非虚拟线程那样用池化(pooling)的方式限制其数量。Java中非虚拟线程和操作系统线程是1:1对应的,所以是其珍贵资源,如果不限制其数量,过多的线程会过多消耗系统资源进而可能会瘫痪应用。所以一般会通过线程池的方式共享一定数量的线程。 比如通过 var executorService = Executors.newFixedThreadPool(10),在应用中共享10个线程。而虚拟线程就不需要这样的限制。 对于像每个请求对应一个线程的(thread per request type)这类的应用,可以直接启动一个虚拟线程完成其任务。如果用户真的想限制虚拟线程的数量,绝不要应用池化的方式,可以通过信号量(Semphore)等机制来实现,比如以下程序,限制实际运行的虚拟线程数量为2个,其他的只能等待信号量释放后才有机会执行。

   var semaphore = new Semaphore(2);
   Thread threadHandlers[] = new Thread[10];
   IntStream.range(0, 10)
       .forEach(i -> {threadHandlers[i] = Thread.ofVirtual().start(() -> {
                       try {
                         semaphore.acquire();
                         System.out.println(
                             "runing in the vitual thread platform ...doing nothing");} 
                       catch (InterruptedException e) {
                         e.printStackTrace();} 
                       finally {
                         semaphore.release();
                       }
                     });
           });
   for (int i = 0; i < 10; i++) {
     threadHandlers[i].join();
   }
 }

有世代机制的Z型垃圾回收(Generational Z Garbage Collection)

JDK21 发布的另外一个重大特性是对ZGC垃圾回收机制的增强,也就是Generational ZGC,为简单有时用简写GZGC。在ZGC的基础之上为年轻和老对象(Objects)维护不同的世代(generations)这样就可以回收那些处于年轻世代对象Objects,因为年轻世代的Objects更倾向于生命周期短暂。所以频繁回收这些年轻的对象及时释放其资源会对整个系统更有价值。这个也是为ZGC增加世代分区的设计理念。 应用程序运行在GZGC 虚拟机上,可以获取一下这些好处

  • 低风险的分配资源时停顿(stalls)。
  • 低的堆内存开销
  • 和低的回收时CPU开销

这些优点并不是以牺牲非世代ZGC吞吐量为代价的。非世代ZGC的本质的属性会被保留,这属性有:

  • JVM的停顿时间不能大于1ms
  • 堆的大小可以从几百Mbytes到Terabytes都应当被支持
  • 最小手动配置(configuration auto-tuning)

对于最小配置这点,比如以下的几点不需要手动配置,完全由系统自动调整(Auto-tuning)

  • 世代的大小
  • 垃圾回收时使用的线程数量
  • 在年轻世代中(Young Generation),对象的生存时间等

这种世代ZGC在大多数情况下应该是优于非世代ZGC,最终为了减少长期维护成本,GZGC会取代ZGC成为默认的JVM GC垃圾回收机制。ZGC的设计目标是低延迟和高可扩展能力,它在JDK15时已经产品可用。ZGC大部分的工作是在应用线程运行的情况下完成的,仅仅在必要的时候暂停这些应用线程,但暂停时间极短。ZGC的暂停时间基本用微妙(microseconds)测量,而默认的G1暂停的时间从毫秒到秒不等。ZGC的低暂停时间独立于堆的大小,堆的大小从几百Mbytes到Terabytes,但暂停时间基本保持一致-低暂停。对多种的工作负荷,简单应用ZGC基本可以解决大部分的垃圾回收的延迟问题。只要拥有足够的资源(比如,内存和CPU),ZGC就能回收内存的速度比并行运行应用线程消耗内存的速度快。

然而,ZGC是保存所有的对象(Objects)在一起,没有任何区分,因此它必须每次扫描所有的内存中的对象。而GZGC则根据弱世代假设理论(Weak Generational Hypothesis)将这些JVM管理的内存中的对象区分成年轻世代和老世代。年轻世代对象(Young Objects)倾向于生命期短暂(Die Young),而老世代对象(Old Objects)倾向生命期长些(Stick around)。其实可以理解,举个例子,比如Web应用,有些对象需要和整个web应用生存的时间一样长比如Applicaton对象和一些静态对象等,但那些临时为满足Request请求产生的session对象,完成当前请求的服务后,就应该被立即释放掉以节省内存资源。这些session对象被频繁创建释放, 回收年轻世代对象需要的资源少但可以释放出多的内存,而回收老世代的对象则需要多的资源但释放内存较少。因此通过频繁回收年轻世代对象可以显著给系统带来性能的提升。

ZGC和GZGC的配置

可以用-XX:+UseZGC 配置使用ZGC垃圾回收机制,也就是非世代功能的ZGC,而用-XX:+ZGenerational启用世代功能的GZGC。

$ java -XX:+UseZGC -XX:+ZGenerational...

按照官网所说,GZGC在未来会是默认选项。待时ZGenerational会被废弃掉。

总结

JDK21正式产品化的这两个特性虚拟线程和GZGC,会给应用系统进一步优化提供更多空间,极大改善基于JVM应用的性能。 并且按照官网所说,这个版本将是LTS版本,也就是长时间支持(Long-Term-Support)版本,所以相信会有更多商家会把系统向这个版本迁移已改善应用性能。SpringBoot3.0以上的版本已经开始支持JDK21.

参考资料

  1. Generational Garbage Collection
  2. JEP 439: Generational ZGC
  3. JDK 21
  4. Virutal Thread

https://www.xamrdz.com/backend/3ck1938346.html

相关文章: