JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

JDK21虚拟线程对gc的影响 java虚拟机设置线程堆栈大小

wys521 2024-10-23 15:49:54 精选教程 32 ℃ 0 评论

JDK21可能是java世界近10年(也可能是20年)最重要的更新,虚拟线程的出现让各类java的开发模式都会发生变化,几乎接近一种新语言。

本文讨论在虚拟线程的原理以及其对gc的影响。

虚拟线程

先测试一下:

Thread.ofVirtual().start(() -> {
  System.out.println("virtual");
}).join();

如果在空项目中测试,一定记得join


日常代码中怎么创建虚拟线程

Thread.ofVirtual().start(() -> System.out.println("virtual"));

像golang使用go一样,切记不要像过去一样使用线程池,虚拟线程并不需要复用。


虚拟线程执行在什么真实线程中?

在java.lang.VirtualThread代码中可以看到,最终还是

return new ForkJoinPool(parallelism, factory, handler, asyncMode,
                         0, maxPoolSize, minRunnable, pool -> true, 30, SECONDS);

因此在jvm层面只能看到几个ForkJoinPool,数量由Runtime.getRuntime().availableProcessors()或-Djdk.virtualThreadScheduler.parallelism决定


虚拟线程的本质是什么?

在同个线程中顺序的执行代码,当遇到io密集任务时,交出控制权到其他代码中,因此本质上来说:

  1. 看起来像是并发,体验也是并发,实际并不是(在同个进程中时)
  2. 对cpu密集型运算没有任何帮助,对于对业务开发更常见的io密集型有很大帮助
  3. 因为在同个线程中,没有对栈内存的使用,都在使用堆内存,因此对gc有更大的压力
  4. 极大的降低了开发及debug成本


虚拟线程+gc

虚拟线程把其栈放在堆中,不像普通线程放在栈中。大量创建线程会产生cpu压力,而大量创建虚拟线程会产生gc压力,虽然每个虚拟线程实际上只有一个java.lang.Thread对象+部分信息的大小(几百bytes)。

测试方法:

使用更偏向io密集的方式进行测试,1000并发在堆内创建大量的对象。只有nio比较能接近虚拟线程的场景,1000个普通线程在真实场景中几乎不可能出现。

因此使用1000个mono循环调用自身 vs 1000个虚拟线程while(true)死循环运行,两者背后都是真实的10个线程,http get请求一个1M的文件,放在本机的nginx下。1m的String对象在大并发的情况下会给gc造成很大压力,以此来测试gc性能。

非常简单的虚拟线程代码:

IntStream.range(0, 1000).forEach(j -> Thread.ofVirtual().start(() -> {
    while(true) {
        http.Do(j);
    }
}));

比较晦涩的reactor代码:

public void run(String... args) throws Exception {
        IntStream.range(0, 1000).forEach(i -> Do());
    }

    void Do(){
        webClient.get().uri("/1m.json").retrieve().bodyToMono(String.class).doOnSuccess((s) -> {
            Do();
        }).subscribe();
    }

java21最新的分代zgc

使用参数:-XX:+UseZGC -XX:+ZGenerational -Xmx4g

zgc是没有年轻代老年代的,因此它的gc动作是发生在所有堆内存上的。分代zgc把分代这个传统模式又加回了zgc,毕竟再快的gc,运行在更小的范围中也会更快。


添加图片注释,不超过 140 字(可选)


?? 虚拟线程


添加图片注释,不超过 140 字(可选)


?? reactor

可以看到reactor模式的内存明显要低大概30%,虚拟线程还是比较耗费内存


普通zgc


添加图片注释,不超过 140 字(可选)


?? 虚拟线程


添加图片注释,不超过 140 字(可选)


?? reactor

可以看到reactor模式的内存会稍微低一些,但不太明显。这也是为什么分代zgc会在将来全面替换普通zgc的原因,分代后效率还是更高


默认的g1


添加图片注释,不超过 140 字(可选)


?? 虚拟线程


添加图片注释,不超过 140 字(可选)


?? reactor

g1似乎非常不适应虚拟线程,几乎满载的状态,很担心快要oom了。使用虚拟线程一定不要使用g1


结论:

虚拟线程使用要点:

  1. 不要使用线程池
  2. 尽量不要使用ThreadLocal。虽然可以正常运行,但创建太快又不能复用,会给ThreadLocal造成很大压力。这里golang的channel是比较好的方案,希望java世界在虚拟线程的普及中也有类似的方案出来
  3. 不要使用synchronized,因为其会锁住真实的ForkJoinPool造成协程调度失败。使用ReentrantLock
  4. 必须使用分代zgc,否则gc压力比过去大很多


jdk 8-17的gc对比:

jdk17的gc选择

更多精彩内容

小企业的低成本跨云指南-从阿里云宕机看跨云多活

前端后端交互争议怎么解决

内存占用不到20m的cdc工具

研发团队新鲜事儿,来公众号「迷路idea」找我一起探讨

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表