细说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的内存分配担保机制。

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

目录
相关文章
|
6月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
601 55
|
28天前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
185 4
|
1月前
|
存储 缓存 算法
深入理解JVM《JVM内存区域详解 - 世界的基石》
Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。
|
7月前
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
588 6
|
6月前
|
Arthas 监控 Java
Arthas vmtool(从 jvm 里查询对象,执行 forceGc)
Arthas vmtool(从 jvm 里查询对象,执行 forceGc)
418 16
|
8月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
378 29
JVM简介—1.Java内存区域
|
8月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
8月前
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
|
8月前
|
存储 设计模式 监控
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
225 0
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
|
9月前
|
存储 算法 Java
JVM: 内存、类与垃圾
分代收集算法将内存分为新生代和老年代,分别使用不同的垃圾回收算法。新生代对象使用复制算法,老年代对象使用标记-清除或标记-整理算法。
137 6

热门文章

最新文章