⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7276999034962280508 「BiggFish」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

代码测试

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 倍左右。(具体需要时间根据电脑的配置决定)。

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

对象分配规则

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

栈上分配

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

  • 栈空间小,对于大对象无法实现栈上分配
  • 对象不能出现逃逸(JVM 参数:-XX:+DoEscapeAnalysis)
  • 对象可以进行标量替换,即是使用字段来表示对象(-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是如何分配的呢?可以有两种情况:

  • 废弃当前的 TLAB,重新申请;
  • 将这个 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 一般会对多线程竞争分配的时候提升比较明显,此处不再验证)

文章目录
  1. 1. 代码测试
  2. 2. 对象分配规则
    1. 2.0.1. 栈上分配
    2. 2.0.2. TLAB 分配
    3. 2.0.3. Demo 分析