一行代码引发的性能暴跌 10 倍!

业余草

共 4332字,需浏览 9分钟

 ·

2023-09-18 20:10

你知道的越多,不知道的就越多,业余的像一棵小草!

你来,我们一起精进!你不来,我和你的竞争对手一起精进!

编辑:业余草

来源:juejin.cn/post/7276999034962280508

推荐:https://t.zsxq.com/12FEd8lJL

自律才能自由

今天给大家分享一个,最近遇到的性能优化问题。线上的业务逻辑比较复杂,本文只提供相关代码的简化程序。

下面我们直接上测试代码。

代码测试

import com.google.common.base.Stopwatch;
import java.util.concurrent.TimeUnit;
public class StackTest {
    public static void main(String[] args) {
        Stopwatch started = new Stopwatch();
        started.start();
        User user = null;
        for (long i = 0; i < 1000_000_000; i++) {
            user = new User();
        }
        started.stop();
        System.out.println(started.elapsed(TimeUnit.MILLISECONDS) + "ms");
        //不加打印 300ms
        //加了打印 3000ms
//        System.out.println(user);
    }
}

class User {
    private int age;
    private String userName;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
}

上面的一个简单的代码是测试 Java 创建对象的性能,如果没有 System.out.println(user); 输出的时间是 300ms左右,如果加上性能是 3000ms 左右,整整慢了 10 倍左右。(具体需要时间根据电脑的配置决定)。

有人可能会说,System.out.println函数有锁(都 9102 了,你还不知道 System.out.println 的危害!),但要注意,这里是单线程,Java 会锁消除

看似很简单的代码,却会带来这样的性能消耗,确实很让人费解。为了弄清楚这个问题,我们需要讨论下,java 代码分配的规则。

对象分配规则

在前面的博客已经提过 Java 对象的分配过程,具体流程图如下:

栈上分配

栈上分配是 Java 虚拟机提供的一项优化技术,将线程私有的对象打散分配在栈上,栈上分配的对象回收直接 POP 出站,不需要垃圾回收器的介入,效率很高。当然栈上分配也需要一些特殊的条件:

  1. 栈空间小,对于大对象无法实现栈上分配
  2. 对象不能出现逃逸(JVM 参数:-XX:+DoEscapeAnalysis
  3. 对象可以进行标量替换,即是使用字段来表示对象(-XX:+EliminateAllocations)。

如 demo 所示,我们可以是用 age 和 username 两个字段来代替 User 对象。

TLAB 分配

TLAB Thread Local Allocation Buffer, 即:线程本地分配缓存。这是一块线程专用的内存分配区域。TLAB 占用的是 eden 区的空间。在TLAB 启用的情况下(默认开启),JVM会为每一个线程分配一块TLAB区域。

使用 TLAB 是为了加速对象的分配。由于对象一般分配在堆上,而堆是线程共用的,因此可能会有多个线程在堆上申请空间,而每一次的对象分配都必须线程同步,会使分配的效率下降。

考虑到对象分配几乎是 Java中 最常用的操作,因此 JVM 使用了 TLAB 这样的线程专有区域来避免多线程冲突,提高对象分配的效率。

同样,TLAB 空间一般不会太大(占用 eden 区),所以大对象无法进行 TLAB 分配,只能直接分配到堆上。

分配策略

一个100KB的TLAB区域,如果已经使用了80KB,当需要分配一个30KB的对象时,TLAB是如何分配的呢?可以有两种情况:

  1. 废弃当前的 TLAB,重新申请;
  2. 将这个 30KB 的对象直接分配到堆上,保留当前 TLAB(当有小于 20KB 的对象请求 TLAB 分配时可以直接使用该 TLAB 区域)。

JVM选择的策略是:在虚拟机内部维护一个叫 refill_waste 的值,当请求对象大于 refill_waste 时,会选择在堆中分配,反之,则会废弃当前 TLAB,新建 TLAB来分配新对象。【默认情况下,TLAB和refill_waste都是会在运行时不断调整的,使系统的运行状态达到最优。】

JVM参数解析

参数 作用 备注
-XX:+UseTLAB 启用TLAB 默认启用
-XX:TLABRefillWasteFraction 设置允许空间浪费的比例 默认值:64,即:使用1/64的TLAB空间大小作为refill_waste值
-XX:-ResizeTLAB 禁止系统自动调整TLAB大小
-XX:TLABSize 指定TLAB大小 单位:B

Demo 分析

通过上面的分析,可以剖析出原因了,在使用打印的时候导致了 user 对象的逃逸,所以导致在栈上分配条件不满足,只能在堆上分配,这样就会导致频繁的 GC,效率低下。

如果我们再使用(-XX:+UseTLAB)关闭 TLAB分配原则,则会导致分配的速度又会降低一点(TLAB 一般会对多线程竞争分配的时候提升比较明显,此处不再验证)

浏览 1045
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报