当前位置: 首页 > 科技观察

说说Java对象在栈上的分配

时间:2023-03-19 23:33:53 科技观察

通过对对象分配过程的分析,除了堆之外,还有两个地方可以存放对象:栈和TLAB(ThreadLocalAllocationBuffer)。Java对象分配流程图:如果启用了栈上分配,JVM会先进行栈上分配。如果不启用栈上分配或者不满足条件的会分配到TLAB,如果TLAB分配不成功,则尝试在eden区分配,如果对象满足直接进入老年代,直接在老年代分配。栈在JVM中分配,堆由线程共享。因此,堆上的对象是共享的,对每个线程都是可见的。只要持有对象的引用,就可以访问存储在堆中的对象数据。虚拟机的垃圾回收系统可以回收堆中不再使用的对象,但是对于垃圾回收器来说,过滤可回收对象,回收整理内存是需要时间的。如果确定一个对象的作用域不会脱离方法,那么就可以在栈上分配这个对象,这样就可以随着栈帧出栈而销毁该对象所占用的内存空间。在一般应用中,不会逃逸的局部对象所占比例非常大。如果可以使用栈分配,大量的对象会随着方法结束自动销毁,不需要通过垃圾收集器回收,可以减少小型垃圾收集器的负载。JVM允许线程私有对象分散分配在栈上而不是堆上。在栈上分配的好处是可以在函数调用结束后销毁,无需垃圾收集器的干预,从而提高系统性能。在栈上分配的技术基础:一、逃逸分析:逃逸分析的目的是判断对象的范围是否有可能逃出函数体。关于逃逸分析的问题可以看我的另一篇文章:二是标量替换:允许对象在栈上分散分配。例如,如果一个对象有两个字段,这两个字段将被视为局部变量进行分配。逃逸分析只能在服务器模式下启用。参数-XX:DoEscapeAnalysis启用逃逸分析,参数-XX:+EliminateAllocations启用标量替换(默认开启)。JavaSE6u23版本之后,HotSpot默认开启逃逸分析,可以通过选项-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果。TLAB(ThreadLocalAllocationBuffer)`TLAB的全称是ThreadLocalAllocationBuffer,即线程本地分配缓冲区,是线程专有的内存分配区域。由于对象一般分配在堆上,而堆是全局共享的。因此,同时可能有多个线程在堆上申请空间。因此,每次对象分配都必须同步(虚拟机使用失败重试的CAS来保证更新操作的原子性),在竞争激烈的情况下分配效率会进一步降低。JVM使用TLAB来避免多线程冲突。在为对象分配内存时,每个线程使用自己的TLAB,可以避免线程同步,提高对象分配效率。TLAB本身占用eEden区的空间。启用TLAB后,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认开启。TLAB空间的内存很小。默认情况下,它只占整个Eden空间的1%。当然你可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间占用Eden空间的百分比。由于TLAB空间一般不会很大,大对象不能分配在TLAB上,总是直接分配在堆上。由于TLAB空间比较小,很容易被填满。比如100K的空间已经用了80KB。当需要再分配一个30KB的对象时,肯定是无能为力了。这时,虚拟机有两种选择。首先,丢弃当前的TLAB,会浪费20KB的空间;第二,直接在堆上分配30KB的对象,保留当前的TLAB,这样以后就可以希望不到20KB了。对象分配请求可以直接使用这个空间。实际上,虚拟机会维护一个叫做refill_waste的值。当请求的对象大于refill_waste时,会选择在堆中分配。如果小于这个值,它将丢弃当前的TLAB并创建一个新的TLAB来分配对象。可以使用TLABRefillWasteFraction调整此阈值,它表示允许产生此废物的TLAB分数。默认值为64,也就是说大约有1/64的TLAB空间被用作refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整,使系统运行在最佳状态。如果要禁用TLAB的自动调整大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,使用-XX:TLABSize手动指定一个TLAB大小,-XX:+PrintTLAB可以跟踪TLAB的使用情况。一般不建议手动修改TLAB相关参数,建议使用虚拟机的默认行为。`所谓的TLAB其实就是这样一个东西:(简化伪代码)structThreadLocalAllocBuffer{HeapWord*_start;堆字*_top;堆字*_end;};每个线程都会从Eden中分配一个很大的空间,比如100KB,作为自己的TLAB。这个start是TLAB的起始地址,end是TLAB的结束,top是当前分配指针。显然开始<=顶部<结束。Eden在分配空间时,采用bump-the-pointer的方式进行分配,但是由于Eden是所有Java线程共享的,所以在bumppointers时必须加锁(或CAS)以保证安全;而当每个线程从Eden中分配一块空间作为TLAB后,在TLAB中分配一小块空间也是bump-the-pointer(如上图的top指针),不需要加锁。Java线程在自己的TLAB分配到末尾时,如果需要再次分配,会触发“TLABrefill”,也就是说自己的TLAB被“忽略”(归还共享Eden)),然后重新启动。从Eden分配一个空间作为新的TLAB。所谓“不管”,并不是让旧TLAB中的物体直接消亡,而是将那片空间的控制权还给普通的伊甸园,里面的物体就该是它们本来的样子。通常,TLAB会被分配多次来填满TLAB并触发TLABrefill。这样一来,与直接从Eden的共享部分进行分配相比,使用TLAB的分配得到了摊销(amortized),从而提高了性能。事实上,很多注重多线程性能的malloc库实现也使用了类似的方法,比如TCMalloc。当GC被触发时,无论是minorGC还是fullGC,在收集Eden时,对其中的空间属于某个线程的某个TLAB还是不属于任何TLAB一视同仁,Eden中的对象是collectedasawhole——将存活的对象复制到survivorspace(或者直接提升到OldGen)。GC结束后,每个Java线程都会从Eden重新分配自己的TLAB。反复。想象这样的代码:publicclassTest{publicstaticTestsharedStatic;公共测试sharedInstanceField;publicstaticvoidfoo(){测试localVar=newTest();//1if(sharedStatic==null){sharedStatic=localVar;//2}else{sharedStatic.sharedInstanceField=localVar;//3}}}(这个例子纯粹是为了说明“独占”和“共享”的概念,请不要抱怨线程安全问题),我们新建了一个Test实例。正如题主所说,当UseTLAB启动时(默认开启),Test实例会被分配到当前执行Test.foo()的线程的TLAB中。TLAB在执行分配动作时需要更新top指针,更新这个指针不需要任何锁。对象内存分配的两种方法为对象分配空间的任务相当于从Java堆中划分出一定大小的内存。指针碰撞(Serial、ParNew等带Compact进程的收集器)假设Java堆中的内存是绝对有规律的,所有已用内存放在一边,空闲内存放在另一边,指针放在中间作为分界点分配的内存只是将指针移动到空闲空间的距离等于对象的大小。这种分配方法称为“BumpthePointer”。freelist(CMS,一种基于Mark-Sweep算法的收集器)如果Java堆中的内存不规则,已用内存和空闲内存交错,那么简单的指针碰撞是没有办法的。虚拟机需要维护一个列表来记录哪些内存块是可用的。分配时,从链表中找到足够大的空间分配给对象实例,并更新链表上的记录。这种分配方式称为“空闲列表”(FreeList)。