细说jvm(三)、对象创建的内存分配

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 细说jvm(三)、对象创建的内存分配

对象创建的内存分配


在对象创建的时候给对象分配内存总共是可能有如下的几种可能:

(1)将对象分配在栈上 (2)使用TLAB (3)分配在eden

我们一点一点的来说下,每一点展开都是个知识点


栈上分配

这里需要先说的一个是逃逸分析,在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。而hotspot能够在动态加载方法的时候对代码进行逃逸分析,如果发现一个新对象的引用仅仅是在这个方法的范围内,那么这个对象的分配区域就会仅仅在栈上。为了证明我说的是对的,我需要用一段代码来证明一下,在代码开始之前,我先简单介绍几个会用到的jvm的参数,不然你可能会比较懵逼。

  • -Xss  这个参数是指明栈空间的大小,我们这里为了让一个栈有足够的大小,因此给2m的大小
  • -Xms  这个是堆的初始化大小
  • -Xmx  这个是堆的最大大小
  • -XX:+PrintGCDetails  开启打印垃圾回收日志
  • -XX:+UseConcMarkSweepGC 使用CMS垃圾回收器
  • -XX:+PrintGCDateStamps  打印GC发生的时间,用的humanbeing的方式
  • -XX:-DoEscapeAnalysis  关闭逃逸分析


我们仔细观察上面的参数,发现有的参数前面带着+或者-,jvm的参数有两类,一类是需要设置具体的值的,另外一类只是单纯的开启和关闭的,
单纯的开启和关闭就用的是+和-,+指的是开启,-指的是关闭,设置值的在后面跟上值就行了


上面是我使用的参数,具体的设置如下:


-Xss2048k
-Xms50m
-Xmx50m
-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDateStamps
-XX:-DoEscapeAnalysis


代码如下:


public static void main(String[] args) throws IOException {
        while (true) {
            new MyEntity(1, "a");
        }
}
// MyEntity类如下
public class MyEntity {
    private Integer id;
    private String name;
    public MyEntity(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
}


然后我们开始跑main方法,如我们想的,GC会疯狂运行,因为我们关闭了逃逸分析,GC日志如下:


1686814433725.png


我们先不在本章教你读GC日志(这是个很长的话题),这里贴出来的目的只是证明关闭逃逸分析的影响而已,我们再从另外一方面证明堆内存中有很大量的MyEntity对象,我们打开终端,windows是打开cmd,输入jvisualVM,它的界面如下:


1686814441541.png


双击我画红框的进程,这个进程就是我们正在跑着的java程序,打开之后界面如下:


1686814447802.png


按顺序选择我画红框的按钮,之后你可以看到的如下图:


1686814455150.png


可以看到MyEntity实例数量在堆内存中非常多,其实它的数量应该是一会儿多一会儿少,因为在每次GC的时候总会有很多被回收掉(GC的细节我们下篇文章开始说)。

接下来我们证明下,逃逸分析开启之后,对象是有可能被分配到栈上的。

参数如下:注意仅仅修改最后一项的-为+号


-Xss2048k
-Xms50m
-Xmx50m
-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDateStamps
-XX:+DoEscapeAnalysis


代码和上面一样,但是这次跑起来根本没有GC日志输出,我的IDEA控制台干干净净一片:


1686814464256.png


同时,继续看jvisualVM的内存监控如下,你会发现内存中根本没有MyEntity的实例


1686814472183.png


TLAB分配


TLAB全称是Thread Local Allocate Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。 由于对象一般会分配在堆上,而堆是所有线程共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(jvm采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。

TLAB本身占用Eden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

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

一般不建议手工修改TLAB相关参数,推荐使用虚拟机默认行为。


为什么说不推荐修改TLAB相关的东西呢?这是因为TLAB的优化是极其难控制的,在不同的业务场景下对象创建的情况差别会非常大,
因此我们一般不会优化这里的参数,只是使用默认的参数。


分配在eden区域


当jvm判断不能使用前两种分配方式的时候就会触发这种分配方式,在这种情况下,会有两种选择:


1、指针碰撞:所谓的连续内存是指Java堆中的内存是绝对规整的,用过的内存在一边,空闲的内存在另一边。中间有个指针作为分界点,
这时如果要分配新内存,只要指针向空闲的内存一方移动一下就可以了。这种分配内存的方式就叫指针碰撞。
2、空闲列表:如果Java堆中的内存并不是完整的,也就是不是连续的。这时使用的内存和空闲的内存没有任何规则,无法用指针碰撞的方
式来分配内存。这时虚拟机只能采取其它办法来标识出哪些内存是使用的,哪些内存是空闲的,所以虚拟机就要维护一个列表,用来存储哪些
内存是空闲的,分配内存时,只要从列表中划分一块区域存储对象实例,并更新列表上的记录就可以了。这种方式就叫空闲列表


具体使用的是哪种,取决于使用的垃圾回收器使用的哪种算法,一般来说,我们的hotspot在使用CMS和G1垃圾回收器的时候都是用的第二种。

其实分配在eden这种说法并不绝对,因为当一个对象非常大大到了eden都放不下的时候,这时候还要保证这个分配一定成功,这时候就会让这个对象进入老年代,这个是jvm的内存分配担保机制。

对象创建时候的内存分配就说这么多,下一篇我们开始说垃圾回收

目录
相关文章
|
8天前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
23 4
|
8天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
29 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
7天前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
33 2
|
8天前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
19 2
|
8天前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
24 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
6天前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
10天前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
2月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
11天前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
|
11天前
|
Java API 对象存储
JVM进阶调优系列(2)字节面试:JVM内存区域怎么划分,分别有什么用?
本文详细解析了JVM类加载过程的关键步骤,包括加载验证、准备、解析和初始化等阶段,并介绍了元数据区、程序计数器、虚拟机栈、堆内存及本地方法栈的作用。通过本文,读者可以深入了解JVM的工作原理,理解类加载器的类型及其机制,并掌握类加载过程中各阶段的具体操作。