JVM对象分配流程,如何保证线程安全?

背景

了解垃圾回收器,除了要知道垃圾回收器的工作原理,我们还要想一个问题,垃圾回收器的垃圾是怎么产生的。所以本期我们讲解一些垃圾回收器的对象内存分配原则。结合这些分配原则,帮助大家在面试时能更好的和面试官吹牛皮。

分配规则

先说一个大的原则,一般的对象会被分配在新生代Eden区,然后经过垃圾回收存活一定时间后进入老年代。这个原则也符合我们的对内存的分带收集理论。但是并不绝对,有一些特例。下面我将讲解这些特例。

大对象直接进入老年代

所谓大对象,就是指很长的字符串或者元素众多的数组。大对象的分配需要连续的内存空间,如果直接分配在新生代,那么在新生代垃圾回收时需要反复的拷贝这个对象,而反复的大内存的拷贝会严重影响GC性能。实际上,垃圾回收过程里,最耗时的就是内存的拷贝过程。所以为了避免这个问题,JVM要求超过一定容量的对象直接分配在老年代,使用参数-XX:PretenureSizeThreshold设置,这个参数的单位是字节数。

长期存活的对象直接进入老年代

当对象经过一定的生存周期,那么对象就可以从新生代晋升为老年代,这个规则是显而易见的,JVM默认是存活15次GC的对象会晋升老念叨。可以使用JVM参数-XX:MaxTenuringThreshold设置

动态年龄

我们知道JVM是很"聪明"的,能够自适应的调节一些参数。对象晋升老年代也是,如果一次S区中,某个年龄的对象占据S区的一半,那么年龄大于等于该年龄的对象,直接晋升老年代。

内存分配担保

所谓内存担保,是指在老年代预留出内存保证新生代的垃圾回收能够进行。

在发生Minor GC之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立,那么Minor GC就是安全的。否则就检查是否允许担保失败,如果允许再检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则发生Monir GC,如果小于,或者之前设置不允许担保失败,就发生Full GC。

之所以要这么做,是因为存在一种情况,假设Minor GC时,Eden区所有对象都是存活的,需要拷贝到S区,而S区的容量我们知道是不足以容纳Eden区的的,所以需要把对象拷贝到老年代。如果这时老年代没有足够的内存空间,那么本次GC就会失败,所以需要内存分配担保机制。

Java对象的内存分配

我们知道,Java是一门面向对象的语言,我们在Java中使用的对象都需要被创建出来,在Java中,创建一个对象的方法有很多种,如使用new、使用反射、使用Clone方法等,但是无论如何,对象在创建过程中,都需要进行内存分配。

拿最常见的new关键字举例,当我们使用new创建对象后代码开始运行后,虚拟机执行到这条new指令的时候,会先检查要new的对象对应的类是否已被加载,如果没有被加载则先进行类加载。

在类加载检查通过之后,就需要给对象进行内存分配了,分配的内存主要用来存放对象的实例变量。

在进行内存分配时,需要根据对象中的实例变量情况等信息确定需要分配的空间大小,然后从Java堆中划分出这样一块区域(假设没有JIT优化)。

根据JVM使用的垃圾回收器的类型,因其回收算法不同,会导致堆中内存分配情况不同。如标记-清楚算法回收后的内存中会有大量不连续的内存碎片,在给新的对象分配的时候,就需要通过"空闲列表"来确定一块空闲区域。(这部分不是本文重点,读者可以自行学习一下。)

无论那种方式,最终都需要确定出一块内存区域,用于给新建对象分配内存。我们知道,对象的内存分配过程中,主要是对象的引用指向这个内存区域,然后进行初始化操作。

那么问题就来了:

在并发场景中,如何内存分配过程的线程安全性?如果两个线程先后把对象引用指向了同一个内存区域,怎么办。

TLAB

一般有两种解决方案:

  • 1、对分配内存空间的动作做同步处理,采用CAS机制,配合失败重试的方式保证更新操作的线程安全性。

  • 2、每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配,当这部分区域用完之后,再分配新的"私有"内存。

方案1在每次分配时都需要进行同步控制,这种是比较低效的。

方案2是HotSpot虚拟机中采用的,这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。

这里值得注意的是,我们说TLAB时线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。

另外,TLAB仅作用于新生代的Eden Space,对象被创建的时候首先放到这个区域,但是新生代分配不了内存的大对象会直接进入老年代。因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。

所以,虽然对象刚开始可能通过TLAB分配内存,存放在Eden区,但是还是会被垃圾回收或者被移到Survivor Space、Old Gen等。

不知道大家有没有想过,我们使用了TLAB之后,在TLAB上给对象分配内存时线程独享的了,这就没有冲突了,但是,TLAB这块内存自身从堆中划分出来的过程也可能存在内存安全问题啊。

所以,在对于TLAB的分配过程,还是需要进行同步控制的。但是这种开销相比于每次为单个对象划分内存时候对进行同步控制的要低的多。

虚拟机是否使用TLAB是可以选择的,可以通过设置-XX:+/-UseTLAB参数来指定。

总结

为了保证Java对象的内存分配的安全性,同时提升效率,每个线程在Java堆中可以预先分配一小块内存,这部分内存称之为TLAB(Thread Local Allocation Buffer),这块内存的分配时线程独占的,读取、使用、回收是线程共享的。

可以通过设置-XX:+/-UseTLAB参数来指定是否开启TLAB分配。

你可能感兴趣的:(jvm,java,算法)