【Java学习】Java对象怎么分配

  • Post author:
  • Post category:java



目录


对象怎么分配


逃逸分析


标量替换


栈上分配对象


测试一:开启逃逸分析


测试二:关闭逃逸分析


对象内存分配的两种方法


TLAB分配


为什么有TLAB?


关键字:逃逸分析,标量替换,TLAB,指针碰撞,空闲列表。

对象怎么分配

对象是否能在栈上分配依赖于JIT(及时编译)和逃逸分析。

逃逸分析

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用。


  • 方法逃逸

    :例如作为调用参数传递到其他方法中。


  • 线程逃逸:

    有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量。

标量替换

首先要明白标量和聚合量,基础类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象。

对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做

标量替换

这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。

标量替换的 JVM 参数如下:

  • 开启标量替换:

    -XX:+EliminateAllocations
  • 关闭标量替换:

    -XX:-EliminateAllocations
  • 显示标量替换详情:

    -XX:+PrintEliminateAllocations

注:标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上。

栈上分配对象

栈上分配对象是jvm提供的一种技术优化



栈上分配



依赖于



逃逸分析







标量替换



在我们的应用程序中,其实有很多的对象的作用域都不会逃逸出方法外,也就是说该对象的

生命周期会随着方法的调用开始而开始,方法的调用结束而结束。

因为一旦分配在堆空间中,当方法调用结束,没有了引用指向该对象,该对象就需要被gc回收,而如果存在大量的这种情况,对gc来说无疑是一种负担。

因此,JVM提供了一种叫做

栈上分配

的概念,针对那些

作用域不会逃逸出方法的对象

,在分配内存时不再将对象分配在堆内存中,而是将对象属性

打散后分配在栈(线程私有的,属于栈内存)上

,这样,随着方法的调用结束,栈空间的回收就会随着将栈上分配的打散后的对象回收掉,不再给gc增加额外的无用负担,从而提升应用程序整体的性能

测试一:开启 逃逸分析 和 标量替换

设置jvm参数:开启逃逸分析,允许对象打散分散到栈上。

// 打印gc日志 -XX:+PrintGC
// 开启逃逸分析(hotspot默认开启) -XX:+DoEscapeAnalysis
// 设置 最大堆空间250M 初始堆空间250M -Xmx250m -Xms250m

// 设置jvm参数:开启逃逸分析,允许对象打散分散到栈上
// -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
public static void main(String[] args) {
    long start = System.currentTimeMillis();
    for (int i = 0; i < 50000000; i++) {
        createUser();
    }
    long end = System.currentTimeMillis();
    System.out.println("耗时:" + (end - start) + "ms");
}

public static void createUser() {
    // 此时逃逸分析开启,user对象不会逃逸到方法外,且是热点对象,对象分配在栈上,createUser方法结束后,user对象被清除
    User user = new User("张三");
}

static class User {

    private String name;

    public User(String name) {
        this.name = name;
    }
}

结果:

user对象随着createUser方法结束自动被清除,不会造成gc回收。



注:设置jvm参数:开启逃逸分析,

不允许

对象打散分散到栈上。此时会触发大量gc。

测试二:关闭逃逸分析

// 打印gc日志 -XX:+PrintGC 
// 关闭逃逸分析 -XX:-DoEscapeAnalysis
// 设置 最大堆空间250M 初始堆空间250M -Xmx250m -Xms250m
public static void main(String[] args) {
    long start = System.currentTimeMillis();
    for (int i = 0; i < 50000000; i++) {
        createUser();
    }
    long end = System.currentTimeMillis();
    System.out.println("耗时:" + (end - start) + "ms");
}

public static void createUser() {
    // 此时逃逸分析关闭,对象分配在堆上,createUser方法结束后,user对象不会立即被清除,需要等待gc回收
    User user = new User("张三");
}

static class User {

    private String name;

    public User(String name) {
        this.name = name;
    }
}

结果:

由于循环次导致user对象创建,堆内存不够触发gc。

总耗时245ms,相比于开启逃逸分析多了241ms,主要是因为gc会触发STW(stop-the-world,即停止所有业务线程,知道gc线程回收完)。

对象内存分配的两种方法

为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

  • 指针碰撞(

    Serial、ParNew等带Compact过程的收集器

    )

    假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。

  • 空闲列表(

    CMS这种基于Mark-Sweep算法的收集器

    )

    如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。



选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。


因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

TLAB分配

为什么有TLAB?

1、堆是 JVM 中所有线程共享的,因此在其上

进行对象内存的分配均需要进行加锁

,这也导致了 new 对象的开销是比较大的。

(1)、Sun Hotspot JVM 为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间 TLAB(Thread Local Allocation Buffer),其大小由 JVM 根据运行的情况计算而得,在 TLAB 上分配对象时不需要加锁,因此 JVM 在给线程的对象分配内存时会尽量的在 TLAB 上分配,但如果对象过大的话则仍然是直接使用堆空间分配。

(2)、TLAB 仅作用于新生代的 Eden Space,因此在编写 Java 程序时,通常多个小的对象比大的对象分配起来更加高效。


值得注意的是,我们说 TLAB 是线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。

2、对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

解决这个问题有两种方案:

(1)、对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;

(2)、把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,

TLAB

)。

哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。默认的TLAB区域大小是Eden区域的1%,也可以手工进行调整,对应的JVM参数是

-XX:TLABWasteTargetPercent

。内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。



版权声明:本文为qq_27688393原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。