写点什么

【JVM】HotspotJVM 对象的分配策略, 栈上分配与 TLAB

  • 2022 年 8 月 30 日
    上海
  • 本文字数:4837 字

    阅读完需:约 16 分钟

前言

📫作者简介小明java问道之路,专注于研究计算机底层/Java/Liunx 内核,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计📫 

🏆 InfoQ 签约作者、CSDN 专家博主/Java 领域优质创作者/CSDN 内容合伙人、阿里云专家/签约博主、华为云专家、51CTO 专家/TOP 红人 🏆

🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~


本文导读

​前面我们学习了 JVM 内存区域、JVM 中的对象及引用,这节首先大家想一个问题:平时写代码需要去编写对象被分配在内存的什么位置了吗?是的,就像是不需要考虑垃圾回收具体什么时间点回收,JVM 已经自动进行内存管理了,JVM 这么做也是有原因的。

一、Java 自动内存管理概述

Java 所支持的自动内存管理针对的是对象内存的自动分配和回收,原因如下:

1、在 Java 的内存区域中,本地方法栈、虚拟机栈、程序计数器这三块内存区域的分配和回收具有确定性,他们在编译阶段就能确定需要分配的空间大小。此外,这些内存区域属于线程私有,随线程生而生,随线程灭而灭。综上,虚拟机不需要在这部分内存区域花费太多精力用于垃圾回收。2、方法区存储的是类信息、静态变量、常量、即时编译器编译过的代码,这部分数据的回收条件较为苛刻,垃圾回收的“成绩”并不是那么令人满意,因此不是垃圾收集器需要重点关注的区域。3、Java 堆存储所有线程的对象,这些对象内存空间的分配是在程序运行期间才进行的,因此具有不确定性。此外,对象的生命周期长短不一,为了提高垃圾收集的效率,需要针对不同生命周期的对象设置不同的垃圾收集算法,这也就增加了内存管理的复杂度。

二、对象分配策略

1、对象优先在 Eden 区分配

大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机将发起一次 Minor GC。

2、大对象直接进入老年代

大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到“朝生夕灭”的“短命大对象”,我们写程序的时候应注意避免。在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们。而当复制对象时,大对象就意味着高额的内存复制开销。HotSpot 虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作。这样做的目的:1.避免大量内存复制,2.避免提前进行垃圾回收,明明内存有空间进行分配。PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效。-XX:PretenureSizeThreshold=4m

3、长期存活对象进入老年区

HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1,当它的年龄增加到一定程度(并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代中。-XX:MaxTenuringThreshold 调整。

4、空间分配担保

在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 MinorGC 可以确保是安全 的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。

如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 MinorGC,尽管这次 MinorGC 是有风险的(因为判断的是平均大小,有可能这次的晋升对象比平均值大很多),如果担保失败则会进行一次 FullGC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 FullGC。

在《深入理解 Java 虚拟机》书中,有这么一句话:“对于大多数应用来说,Java 堆是 Java 虚拟机所管理的内存中最大的一块 Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在堆上分配”。这里没有说所有的对象都在堆上进行分配,而是使用了“几乎所有”一词进行描述,那么今天就来简单聊一聊,除了堆以外的对象分配。

对 Java 对象分配的过程进行了分析,分析后可知为了解决线程安全问题并且提高效率,有另外两个地方也是可以存放对象的这两个地方分别是栈和 TLAB。

三、栈上分配

再问一个问题:如果确定一个对象的作用域不会逃逸出方法之外,那可不可以将这个对象分配在栈上?这样的话,对象所占用的内存空间就可以随着栈帧的出栈而销毁。而且,在一般应用中,不会逃逸的局部对象所占的比例很大,所以如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以还可以减小垃圾收集器的负载。

分析完以后给出栈上分配官方定义:JVM 允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。栈上分配只是 JVM 虚拟机提供的一种优化技术,对象主要还是分配在堆上的

1、逃逸分析

栈上分配也是有前提的,并不是所有的对象都可以栈上分配,首先需要进行逃逸分析的,所以逃逸分析是栈上分配的技术基础那什么是逃逸分析呢?逃逸分析是指判断对象的作用域是否有可能逃逸出函数体,关于具体的逃逸分析算法和技术此篇不讨论 Java SE 6u23 版本之后,HotSpot 中默认就开启了逃逸分析,可以通过选项-XX:+PrintEscapeAnalysis 查看逃逸分析的筛选结果。

如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。

逃逸分析的几种情况:

public class EscapeAnalysisTest {    static V global_v;    public void a_method() {        V v = b_method();        c_method();    }    public V b_method() {        V v = new V();        return v;    }    public void c_method() {        global_v = new V();    }}
复制代码

​采用了逃逸分析后,满足逃逸的对象在栈上分配

/** * 逃逸分析-栈上分配 * -XX:-DoEscapeAnalysis */public class EscapeAnalysisTest {    private static class Stu {        String a;        int b;        public Stu(String a, int b) {            this.a = a;            this.b = b;        }    }    public static void alloc() {        Stu stu = new Stu("小明", 22);    }    public static void main(String[] args) {        long b = System.currentTimeMillis();        for (int i = 0; i < 100000000; i++) {            alloc();        }        long e = System.currentTimeMillis();        System.out.println(e - b);    }}
复制代码

运行结果:没有开启逃逸分析,对象都在堆上分配,会频繁触发垃圾回收(垃圾回收会影响系统性能),导致代码运行慢。

// 1、参数为:-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC[GC (Allocation Failure)  2048K->728K(9728K), 0.0017996 secs][GC (Allocation Failure)  2776K->696K(9728K), 0.0013323 secs]10
// 2、参数为:-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC[GC (Allocation Failure) 2760K->712K(9728K), 0.0004889 secs]...疯狂GC[GC (Allocation Failure) 2760K->712K(9728K), 0.0004889 secs][GC (Allocation Failure) 2760K->712K(9728K), 0.0003785 secs][GC (Allocation Failure) 2760K->712K(9728K), 0.0008545 secs]1955
复制代码

​​总结:

1、栈上分配可以提升代码性能,降低在多线程情况下的锁使用,但是会受限于其空间的大小。

2、进行逃逸分析之后,产生的后果是所有的对象都将由栈上分配,而非从 JVM 内存模型中的堆来分配。

3、栈上分配可以提升代码性能,降低在多线程情况下的锁使用,但是会受限于其空间的大小。

4、分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。

5、能在方法内创建对象,就不要再方法外创建对象。

四、TLAB(线程本地分配缓冲)

TLAB 的全称是 Thread Local Allocation Buffer,即线程本地分配缓存区。

为什么需要 TLAB?创建对象时,需要在堆上为新生的对象申请指定大小的内存,如果同时有大量线程申请内存的话,可以通过锁机制确保不会申请到同一块内存,在 JVM 运行中,内存分配是一个极其频繁的动作,使用锁这种方式势必会降低性能。所以就出现了 TLAB,JVM 通过使用 TLAB 来避免多线程冲突,每个线程使用自己的 TLAB,这样就保证了不使用同步,也不会出现线程安全问题,提高了对象分配的效率。(为新对象分配内存空间时,让每个 Java 应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。)

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 区进行申请。

总结

对象分配流程图


相信大家已经掌握 JVM 是如何自动进行内存管理,本文适合点赞+收藏。有什么错误希望大家直接指出~

发布于: 11 小时前阅读数: 3
用户头像

InfoQ签约作者/技术专家/博客专家 2020.03.20 加入

🏆InfoQ签约作者、CSDN专家博主/Java领域优质创作者、阿里云专家/签约博主、华为云专家、51CTO专家/TOP红人 📫就职某大型金融互联网公司高级工程师 👍专注于研究Liunx内核、Java、源码、架构、设计模式、算法

评论

发布
暂无评论
【JVM】HotspotJVM对象的分配策略,栈上分配与TLAB_8月月更_小明Java问道之路_InfoQ写作社区