Java做为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点,因此,即便是一个Java的初学者,也必定或多或少的对JVM有一些了解。能够说,关于JVM的相关知识,基本是每一个Java开发者必学的知识点,也是面试的时候必考的知识点。面试
在JVM的内存结构中,比较常见的两个区域就是堆内存和栈内存(如无特指,本文提到的栈均指的是虚拟机栈),关于堆和栈的区别,不少开发者也是如数家珍,有不少书籍,或者网上的文章大概都是这样介绍的:安全
一、堆是线程共享的内存区域,栈是线程独享的内存区域。并发
二、堆中主要存放对象实例,栈中主要存放各类基本数据类型、对象的引用。优化
可是,做者能够很负责任的告诉你们,以上两个结论均不是彻底正确的。线程
本文首先带你们了解一下为何我会说“堆是线程共享的内存区域,栈是线程独享的内存区域。”这句话并不彻底正确!?关于JVM内存结构的相关知识,你们能够阅读JVM内存结构 VS Java内存模型 VS Java对象模型、万万没想到,JVM内存结构的面试题能够问的这么难?等文章。翻译
在开始进入正题以前,请容许我问一个和这个问题看似没有任何关系的问题:Java对象的内存分配过程是如何保证线程安全的?3d
咱们知道,Java是一门面向对象的语言,咱们在Java中使用的对象都须要被建立出来,在Java中,建立一个对象的方法有不少种,可是不管如何,对象在建立过程当中,都须要进行内存分配。cdn
对象的内存分配过程当中,主要是对象的引用指向这个内存区域,而后进行初始化操做。对象
可是,由于堆是全局共享的,所以在同一时间,可能有多个线程在堆上申请空间,那么,在并发场景中,若是两个线程前后把对象引用指向了同一个内存区域,怎么办。blog
为了解决这个并发问题,对象的内存分配过程就必须进行同步控制。可是咱们都知道,不管是使用哪一种同步方案(实际上虚拟机使用的多是CAS),都会影响内存的分配效率。
而Java对象的分配是Java中的高频操做,全部,人们想到另一个办法来提高效率。这里咱们重点说一个HotSpot虚拟机的方案:
每一个线程在Java堆中预先分配一小块内存,而后再给对象分配内存的时候,直接在本身这块”私有”内存中分配,当这部分区域用完以后,再分配新的”私有”内存。
这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,可是是本地线程独享的。
TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的状况下,在线程初始化时,虚拟机会为每一个线程分配一块TLAB空间,只给当前线程使用,这样每一个线程都单独拥有一个空间,若是须要分配内存,就在本身的空间上分配,这样就不存在竞争的状况,能够大大提高分配效率。
注意到上面的描述中"线程专属"、"只给当前线程使用"、"每一个线程单独拥有"的描述了吗?
因此说,由于有了TLAB技术,堆内存并非完彻底全的线程共享,其eden区域中仍是有一部分空间是分配给线程独享的。
这里值得注意的是,咱们说TLAB是线程独享的,可是只是在“分配”这个动做上是线程独占的,至于在读取、垃圾回收等动做上都是线程共享的。并且在使用上也没有什么区别。
也就是说,虽然每一个线程在初始化时都会去堆内存中申请一块TLAB,并非说这个TLAB区域的内存其余线程就彻底没法访问了,其余线程的读取仍是能够的,只不过没法在这个区域中分配内存而已。
而且,在TLAB分配以后,并不影响对象的移动和回收,也就是说,虽然对象刚开始可能经过TLAB分配内存,存放在Eden区,可是仍是会被垃圾回收或者被移到Survivor Space、Old Gen等。
还有一点须要注意的是,咱们说TLAB是在eden区分配的,由于eden区域自己就不太大,并且TLAB空间的内存也很是小,默认状况下仅占有整个Eden空间的1%。因此,必然存在一些大对象是没法在TLAB直接分配。
遇到TLAB中没法分配的大对象,对象仍是可能在eden区或者老年代等进行分配的,可是这种分配就须要进行同步控制,这也是为何咱们常常说:小的对象比大的对象分配起来更加高效。
虽然在必定程度上,TLAB大大的提高了对象的分配速度,可是TLAB并非就没有任何问题的。
前面咱们说过,由于TLAB内存区域并非很大,因此,有可能会常常出现不够的状况。在《实战Java虚拟机》中有这样一个例子:
好比一个线程的TLAB空间有100KB,其中已经使用了80KB,当须要再分配一个30KB的对象时,就没法直接在TLAB中分配,遇到这种状况时,有两种处理方案:
一、若是一个对象须要的空间大小超过TLAB中剩余的空间大小,则直接在堆内存中对该对象进行内存分配。
二、若是一个对象须要的空间大小超过TLAB中剩余的空间大小,则废弃当前TLAB,从新申请TLAB空间再次进行内存分配。
以上两个方案各有利弊,若是采用方案1,那么就可能存在着一种极端状况,就是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功能是能够选择开启或者关闭的,能够经过设置-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(Thread Local Allocation Buffer)的技术。
在线程初始化时,虚拟机会为每一个线程分配一块TLAB空间,只给当前线程使用,当须要分配内存时,就在本身的空间上分配,这样就不存在竞争的状况,能够大大提高分配效率。
因此,“堆是线程共享的内存区域”这句话并不彻底正确,由于TLAB是堆内存的一部分,他在读取上确实是线程共享的,可是在内存分分配上,是线程独享的。
TLAB的空间其实并不大,因此大对象仍是可能须要在堆内存中直接分配。那么,对象的内存分配步骤就是先尝试TLAB分配,空间不足以后,再判断是否应该直接进入老年代,而后再肯定是再eden分配仍是在老年代分配。
相信一部分看完这篇文章以后,可能会以为做者有点过于“咬文嚼字”、“吹毛求疵”了。可能不乏有些性子急的人只看了开头就直接翻到文末准备开怼了。
无论你认不认同做者说的:“堆是线程共享的内存区域这句话并不彻底正确”。这其实都不重要,重要的是当提到堆内存、提到线程共享、提到对象内存分配的时候,你能够想到还有个TLAB是比较特殊的,就能够了。
有些时候,最可怕的不是本身不知道,而是,不知道本身不知道。
还有就是,TLAB只是HotSpot虚拟机的一个优化方案,Java虚拟机规范中也没有关于TLAB的任何规定。因此,不表明全部的虚拟机都有这个特性。
本文的概述都是基于HotSpot虚拟机的,做者也不是故意“以偏概全”,而是由于HotSpot虚拟机是目前最流行的虚拟机了,大多数默认状况下,咱们讨论的时候也都是基于HotSpot的。
哎,每次写一些技术文章,都会有不少人喷,喷的角度也都是千奇百怪,因此只好多说几句找补找补了。Anyway,任何形式的讨论仍是欢迎的,由于即便是喷,也未必有对手!