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分配。

TLABThread Local Allocation Buffer)本地线程分配缓冲区,每个线程会首先向OS申请一大块内存作为私有的内存缓冲区,当创建对象需要申请内存时,优先从内存缓冲区中进行分配,由于是线程私有的,不存在并发问题,速度会非常快。这样只有当内存缓冲区不够用时,线程才会同步的向OS申请内存,大大减少了锁的争用。

如下代码,开启10个线程并发创建一千万个对象,分别开启和禁用TLAB进行测试:
在这里插入图片描述
测试结果如下:

是否开启TLAB耗时(ms)
1697
12070

可以看到,开启TLAB后可以获得7倍多的性能提升,效果还是很明显的。

创建对象时,为了避免JVM进行栈上分配看不到效果,记得把对象赋值给静态变量,或者关闭JVM逃逸分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值