【JVM深度解析】对象的分配策略栈上分配与TLAB

简介: JVM是如何自动进行内存管理的呢?本文详细对象的分配策略,栈上分配与TLAB,相信相信大家看完已经掌握JVM是如何管理,本文适合点赞+收藏。

 JVM是如何自动进行内存管理的呢?本文详细对象的分配策略,栈上分配与TLAB,相信相信大家看完已经掌握JVM是如何管理,本文适合点赞+收藏。有什么错误希望大家直接指出~

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

Java自动内存管理概述

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

1、在Java的内存区域中,本地方法栈、虚拟机栈、程序计数器这三块内存区域的分配和回收具有确定性,他们在编译阶段就能确定需要分配的空间大小。此外,这些内存区域属于线程私有,随线程生而生,随线程灭而灭。综上,虚拟机不需要在这部分内存区域花费太多精力用于垃圾回收。

2、方法区存储的是类信息、静态变量、常量、即时编译器编译过的代码,这部分数据的回收条件较为苛刻,垃圾回收的“成绩”并不是那么令人满意,因此不是垃圾收集器需要重点关注的区域。

3、Java堆存储所有线程的对象,这些对象内存空间的分配是在程序运行期间才进行的,因此具有不确定性。此外,对象的生命周期长短不一,为了提高垃圾收集的效率,需要针对不同生命周期的对象设置不同的垃圾收集算法,这也就增加了内存管理的复杂度。

对象分配策略

对象优先在 Eden 区分配

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

大对象直接进入老年代

大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。

大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到“朝生夕灭”的“短命大对象”,我们写程序的时候应注意避免。

在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们。而当复制对象时,大对象就意味着高额的内存复制开销。

HotSpot 虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor区之间来回复制,产生大量的内存复制操作。

这样做的目的:1.避免大量内存复制,2.避免提前进行垃圾回收,明明内存有空间进行分配。

PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效。-XX:PretenureSizeThreshold=4m

长期存活对象进入老年区

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

空间分配担保

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

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

image.gif编辑

BUT

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

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

栈上分配

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

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

逃逸分析

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

image.gif编辑

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

逃逸分析的几种情况:

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();
    }
}

image.gif

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

/**
 * 逃逸分析-栈上分配
 * -XX:-DoEscapeAnalysis
 *
 * @author macfmc
 * @date 2020/8/1-19:57
 */
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);
    }
}

image.gif

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

// 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

image.gif

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

总结

名称 针对点 处于对象分配流程的位置
栈上分配 避免gc无谓负担 1
TLAB 加速堆上对象的分配

2

对象分配流程图

image.gif编辑

image.gif编辑

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

参考与致谢:用大白话来聊一聊Java对象的栈上分配和TLAB享学课堂第三期JVM


相关文章
|
1月前
|
人工智能 搜索推荐 安全
打造精准营销!营销电子邮件以客户为中心策略解析!
营销电子邮件是数字营销的核心,用于建立客户关系、推广产品和服务,提高品牌忠诚度和转化率。它们在客户旅程中扮演关键接触点角色,如欢迎邮件、购物车提醒和个性化推荐。电子邮件营销能提升品牌知名度,细分营销可带来760%的收入增长。然而,大量邮件可能导致邮件过载,缺乏个性化可能引起收件人反感,甚至网络安全问题。收件人和IT团队可通过过滤、优化设置、启用2FA等措施改善体验。营销团队则需克服管理、个性化和法规遵从等挑战,采用先进技术同时确保隐私和安全,以同理心驱动的策略建立客户连接,实现业务成功。
21 1
打造精准营销!营销电子邮件以客户为中心策略解析!
|
存储 安全 算法
深入剖析JVM内存管理与对象创建原理
JVM内存管理,JVM运行时区域,直接内存,对象创建原理。
40 2
|
1月前
|
存储 算法 安全
【JVM】深入理解JVM对象内存分配方式
【JVM】深入理解JVM对象内存分配方式
29 0
|
1月前
|
存储 Java 数据安全/隐私保护
【JVM】Java虚拟机栈(Java Virtual Machine Stacks)
【JVM】Java虚拟机栈(Java Virtual Machine Stacks)
36 0
|
1月前
|
存储 JSON 安全
【C++ 泛型编程 综合篇】泛型编程深度解析:C++中的五种类型泛型策略综合对比
【C++ 泛型编程 综合篇】泛型编程深度解析:C++中的五种类型泛型策略综合对比
65 1
|
5天前
|
存储 安全 Java
JVM之本地方法栈和程序计数器和堆
JVM之本地方法栈和程序计数器和堆
9 0
|
6天前
|
存储 安全 网络安全
解析企业邮箱迁移:从技术到策略的完全指南
公司邮箱迁移是业务连续性和数据安全的关键步骤。涉及数据加密、安全存储和密钥管理,确保转移过程中的完整性与机密性。迁移应尽量减少对业务影响,通过IMAP/POP协议实现无缝转移。以Zoho Mail为例,需开启服务,获取授权码,设置转移,选择内容,填写原邮箱信息,最后验证数据。迁移前后注意备份和问题解决,确保顺利进行。
9 0
|
19天前
|
存储 缓存 算法
深度解析JVM世界:垃圾判断和垃圾回收算法
深度解析JVM世界:垃圾判断和垃圾回收算法
|
22天前
|
负载均衡 算法 Linux
深度解析:Linux内核调度器的演变与优化策略
【4月更文挑战第5天】 在本文中,我们将深入探讨Linux操作系统的核心组成部分——内核调度器。文章将首先回顾Linux内核调度器的发展历程,从早期的简单轮转调度(Round Robin)到现代的完全公平调度器(Completely Fair Scheduler, CFS)。接着,分析当前CFS面临的挑战以及社区提出的各种优化方案,最后提出未来可能的发展趋势和研究方向。通过本文,读者将对Linux调度器的原理、实现及其优化有一个全面的认识。
|
30天前
|
算法 IDE Linux
【CMake 小知识】CMake中的库目标命名和查找策略解析
【CMake 小知识】CMake中的库目标命名和查找策略解析
99 1

推荐镜像

更多