Java对象的内存是在哪里分配的?
Java内存分配策略
当我们使用new
关键字去实例化一个对象时,对象的内存在哪里分配?
相信很多Java程序员给出的答案都是【堆】,但事实并非绝对如此,JVM为此做了许多优化。
对于绝大多数对象,内存的确是在堆中分配的,但是随着JIT编译器的进步、逃逸分析技术的成熟,【Java对象都是在堆中分配内存】这个结论变得不是那么绝对了。
针对Java的内存分配策略,笔者这里画了一张简图如下:
栈上分配
当实例化的对象占用的内存空间较小,且对象没有发生逃逸时,JVM就可以直接进行【栈上分配】优化,即对象的内存空间分配在栈上,而不是堆上。
这样做的好处是:对象可以随着方法运行结束时栈帧的出栈而被销毁,不需要GC介入处理,减轻GC的压力。
如下代码,频繁的创建byte数组,由于allocate()将数组赋值给了静态变量,使得其他线程可以访问到,即对象发生了线程逃逸,使得JVM无法进行栈上分配,只能分配在堆上,这时候就导致GC需要不断的清理对象,导致系统停顿时间过长。
如下,是堆上分配的GC情况。
注释赋值语句,对象不能逃逸,测试结果如下:
可以看到,【栈上分配】可以极大的减轻GC的压力,减少GC的停顿时间,提高系统的性能和吞吐量。
大对象进入老年代
当实例化的对象占用内存非常大时,JVM也会做优化,优先将其分配进老年代,而不是新生代。
原因是:首先Eden区不一定能装得下大对象,其次,就算Eden区能装的下,如果经历一次GC后大对象没有被回收,由于新生代使用的是标记复制算法,JVM会将其复制到其中一个Survivor区,大对象的复制开销是比较大的,而且Survivor区空间通常很小,很难容纳大对象。如果把大对象放入Survivor区,会导致其他小对象难以复制,影响新生代的标记复制算法的正常工作。
如下代码,证明了这个结论:
在实际开发中,还是应该尽量避免使用大对象,例如:长度很大的数组。
使用大对象会增加JVM的压力,大对象通常较难以回收,要想回收大对象JVM就不得不触发Full GC,效率还是很低的。
TLAB快慢分配
当实例对象不能在栈上分配内存时,JVM就只能将其分配到堆了,即便如此,JVM依然可以做很多优化。
由于对象的创建是非常频繁的行为,可能存在多个线程并发的创建对象,如果不做同步处理,可能会导致多个实例申请同一块内存。
JVM可以选择通过加锁来分配内存,但是这样性能较低,于是就有了TLAB分配。
TLAB(Thread Local Allocation Buffer)本地线程分配缓冲区,每个线程会首先向OS申请一大块内存作为私有的内存缓冲区,当创建对象需要申请内存时,优先从内存缓冲区中进行分配,由于是线程私有的,不存在并发问题,速度会非常快。这样只有当内存缓冲区不够用时,线程才会同步的向OS申请内存,大大减少了锁的争用。
如下代码,开启10个线程并发创建一千万个对象,分别开启和禁用TLAB进行测试:
测试结果如下:
是否开启TLAB | 耗时(ms) |
---|---|
是 | 1697 |
否 | 12070 |
可以看到,开启TLAB后可以获得7倍多的性能提升,效果还是很明显的。
创建对象时,为了避免JVM进行栈上分配看不到效果,记得把对象赋值给静态变量,或者关闭JVM逃逸分析。