(2.1.1.6)JVM之对象分配:栈上分配 & TLAB分配

Java对象实例和数组元素都是在堆上分配内存的吗?
答:不一定。满足特定条件时,它们可以在(虚拟机)栈上分配内存

这和我们平时的理解可能有些不同。虚拟机栈一般是用来存储基本数据类型、引用和返回地址的,怎么可以存储实例数据了呢?

这是因为Java JIT(just-in-time)编译器进行的两项优化,分别称作逃逸分析(escape analysis)和标量替换(scalar replacement)

在这里插入图片描述

结合栈上分配 & TLAB分配, Java对象分配流程描述为:
Java对象分配流程
在这里插入图片描述

一、栈上分配

  • 本质:Java虚拟机提供的一项优化技术
  • 基本思想: 将线程私有的对象打散分配在栈上
  • 优点:
    • 可以在函数调用结束后自行销毁对象,不需要垃圾回收器的介入,有效避免垃圾回收带来的负面影响
    • 栈上分配速度快,提高系统性能
  • 局限性
    • 栈空间小,对于大对象无法实现栈上分配
    • 只有满足 逃逸分析 & 标量替换 的对象才会栈上分配

栈上分配依赖于逃逸分析和标量替换

1.1 技术基础: 逃逸分析

逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针

  • 如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中被访问到的地方无法确定——这样指针就成功“逃逸”了。
  • 如果指针存储在全局变量或者其它数据结构中,因为全局变量是可以在当前子程序之外访问的,此时指针也发生了逃逸

逃逸分析的目的: 判断对象的作用域是否超出函数体[即:判断是否逃逸出函数体]

简单来讲,JVM中的逃逸分析可以通过分析对象引用的使用范围(即动态作用域),来决定对象是否要在堆上分配内存,也可以做一些其他方面的优化。

//user的作用域超出了函数setUser的范围,是逃逸对象
//当函数结束调用时,不会自行销毁user
private User user;
public void setUser(){
    user = new User();
    user.setId(1);
    user.setName("blueStarWei");
}

//u只在函数内部生效,不是逃逸对象
//当函数调用结束,会自行销毁对象u
public void createUser(){
    User u = new User();
    u.setId(2);
    u.setName("JVM");
}

1.2 标量替换

逃逸分析只是栈上内存分配的前提,接下来还需要进行标量替换才能真正实现

所谓标量,就是指JVM中无法再细分的数据,比如int、long、reference等。相对地,能够再细分的数据叫做聚合量

public class EscapeAnalysisTest {
  public static void main(String[] args) throws Exception {
    long start = System.currentTimeMillis();
    for (int i = 0; i < 5000000; i++) {
      allocate();
    }
    System.out.println((System.currentTimeMillis() - start) + " ms");
    Thread.sleep(600000);
  }

  static void allocate() {
    MyObject myObject = new MyObject(2019, 2019.0);
  }

  static class MyObject {
    int a;
    double b;

    MyObject(int a, double b) {
      this.a = a;
      this.b = b;
    }
  }
}
  • 关闭逃逸分析之后,堆上有5000000个MyObject实例,而开启逃逸分析之后,就只剩下90871个实例了,不管是实例数还是内存占用都只有原来的2%不到
  • MyObject就是一个聚合量,因为它由两个标量a、b组成

通过逃逸分析,JVM会发现myObject没有逃逸出allocate()方法的作用域,标量替换过程就会将myObject直接拆解成a和b,也就是变成了:

  static void allocate() {
    int a = 2019;
    double b = 2019.0;
  }

可见,对象的分配完全被消灭了,而int、double都是基本数据类型,直接在栈上分配就可以了。

所以,在对象不逃逸出作用域并且能够分解为纯标量表示时,对象就可以在栈上分配。

1.3 栈上分配示例

package com.blueStarWei.templet;

public class AllotOnStack {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    private static void alloc() {
        User user = new User();
        user.setId(1);
        user.setName("blueStarWei");
    }
}

上述代码调用了1亿次alloc(),如果是分配到堆上,大概需要1.5GB的堆空间,如果堆空间小于该值,必然会触发GC。

  • 使用如下参数运行,发现不会触发GC
-server -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
  • 使用如下参数(任意一行)运行,会发现触大量GC
//不使用逃逸分析
-server -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
//不使用标量替换
-server -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:-EliminateAllocations

1.4 JVM参数解析

参数作用备注
-server使用server模式只有在server模式下,才可以弃用逃逸分析
-Xmx15m设置最大堆空间为15m如果在堆上分配,必然触发大量GC
-Xms15m设初始对空间为15m
-XX:+DoEscapeAnalysis启用逃逸分析 默认启用
-XX:-DoEscapeAnalysis关闭逃逸分析
-XX:+PrintGC打印GC日志
-XX:-UseTLAB 关闭TLABTLAB(Thread Local Allocation Buffer)线程本地分配缓存区
-XX:+EliminateAllocations启用标量替换,允许对象打散分配到栈上默认启用
-XX:-EliminateAllocations关闭标量替换

二、TLAB 分配

TLAB,全称Thread Local Allocation Buffer, 即:线程本地分配缓存。这是一块线程专用的内存分配区域。

TLAB占用的是eden区的空间。在TLAB启用的情况下(默认开启),JVM会为每一个线程分配一块TLAB区域。

  • 为什么需要TLAB?
    • 这是为了加速对象的分配。
    • 由于对象一般分配在堆上,而堆是线程共用的,因此可能会有多个线程在堆上申请空间,而每一次的对象分配都必须线程同步,会使分配的效率下降。
    • 考虑到对象分配几乎是Java中最常用的操作,因此JVM使用了TLAB这样的线程专有区域来避免多线程冲突,提高对象分配的效率。
  • 局限性
    • TLAB空间一般不会太大(占用eden区),所以大对象无法进行TLAB分配,只能直接分配到堆上。

2.1 分配策略

一个100KB的TLAB区域,如果已经使用了80KB,当需要分配一个30KB的对象时,TLAB是如何分配的呢?

此时,虚拟机有两种选择:

  • 第一,废弃当前的TLAB(会浪费20KB的空间);
  • 第二,将这个30KB的对象直接分配到堆上,保留当前TLAB(当有小于20KB的对象请求TLAB分配时可以直接使用该TLAB区域)。

JVM选择的策略是:在虚拟机内部维护一个叫refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,反之,则会废弃当前TLAB,新建TLAB来分配新对象。

【默认情况下,TLAB和refill_waste都是会在运行时不断调整的,使系统的运行状态达到最优。】

2.2 JVM指令

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

参考文献

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值