jvm 内存分配性能提升之——逃逸分析与tlab
Posted 东海陈光剑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jvm 内存分配性能提升之——逃逸分析与tlab相关的知识,希望对你有一定的参考价值。
柏拉图说过:思想永远是宇宙的统治者。只要思想不滑坡,办法总比困难多。
Java从最开始被诟病速度慢,到现在执行速度直追C语言。这些运行时优化是必不可少的。还记得我们之前讲的逃逸分析是怎么回事吗?
jvm 分配内存
当类已经被加载完毕了,那么会执行第二步,也就是分配内存。我们都知道new对象一般情况来说生成的对象都是会存放在堆当中(当存在栈上分配时,逃逸对象会优先分配在栈当中),那么存放肯定是需要内存空间去存放对象的。这里就涉及到两个问题。
问题1 如何分配内存?
分配内存的方式有两种。第一种是指针碰撞,第二种是空闲列表。
指针碰撞
指针碰撞的前提条件是堆中的内存是规整的,也就是说没有内存碎片的产生。因为对象实际上是以连续的内存空间去存放的。所以,当内存规整的时候,通过指针碰撞的方式就可以更加充分的利用内存。
堆中内存是绝对规整的,所有用过的内存都被放在了一边,没有用过的内存放在另外一边。中间通过一个指针来进行划分。当有新new的对象要在堆中划分内存时,这个指针会向空闲内存空间偏移一段可以存放下新对象的内存地址,然后再将新的对象存放到刚刚划分出来的新的内存空间当中。
空闲列表
空闲列表的方式是在内存不规整的情况下的一种内存的分配方式。
空闲列表是指,堆中可用空间跟已经使用的空间都相互交错,就没有办法通过指针碰撞这种方式来进行内存分配。这个时候虚拟机会维护一个列表去记录堆当中大大小小的可用内存空间,当新的对象需要进来分配内存空间的时候,会从空闲列表中找到一块能够存放进新对象的内存区域去存放对象,并且更新空闲列表的记录。
通过这两种方式,我们了解了JVM分配内存的机制。但是这里有一个问题,我们从一开始就在讨论规整的内存与不规整的内存的内存分配方式,但是大家有没有想过堆中的内存规整不规整这个又是由什么导致的呢?
其实这个跟使用的垃圾回收器有关。关于垃圾回收器的我会另外再开一篇文章来讲。
问题2 并发情况下如何去处理内存分配?
创建对象肯定是会发生并发情况的,当某个线程调用的方法在创建对象的时候,他并不知道这个时候会不会有其他线程在这个时候恰巧也在创建对象。这就会产生并发争抢内存的现象。
JVM针对这种现象也给出了相应的解决措施,一种是CAS,另外一种则是TLAB。
CAS(compare and swap)
通过CAS + 失败重试,保证以原子性的方式来对分配内容的动作进行同步处理。
TLAB(Thread Local Allocation Buffer)
TLAB翻译过来叫做本地线程分配缓冲区。是指把内存分配的执行按照线程划分到不同的空间之中进行,也就是说每个开启的线程都会在堆中事先分配一小块内存空间,用这一块空间来存放对象。也就避免的多个线程同时分配对象内存的资源争抢的问题。
JVM默认是开启了TLAB
可以通过 -XX:+/-UseTLAB来决定开启或者关闭,还可以通过-XX:TLABSize指定每个线程的缓冲区大小。
逃逸分析:单线程中new对象的栈上分配
如果一个对象的分配是在方法内部,并且没有多线程访问的情况下,那么这个对象其实可以看做是一个本地对象,这样的对象不管创建在哪里都只对本线程中的本方法可见,因此可以直接分配在栈空间中。
栈上分配的对象因为不用考虑同步,所以执行速度肯定会更加快速,这也是为什么JVM会引入栈上分配的原因。
逃逸分析不在公共空间分配这个对象,而是在私人的栈空间中分配。
逃逸分析还有一个作用就是lock coarsening。
同样的,单线程环境中,锁也是不需要的,也可以优化掉。
多线程环境中new对象之线程本地分配缓存区:TLAB
TLAB简介
小师妹:师兄,我觉得逃逸分析很好呀,栈上分配也不错。既然又这么厉害的两项技术了,为什么还要用到TLAB呢?
首先这是两个不同的概念,TLAB的全称是Thread-Local Allocation Buffers。Thread-Local大家都知道吧,就是线程的本地变量。而TLAB则是线程的本地分配空间。
逃逸分析和栈上分配只是争对于单线程环境来说的,如果在多线程环境中,不可避免的会有多个线程同时在堆空间中分配对象的情况。
这种情况下如何处理才能提升性能呢?
小师妹:哇,多个线程竞争共享资源,这不是一个典型的锁和同步的问题吗?
锁和同步是为了保证整个资源一次只能被一个线程访问,我们现在的情况是要在资源中为线程划分一定的区域。这种操作并不需要完全的同步,因为heap空间够大,我们可以在这个空间中划分出一块一块的小区域,为每个线程都分一块。这样不就解决了同步的问题了吗?这也可以称作空间换时间。
TLAB详解
小师妹,还记得heap分代技术中的一个中心两个基本点吗?哦,1个Eden Space和2个Suvivor Space吗?
Young Gen被划分为1个Eden Space和2个Suvivor Space。当对象刚刚被创建的时候,是放在Eden space。垃圾回收的时候,会扫描Eden Space和一个Suvivor Space。如果在垃圾回收的时候发现Eden Space中的对象仍然有效,则会将其复制到另外一个Suvivor Space。
就这样不断的扫描,最后经过多次扫描发现任然有效的对象会被放入Old Gen表示其生命周期比较长,可以减少垃圾回收时间。
因为TLAB关注的是新分配的对象,所以TLAB是被分配在Eden区间的,从图上可以看到TLAB是一个一个的连续空间。
然后将这些连续的空间分配个各个线程使用。
因为每一个线程都有自己的独立空间,所以这里不涉及到同步的概念。
jvm默认情况下TLAB是开启的,你可以通过:
-XX:-UseTLAB
来关闭它。
设置TLAB空间的大小
小师妹,F师兄,这个TLAB的大小是系统默认的吗?我们可以手动控制它的大小吗?
要解决这个问题,我们还得去看JVM的C++实现,也就是threadLocalAllocBuffer.cpp:
上面的代码可以看到,如果设置了TLAB(默认是0),那么TLAB的大小是定义的TLABSize除以HeapWordSize和max_size()中最小的那个。
HeapWordSize是heap中一个字的大小,我猜它=8。别问我为什么,其实我也是猜的,有人知道答案的话可以留言告诉我。
TLAB的大小可以通过:
-XX:TLABSize
来设置。
如果没有设置TLAB,那么TLAB的大小就是分配线程的平均值。
TLAB的最小值可以通过:
-XX:MinTLABSize
来设置。
默认情况下:
-XX:ResizeTLAB
resize开关是默认开启的,那么JVM可以对TLAB空间大小进行调整。
TLAB中大对象的分配
小师妹:F师兄,我想到了一个问题,既然TLAB是有大小的,如果一个线程中定义了一个非常大的对象,TLAB放不下了,该怎么办呢?
好问题,这种情况下又有两种可能性,我们假设现在的TLAB的大小是100K:
第一种可能性:
目前TLAB被使用了20K,还剩80K的大小,这时候我们创建了一个90K大小的对象,现在90K大小的对象放不进去TLAB,这时候需要直接在heap空间去分配这个对象,这种操作实际上是一种退化操作,官方叫做 slow allocation。
第二中个可能性:
目前TLAB被使用了90K,还剩10K大小,这时候我们创建了一个15K大小的对象。
这个时候就要考虑一下是否仍然进行slow allocation操作。
因为TLAB差不多已经用完了,为了保证后面new出来的对象仍然可以有一个TLAB可用,这时候JVM可以尝试将现在的TLAB Retire掉,然后分配一个新的TLAB空间,把15K的对象放进去。
JVM有个开关,叫做:
-XX:TLABWasteTargetPercent=N
这个开关的默认值是1。表示如果新分配的对象大小如果超出了设置的这个百分百,那么就会执行slow allocation。否则就会分配一个新的TLAB空间。
同时JVM还定义了一个开关:
-XX:TLABWasteIncrement=N
为了防止过多的slow allocation,JVM定义了这个开关(默认值是4),比如说第一次slow allocation的极限值是1%,那么下一次slow allocation的极限值就是%1+4%=5%。
TLAB空间中的浪费
小师妹:F师兄,如果新分配的TLAB空间,那么老的TLAB中没有使用的空间该怎么办呢?
这个叫做TLAB Waste。因为不会再在老的TLAB空间中分配对象了,所以剩余的空间就浪费了。
以上是关于jvm 内存分配性能提升之——逃逸分析与tlab的主要内容,如果未能解决你的问题,请参考以下文章