Java是一种面向对象的跨平台语言。它的对象、内存等一直是比较难的知识点。因此,即使是Java初学者,也一定或多或少对JVM有一定的了解。可以说关于JVM的知识基本上是每个Java开发者必须学习的知识点,也是面试时必须考的知识点。在JVM的内存结构中,两个共同的区域是堆内存和栈内存(如果没有特别说明,本文所说的栈指代虚拟机栈)。关于堆和栈的区别,很多开发者在网上也有很多书籍或者文章大概是这样介绍的:1.堆是线程共享的内存区域,栈是线程独享的内存区域线程。2、堆主要存放对象实例,栈主要存放各种基本数据类型和对象的引用。不过笔者可以负责任的告诉大家,以上两个结论都不是完全正确的。本文先带大家理解为什么我说“堆是线程共享的内存区域,栈是线程独享的内存区域”。这句话不完全正确!?JVM内存结构的相关知识可以看JVM内存结构VSJava内存模型VSJava对象模型,没想到JVM内存结构的面试题竟然这么难?和其他文章。在进入正题之前,请允许我提出一个似乎与本题无关的问题:Java对象的内存分配过程是如何保证线程安全的?Java对象的内存分配过程是如何保证线程安全的?我们知道,Java是一种面向对象的语言。我们在Java中使用的对象需要创建。在Java中,创建对象的方式有很多种,但无论如何,在创建过程中都需要存储对象。分发。在对象的内存分配过程中,对象的引用主要是指向这块内存区域,然后进行初始化操作。但是由于堆是全局共享的,可能会有多个线程同时申请堆上的空间。那么,在并发场景下,如果两个线程先后将对象引用指向同一个内存区域呢?为了解决这个并发问题,必须同步控制对象的内存分配过程。但是我们都知道,无论采用哪种同步方案(其实虚拟机都可能使用CAS),都会影响内存分配效率。Java对象的分配是Java中的高频操作,于是人们想到了另外一种提高效率的方法。这里重点介绍HotSpot虚拟机的一种解决方案:每个线程在Java堆中预先分配一小块内存,然后在自己的“私有”内存中直接为对象分配内存。当这部分区域用完后,分配新的“私有”内存。这种方案称为TLAB分配,即ThreadLocalAllocationBuffer。这部分Buffer是从堆中分出来的,但是是本地线程独享的。什么是TLABTLAB是堆内存eden中虚拟机划分的一块专用空间,专供线程使用。当启动虚拟机的TLAB功能时,当线程初始化时,虚拟机为每个线程分配一个TLAB空间,只为当前线程使用,让每个线程都有一个单独的空间。如果需要分配内存,直接在自己的空间中分配即可,这样不存在竞争,可以大大提高分配效率。注意到上面描述中“线程独占”、“仅供当前线程使用”、“每个线程单独拥有”的描述了吗?所以,因为TLAB技术,堆内存并没有完全被线程共享,eden区还有一部分空间专门分配给线程。这里值得注意的是,我们说TLAB是线程独享的,但它只是在“分配”这个动作中是线程独占的。至于读、垃圾回收等动作,则由线程共享。而且使用上没有区别。也就是说,虽然每个线程在初始化的时候都会在堆内存中申请一块TLAB,但是并不代表这个TLAB区域的内存是完全不能被其他线程访问的。在区域中分配内存。而且TLAB分配后,不影响对象的移动和回收。也就是说,虽然对象一开始可能会通过TLAB分配内存,存放在Eden区,但还是会被垃圾回收或者移动到SurvivorSpace,OldGen等等。另外需要注意的是,我们说TLAB分配在eden区,因为eden区本身并不算大,TLAB空间中的内存也很小,默认只占整个Eden的1%空间。所以,肯定有一些大对象不能直接在TLAB中分配。TLAB遇到无法分配的大对象时,对象可能还在eden区或者老年代分配,但是这种分配需要同步控制,这就是为什么我们常说:小对象分配比大对象更有效率.TLAB带来的问题虽然TLAB在一定程度上大大提高了对象分配速度,但是TLAB也不是没有问题。前面我们说过,由于TLAB内存区域不是很大,可能会出现不够用的情况。《实战Java虚拟机》中有这样一个例子:比如一个线程的TLAB空间有100KB,已经使用了80KB。当需要再分配一个30KB的对象时,不能直接在TLAB中分配。遇到这种情况,有两种解决方法:1.如果一个对象需要的空间超过了TLAB中的剩余空间,则直接在堆内存中对该对象进行内存分配。2、如果一个对象需要的空间超过了TLAB中的剩余空间,则丢弃当前的TLAB,重新申请TLAB空间,再次进行内存分配。以上两种方案各有优缺点。如果采用方案一,那么可能会出现一种极端情况,即TLAB中只剩下1KB,这会导致以后需要分配的对象大部分直接分配到堆内存中.如果采用方案2,可能会出现频繁丢弃TLAB和频繁申请TLAB的情况。我们知道,虽然TLAB上的内存分配是线程独占的,但是在TLAB内存从堆中划分的过程中,确实可能存在冲突。,所以TLAB的分配过程其实是需要并发控制的。频繁的TLAB分配失去了使用TLAB的意义。为了解决这两种方案的问题,虚拟机定义了一个refill_waste值,可以翻译为“最大浪费空间”。当申请的内存大于refill_waste时,会分配到堆内存中。如果小于refill_waste值,则丢弃当前TLAB,重新创建TLAB进行对象内存分配。在前面的例子中,总的TLAB空间是100KB,使用了80KB,剩下的20KB设置好了。如果refill_waste的值设置为25KB,那么如果新对象的内存大于25KB,就会直接分配堆内存。如果小于25KB,则前一个将被丢弃。TLAB,重新分配一块TLAB空间,并为新对象分配内存。TLAB使用的相关参数TLAB功能可以打开或关闭。您可以通过设置-XX:+/-UseTLAB参数来指定是否启用TLAB分配。TLAB默认为eden区的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间占用Eden空间的百分比。默认情况下,TLAB的空间会在运行时不断调整,使系统达到最佳运行状态。如果需要关闭TLAB的自动调整大小,可以使用-XX:-ResizeTLAB关闭,使用-XX:TLABSize手动指定TLAB的大小。TLAB的refill_waste也可以调整。默认值为64,这意味着大约1/64的空间被用作refill_waste。使用参数:-XX:TLABRefillWasteFraction进行调整。如果想观察TLAB的使用情况,可以使用参数-XX+PringTLAB进行跟踪。小结为了保证对象内存分配过程中的线程安全,HotSpot虚拟机提供了一种叫做TLAB(ThreadLocalAllocationBuffer)的技术。线程初始化时,虚拟机为每个线程分配一个TLAB空间,只供当前线程使用。当它需要分配内存的时候,就在自己的空间里分配,这样就没有了竞争,可以大大提高分配效率。因此,“堆是线程共享的内存区域”这句话并不完全正确,因为TLAB是堆内存的一部分。在读取上确实是线程共享的,但是在内存分配上是线程独享的。TLAB空间其实并不大,所以大对象可能还是需要直接在堆内存中分配。那么,对象的内存分配步骤就是先尝试TLAB分配。空间不足后,再判断是不是直接进入老年代,再决定是在伊甸园分配还是在老年代分配。我再多说几句,相信看完这篇文章,有些人可能会觉得作者有点太“苦”和“挑剔”了。可能有些心急的人,只看了开头就翻到文章结尾来打架了。不管你是否同意作者的观点:“堆是线程共享的内存区域的说法并不完全正确”。这其实并不重要。重要的是,涉及到堆内存、线程共享、对象内存分配,你能想到一个专门的TLAB,就可以了。有时候,最可怕的不是你不知道,而是你不知道你不知道。更何况TLAB只是HotSpot虚拟机的一种优化方案,Java虚拟机规范中并没有关于TLAB的规定。因此,并非所有虚拟机都具有此功能。本文的概述基于HotSpot虚拟机,笔者无意“一概而论”,而是因为HotSpot虚拟机是目前最流行的虚拟机。大多数默认设置,当我们讨论它时,也是基于HotSpot的。哎,每次写一些技术文章,都会有很多人喷,而且喷的角度都千奇百怪,只好多说几句弥补一下。不管怎样,欢迎任何形式的讨论,因为就算是喷,也未必有对手!
