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),还有一些其他在预览中的新功能,这些预览功能的使用需要在编译和启动Java应用时使用配置(--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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容