Java堆内存是线程共享的!面试官:你确定吗?
Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解。可以说,关于JVM的相关知识,基本是每个Java开发者必学的知识点,也是面试的时候必考的知识点。
在JVM的内存结构中,比较常见的两个区域就是堆内存和栈内存(如无特指,本文提到的栈均指的是虚拟机栈),关于堆和栈的区别,很多开发者也是如数家珍,有很多书籍,或者网上的文章大概都是这样介绍的:
1、堆是线程共享的内存区域,栈是线程独享的内存区域。
2、堆中主要存放对象实例,栈中主要存放各种基本数据类型、对象的引用。
但是,作者可以很负责任的告诉大家,以上两个结论均不是完全正确的。
本文首先带大家了解一下为什么我会说“堆是线程共享的内存区域,栈是线程独享的内存区域。”这句话并不完全正确!?关于JVM内存结构的相关知识,大家可以阅读JVM内存结构 VS Java内存模型 VS Java对象模型、万万没想到,JVM内存结构的面试题可以问的这么难?等文章。
在开始进入正题之前,请允许我问一个和这个问题看似没有任何关系的问题:Java对象的内存分配过程是如何保证线程安全的?
Java对象的内存分配过程是如何保证线程安全的?
我们知道,Java是一门面向对象的语言,我们在Java中使用的对象都需要被创建出来,在Java中,创建一个对象的方法有很多种,但是无论如何,对象在创建过程中,都需要进行内存分配。
对象的内存分配过程中,主要是对象的引用指向这个内存区域,然后进行初始化操作。
但是,因为堆是全局共享的,因此在同一时间,可能有多个线程在堆上申请空间,那么,在并发场景中,如果两个线程先后把对象引用指向了同一个内存区域,怎么办。
为了解决这个并发问题,对象的内存分配过程就必须进行同步控制。但是我们都知道,无论是使用哪种同步方案(实际上虚拟机使用的可能是CAS),都会影响内存的分配效率。
而Java对象的分配是Java中的高频操作,所有,人们想到另外一个办法来提升效率。这里我们重点说一个HotSpot虚拟机的方案:
每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。
这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。
什么是TLAB
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并不是就没有任何问题的。
前面我们说过,因为TLAB内存区域并不是很大,所以,有可能会经常出现不够的情况。在《实战Java虚拟机》中有这样一个例子:
比如一个线程的TLAB空间有100KB,其中已经使用了80KB,当需要再分配一个30KB的对象时,就无法直接在TLAB中分配,遇到这种情况时,有两种处理方案:
1、如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则直接在堆内存中对该对象进行内存分配。
2、如果一个对象需要的空间大小超过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使用的相关参数
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,任何形式的讨论还是欢迎的,因为即使是喷,也未必有对手!
Java堆内存是线程共享的!面试官:你确定吗?的更多相关文章
- java堆内存详解
http://www.importnew.com/14630.htmljava堆的特点<深入理解java虚拟机>是什么描述java堆的 Java堆(Java Heap)是java虚拟机所管 ...
- Java堆内存溢出模拟
先了解一下Java堆: 关于Java内存区域的分配,可以查看Java运行时数据区域一篇文章. Java堆是虚拟机内存管理中最大的一块区域,该区域是线程共享的,某Java进程中所有的线程都可以访问该区域 ...
- 关于java堆内存溢出的几种情况(转)
[情况一]: java.lang.OutOfMemoryError: Java heap space:这种是java堆内存不够,一个原因是真不够,另一个原因是程序中有死循环: 如果是java堆内存不够 ...
- Java堆内存又溢出了!教你一招必杀技
JAVA堆内存管理是影响性能主要因素之一.堆内存溢出是JAVA项目非常常见的故障,在解决该问题之前,必须先了解下JAVA堆内存是怎么工作的. 先看下JAVA堆内存是如何划分的,如图: JVM内存划分为 ...
- MAT工具定位分析Java堆内存泄漏问题方法
一.MAT概述与安装 MAT,全称Memory Analysis Tools,是一款分析Java堆内存的工具,可以快速定位到堆内泄漏问题.该工具提供了两种使用方式,一种是插件版,可以安装到Eclips ...
- Java堆内存的十个要点
Java中的堆空间是什么? 当Java程序开始运行时,JVM会从操作系统获取一些内存.JVM使用这些内存,这些内存的一部分就是堆内存.堆内存通常在存储地址的底层,向上排列.当一个对象通过new关键字或 ...
- (2)java堆内存
java堆内存结构图 [名词解释]--->eden,so,s1通称为新生代对象储区--->tenured称为老年代对象存储区--->s0和s1也称为from和to区域,是两块大小相等 ...
- Java堆内存不足
1)使用IDEA开发程序时有时候会提示“Java Heap space error”,说明IDEA默认配置的Java堆内存不足,程序需要更多的堆内存. 2)堆(Heap)和非堆(Non-heap)内存 ...
- Java堆内存划分
根据对象的存活率(年龄)Java堆内存划分为3种,新生代,老年代,永久代: 1.新生代 比如我们在方法中区new一个对象,那这方法调用完毕后,对象就会被回收,这就是一个典型的新生代对象. 现在的商业虚 ...
随机推荐
- Caused by: java.io.FileNotFoundException: class path resource [../../resources/config/spring.xml] cannot be opened because it does not exist
在尝试使用Spring的Test的时候遇到了这个错误 原来的代码: @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loca ...
- EXAM-2018-7-29
EXAM-2018-7-29 未完成 [ ] H [ ] A D 莫名TLE 不在循环里写strlen()就行了 F 相减特判 水题 J 模拟一下就可以发现规律,o(n) K 每个数加一减一不变,用m ...
- ABC:Meaningful Mean
题目描述 You are given an integer sequence of length N, a= {a1,a2,…,aN}, and an integer K. a has N(N+1)⁄ ...
- php7 安装mongodb扩展
下载 mongodb-1.6.0.tgz wget https://pecl.php.net/get/mongodb-1.6.0.tgz 版本太低的话有些语法不一样,起码1.5以上吧 进入 mo ...
- 树莓派搭建钓鱼wifi热点
我们连接的公共wifi其实是非常不安全的网络,骇客可以利用wifi路由设备进行中间人攻击,劫持DNS伪造钓鱼网站.接下来我会做个简单的实验,伪造中国电信的路由ChinaNet并发射出热点wifi等待别 ...
- ccpc20190823
04 http://acm.hdu.edu.cn/showproblem.php?pid=6705 分析:先把每条边以 形式放进堆,堆按路径权值从小到大排序,然后每次取出堆顶,用v的出边扩展 新的路径 ...
- 收集到的技术相关网址——delphi
1.DLL封装登录框架实现代码复用 https://www.cnblogs.com/wenwencao/articles/1333659.html
- PowerDesign15连接数据库并反向表结构详细
10.0 sql2008 11.0 sql2010 这的服务器名称,复制到上面 这里的默认数据库不要改.Powerdesign会把自已的数据也存在默认的数据库里 一般不用改 测试 成功 这里ODBC的 ...
- Mac下通过FFMpeg实现Android手机推流和播放
一.Mac下搭建推流服务器(Nginx+RTMP+FFMpeg) 安装x264 git clone git://git.videolan.org/x264.git cd x264 ./configur ...
- 自定义servlet重写doGet或doPost方法是如何实现多态的
我们知道,如果我们自定义一个servlet继承HttpServlet,并且重写HttpServlet中的doGet或doPost方法,那么从浏览器发送过来的request请求将调用HttpServle ...