当前位置: 首页 > 科技观察

灵魂拷问:Java对象的内存分配过程是如何保证线程安全的?

时间:2023-03-17 13:25:52 科技观察

JVM内存结构是非常重要的知识。相信每个准备过面试的程序员都能把堆、栈、方法区等介绍清楚。上图是笔者根据《Java虚拟机规范(Java SE 8)》中描述的JVM运行时内存区结构绘制的一张图。很多人都知道Java对象是在堆内存中分配的(JIT优化除外),内存分配过程是线程安全的,那么虚拟机是如何保证线程安全的呢?本文将对其进行简要介绍。一、Java对象的内存分配我们知道Java是一门面向对象的语言,我们在Java中使用的对象都是需要创建的。在Java中,创建对象的方法有很多种,比如使用new、使用Reflection、使用Clone方法等,但不管怎样,在创建对象时都需要分配内存。以最常见的新关键字为例。当我们使用new创建一个对象,代码开始运行时,虚拟机在执行这条new指令时,会先检查要new的对象对应的类是否已经加载。加载首先由类加载执行。类加载检查通过后,需要为对象分配内存,分配的内存主要用于存储对象的实例变量。在进行内存分配时,需要根据对象中的实例变量等信息来确定要分配的空间大小,然后从Java堆中划分这样一块区域(假设没有进行JIT优化)。根据JVM使用的垃圾收集器类型不同,其回收算法不同,堆中内存的分配也会不同。比如通过mark-clear算法回收的内存中会出现大量不连续的内存碎片。在分配新的对象时,需要通过“空闲列表”来确定一个空闲区域。(这部分不是本文的重点,读者可以自行学习。)不管是哪种方式,最后都需要确定一块内存区域,为新的对象分配内存。我们知道,在对象的内存分配过程中,对象的引用主要是指向这块内存区域,然后进行初始化操作。那么问题来了:在并发场景下,内存分配过程的线程安全性如何?如果两个线程连续将对象引用指向同一个内存区域会怎样。2、TLAB一般有两种解决方案:1、同步分配内存空间的动作,采用CAS机制,配合失败重试的方法,保证更新操作的线程安全。2、每个线程在Java堆中预先分配一小块内存,然后直接在自己的“私有”内存中为对象分配内存。当这部分区域用完后,再分配一块新的“私有”内存。方案一需要每次分配都同步控制,效率比较低。方案二在HotSpot虚拟机中采用,这种方案称为TLAB分配,即ThreadLocalAllocationBuffer,这部分Buffer是从堆中划分出来的,但是它是本地线程独占的,这里值得注意的是,当我们说TLAB是线程独占的时候,它只是线程独占在“分配”的动作中。至于读、垃圾回收等动作,都是线程共享的。使用上没有区别。另外,TLAB只作用于新生代的EdenSpace。当对象被创建时,它们首先放在这个区域,但是在新生代无法分配内存的大对象会直接进入老年代。因此,在编写Java程序时,分配多个小对象通常比分配多个小对象效率更高大物体。所以,虽然对象一开始可能会通过TLAB分配内存,存放在Eden区,但还是会被垃圾回收或者移动到SurvivorSpace,OldGen等,不知道大家有没有想过。我们使用TLAB之后,给TLAB上的对象分配内存的时候,线程是独占的,所以不会有冲突。但是,从堆中划分TLAB内存的过程也可能存在内存安全问题。因此,在分配TLAB的过程中,仍然需要进行同步控制。但是这个开销比每次为单个对象分配内存时的同步控制要低很多。虚拟机是否使用TLAB是可选的,可以通过设置-XX:+/-UseTLAB参数来指定。3.总结为了保证Java对象内存分配的安全,提高效率,每个线程可以在Java堆中预分配一小块内存。这部分内存称为TLAB(ThreadLocalAllocationBuffer)。这块内存的分配是线程独享的,读取、使用、回收都是线程共享的。您可以通过设置-XX:+/-UseTLAB参数来指定是否启用TLAB分配。【本文为专栏作家霍利斯原创文章,作者微信公众号Hollis(ID:hollishuang)】点此阅读更多本作者好文