你真的以为对象都是在堆上分配的么?

832 阅读4分钟

这是我参与新手入门的第4篇文章

前言

相信大家在学习jvm的时候都了解它的内存结构,具体如下图,其中堆上存放的是对象,那么问题来了,java中的对象都是在堆上进行分配的么?相信大家看完文章就能明白了。 image.png

分配策略

首先我们先看下java对象的分配流程

graph LR
A[开始] --> B{尝试栈上分配} --> |成功| 1[栈上分配]
    B -->  |失败|C{尝试TLAB分配} --> |成功| 2[TLAB分配]
    C --> |失败| D{是否进入年老代}--> |成功| 3[年老代分配]
    D --> |失败| E[eden分配]

在这个流程图中我们可以看到,java中的对象在最开始的时候都要先进行栈上分配和TLAB分配,当满足条件的是否,就不会进入到堆上分配的环节了,接下来,我们大家熟悉下栈上分配和TLAB分配分别是什么东西

栈上分配

栈上分配是java虚拟机提供的一种优化技术,基本思想是对于那些线程私有的对象(指的是不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提供系统的性能。我们看一段代码

private static void test(){
   User user = new User();
   u.name = "xtianyaa";
   u.website = "https://juejin.cn/user/2084329778071479";
}

可以看到 user 实例作用域只在 test 函数中,不会被其他线程访问到,也不会访问。所以该 user 实例对象的作用域只在该函数中,经过逃逸分析,未发生逃逸。什么是逃逸分析?其实就是判断我们将这个user对象会不会return出去,出去了的话,这时候我们对于这个对象来说就不会受用栈上分配,因为后续的代码可能还需要使用这个对象实例,可以说只要是多个线程共享的对象就是逃逸对象对于这样的情况,虚拟机就有可能将其分配在栈上,而不在堆上。
那么如何开始栈上分配呢,我们可以通过如下命令进行开启
-server -Xms16m -Xmx16m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+UseTLAB -XX:+EliminateAllocations
其中 -server 表示使用 Server 模式运行 JVM,因为只有在 Server 模式下,才可以启用逃逸分析。
那么栈上分配的作用是什么呢?不需要GC介入去回收这个对象,出栈即释放资源,可以提高性能,原理:由于我们GC每次回收对象的时候,都会触发Stop The World(停止世界),这时候所有线程都停止了,然后我们的GC去进行垃圾回收,如果对象频繁创建在我们的堆中,也就意味这我们也要频繁的暂停所有线程,这对于用户无非是非常影响体验的,栈上分配就是为了减少垃圾回收的次数。

TLAB分配

什么是TLAB? 完成单词是Thread-local allocation buffer,是JVM在内存新生代Eden Space中开辟了一小块区域,由线程私有。

  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden区域内。
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以讲这种内存分配方式成为快速分配策略 一般的对象分配内存,都是在新生代进行空间申请的。在多个线程都在申请空间时,每次对象分配都必须进行同步。竞争激烈的场合分配的效率又会进一步下降。TLAB是一个存在于eden区的线程独享内存区域,主要用于降低在新生代分配对象时的内存竞争,提升对象分配的效率。 默认开启,也可以使用-XX:+UseTLA参数主动开启。

总结

那么为什么不都在堆上分配呢?这个问题我们回到最开始的jvm内存结构上来,在jvm的定义中,堆是属于线程共享的,这就意味着在堆上分配的所有对象都是属于竞争资源,那么势必在堆上创建分配时对它们进行加锁处理。既然有锁,就必定存在锁带来的开销,而且由于是对整个堆加锁,相对而言锁的粒度还是比较大的,影响效率。而无论是TLAB还是栈都是线程私有的,私有即避免了竞争。所以对于某些特殊情况,可以采取避免在堆上分配对象的办法,以提高对象创建和销毁的效率。