上篇文章说了,对象有可能会分配栈上,这篇文章就来详细说一下java的内存分配原则。

  1. 先来说说栈上分配
    栈上分配有两个技术基础:
  1. 逃逸分析
    定义:在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。
  2. 标量替换
    允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配
    所以栈上分配的过程就是 先通过逃逸分析 分析出没有逃逸的对象,然后使用标量替换把标量分配到栈上,分配到栈上之后,随着方法栈帧的弹出,标量也会释放。
  1. TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区。

创建对象时,需要在堆上为新生的对象申请指定大小的内存,如果同时有大量线程申请内存的话,可以通过锁机制确保不会申请到同一块内存,在JVM运行中,内存分配是一个极其频繁的动作,使用锁这种方式势必会降低性能。

所以就出现了TLAB,JVM通过使用TLAB来避免多线程冲突,每个线程使用自己的TLAB,这样就保证了不使用同步,也不会出现线程安全问题,提高了对象分配的效率。

TLAB本身占用eden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。

TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满,就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。

TLAB空间由于比较小,因此很容易装满。比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前TLAB,这样就会浪费20KB空间;第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。实际上虚拟机内部会维护一个叫作refill_waste的值,通俗一点来说就是可允许浪费空间的值,当TLAB剩余的空间小于新申请对象的大小,且这个剩余的空间大于refill_waste(可允许浪费空间的值)时,会选择在堆中分配(保留当前的TLAB);若剩余的空间小于refill_waste(可允许浪费空间的值)时,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。

再举两个通俗易懂的例子帮助理解:大家可以花两分钟时间跟着下边的例子算一下,算完后,对refill_waste会有更到位的理解

假设TLAB大小为100KB,refill_waste(可允许浪费空间的值)为5KB
  1、假如当前TLAB已经分配96KB,还剩下4KB,但是现在new了一个对象需要6KB的空间,显然TLAB的内存不够了,这时可以简单的重新申请一个TLAB,原先的TLAB交给Eden管理,这时只浪费4KB的空间,在refill_waste 之内。
  2、假如当前TLAB已经分配90KB,还剩下10KB,现在new了一个对象需要11KB,显然TLAB的内存不够了,这时就不能简单的抛弃当前TLAB,因为此时抛弃的话,就会浪费10KB的空间,10KB是大于咱们设置的refill_waste(可允许浪费空间的值)5KB的,所以此时会保留当前的TLAB不动,会把这11KB会被安排到Eden区进行申请。

由于栈上分配和TLAB分配,没有线程的竞争。所以会有锁消除,即如果程序中使用了synchronized锁,则JVM会将synchronized锁消除。需要注意的是:这种情况针对的是synchronized锁,而对于Lock锁,则JVM并不能消除。