JVM内存分配机制详解

news2025/2/25 12:02:14

文章目录

    • 一、对象的创建流程
      • 1、类加载检查
      • 2、分配内存
      • 3、初始化
      • 4、设置对象头
      • 5、执行方法
    • 二、对象内存分配
      • 1、栈上分配
      • 2、对象在Eden区分配
      • 3、大对象直接进入老年代
      • 4、长期存活的对象进入老年代
      • 5、对象动态年龄判断
      • 6、老年代空间分配担保机制
    • 三、对象内存回收
      • 1、引用计数法
      • 2、可达性分析算法

一、对象的创建流程

对象创建初始化流程

1、类加载检查

虚拟机遇到一条new指令时,首先去检查这个指令的参数能否在常量池中找到一个符号引用,并且检查这个符号引用代表的类是否已加载。若没有加载,则先执行类加载过程。
  new指令对应到语言层面上是指,new关键字、克隆对象、对象序列化等。

2、分配内存

类加载检查通过后,接下来就是为对象分配内存。对象所需内存的大小在类加载期间便可以确定。为对象分配空间就是在堆内存中划分一块确定大小的内存,这个过程会出现两个问题。
  如何划分内存?
  怎么保证并发安全?

划分内存:

  • “指针碰撞”(Bump the Pointer)(默认用指针碰撞)
    如果Java内存是觉得规整的,所有使用过的内存放在一边,未使用的在另一边,中间放着一个指针作为分界点,那么分配内存就是把指针往未使用区域挪动待分配对象大小的距离。
  • “空闲列表”(Free List)
    如果Java内存不规整,已使用和未使用的内存相互交错。这种情况下就不能使用指针碰撞,虚拟机就需要维护一份列表,记录哪些内存是可以使用的,给对象分配内存的时候,从列表中取出一块足够大的空间。

保证并发安全:

  • CAS
    虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理
  • 本地线程分配缓存(Thread Local Allocation Buffer,TLAB)
    把分配内存的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一块内存。通过­XX:+/­ UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启­XX:+UseTLAB),­XX:TLABSize 指定TLAB大小。

3、初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一过程也可以提前至TLAB分配时进行。这一步保证了对象的实例在Java代码中可以不赋初值的情况下就可以直接使用,程序能访问到这些字段的数据类型对应的零值。

4、设置对象头

初始化后,虚拟机要对对象进行必要的设置。例如,这个对象是哪个类的实例,怎么找到类元信息,对象的哈希码、GC的分代年龄等信息,这些都在对象头中。
  在HotSpot虚拟机中,对象在内存中的存储布局可以分为3块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)

  • 对象头(Header)
    对象头包括两部分信息,第一部分Mark Word标记字段(32位占4字节,64位占8字节),用于存储对象自身的运行时数据, 如哈 希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。
      对象头的另外一部分 是类型指针Klass Pointer(开启指针压缩占4字节,关闭占8字节),即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
      另外,数组对象还会有数组长度(占4字节),数组最大容量2^32-1。
    • 32位对象头
      在这里插入图片描述
    • 64位对象头
      在这里插入图片描述

5、执行方法

执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋 零值不同,这是由程序员赋的值),和执行构造方法。

二、对象内存分配

  • 栈上分配
  • Eden区分配
  • 大对象直接进入老年代
  • 长期存活的对象进入老年代
  • 对象动态年龄判断机制
  • 老年代空间分配担保机制
    在这里插入图片描述

1、栈上分配

一般来说Java对象都是在堆上分配的,当对象没有引用时,需要GC进行回收,如果对象数量较多,回收会有一定的压力,也间接影响性能。为了减少临时对象在堆内存的分配数量,JVM通过逃逸分析确定该对象会不会被外部访问。如果能确定对象不会逃逸,就可以将对象分配到栈空间上,这样对象所占用的内存会随着栈帧出栈而释放,减轻了垃圾回收的压力。

  • 逃逸分析
    分析对象动态作用域,当一个对象在方法中定义后,可能会被外部引用,例如作为参数传递到其他方法中,那么该对象的作用域范围不确定。如果一个对象只在本方法内使用,当方法结束后,这个对象就是无效的了,这样的对象可以将其分配到栈空间里,让其在方法结束时跟随栈内存一起被回收掉。
    JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
  • 标量替换
    通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该 对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就 不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认 开启。
  • 标量与聚合量
    标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及 reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一 步分解的聚合量。

结论:栈上分配依赖于逃逸分析和标量替换

2、对象在Eden区分配

大多数情况下,对象都是在Eden区分配的,当Eden区没有足够的空间时,会进行一次MinorGC。

  • MinorGC/YoungGC:指发送在年轻代的垃圾回收动作,MinorGC回收次数频繁,速度很快。
  • MajorGC/FullGC:回收年轻代、方法区、老年代的垃圾,速度很慢,相比MInorGC,要慢上10倍左右。

Eden区与Survivor区默认为8:1:1
  大量对象创建在Eden区,等Eden区满了后,会触发Minor GC,其中99%的对象会被回收掉,剩余存活的对象会进入到一块有空间的Survivor区。等到下次Eden区满时,再次发生Minor GC,把Eden区和Survivor区中的垃圾对象回收,剩余存活的对象会一起进入另一块Survivor区。
  年轻代中的大多数对象的存活时间很短,可以说是朝生夕死,所以JVM中默认8:1:1的比例还是比较合理的,实际应用中让Eden区足够大,Survivor区够用即可
  JVM中的参数-XX:+UseAdaptiveSizePolicy(默认开启)会导致8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy。

新建对象Eden区分配示例

/**
 * 添加运行JVM参数: -XX:+PrintGCDetails
 */
public class GCTest  {

    public static void main(String[] args) {
        byte[] allocation1;
        // 60000K
        allocation1 = new byte[60000*1024];
    }
}
运行结果:
Heap
 PSYoungGen      total 76288K, used 65536K [0x000000076b100000, 0x0000000770600000, 0x00000007c0000000)
  eden space 65536K, 100% used [0x000000076b100000,0x000000076f100000,0x000000076f100000)
  from space 10752K, 0% used [0x000000076fb80000,0x000000076fb80000,0x0000000770600000)
  to   space 10752K, 0% used [0x000000076f100000,0x000000076f100000,0x000000076fb80000)
 ParOldGen       total 175104K, used 0K [0x00000006c1200000, 0x00000006cbd00000, 0x000000076b100000)
  object space 175104K, 0% used [0x00000006c1200000,0x00000006c1200000,0x00000006cbd00000)
 Metaspace       used 3221K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

12345678910111213141516171819202122

此时可以看到Eden区已经被填满(程序运行时,即使什么都不做,新生代也会有几M内存使用),此时再分配一个对象时,内存会怎么样?

Eden区满时再分配

public class GCTest  {

    public static void main(String[] args) {
        byte[] allocation1,  allocation2;
        // 60000K
        allocation1 = new byte[60000*1024];
        // 8000K
        allocation2 = new byte[8000*1024];
    }  
}
运行结果:
[GC (Allocation Failure) [PSYoungGen: 65244K->824K(76288K)] 65244K->60832K(251392K), 0.0293285 secs] [Times: user=0.17 sys=0.03, real=0.03 secs] 
Heap
 PSYoungGen      total 76288K, used 9479K [0x000000076b100000, 0x0000000774600000, 0x00000007c0000000)
  eden space 65536K, 13% used [0x000000076b100000,0x000000076b973ef8,0x000000076f100000)
  from space 10752K, 7% used [0x000000076f100000,0x000000076f1ce030,0x000000076fb80000)
  to   space 10752K, 0% used [0x0000000773b80000,0x0000000773b80000,0x0000000774600000)
 ParOldGen       total 175104K, used 60008K [0x00000006c1200000, 0x00000006cbd00000, 0x000000076b100000)
  object space 175104K, 34% used [0x00000006c1200000,0x00000006c4c9a010,0x00000006cbd00000)
 Metaspace       used 3221K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
123456789101112131415161718192021

分配allocation1时,Eden区被填满,此时分配allocation2,Eden区放不下,所以产生一次MinorGC。GC后allocation1还是存活对象,按理来说应该会进入Survivor区,但是Survivor区放不下(space 10752K),所以allocation1提前进入老年代,老年代的空间足够放下allocation1,所以不会产生Full GC。
  执行玩MInor GC后,Eden区还有足够的空间,后面产生的对象还是会继续分配到Eden区。

Minor GC后Eden区继续分配示例

public class GCTest  {

    public static void main(String[] args) {
        byte[] allocation1,  allocation2, allocation3, allocation4, allocation5, allocation6;

        // 60000K
        allocation1 = new byte[60000*1024];
        // 8000K
        allocation2 = new byte[8000*1024];
        
        // 4*1000k
        allocation3 = new byte[1000*1024];
        allocation4 = new byte[1000*1024];
        allocation5 = new byte[1000*1024];
        allocation6 = new byte[1000*1024];
    }
    
}
运行结果:
[GC (Allocation Failure) [PSYoungGen: 65244K->872K(76288K)] 65244K->60880K(251392K), 0.0347016 secs] [Times: user=0.02 sys=0.00, real=0.04 secs] 
Heap
 PSYoungGen      total 76288K, used 13799K [0x000000076b100000, 0x0000000774600000, 0x00000007c0000000)
  eden space 65536K, 19% used [0x000000076b100000,0x000000076bd9fbe8,0x000000076f100000)
  from space 10752K, 8% used [0x000000076f100000,0x000000076f1da020,0x000000076fb80000)
  to   space 10752K, 0% used [0x0000000773b80000,0x0000000773b80000,0x0000000774600000)
 ParOldGen       total 175104K, used 60008K [0x00000006c1200000, 0x00000006cbd00000, 0x000000076b100000)
  object space 175104K, 34% used [0x00000006c1200000,0x00000006c4c9a010,0x00000006cbd00000)
 Metaspace       used 3221K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0
12345678910111213141516171819202122232425262728293031

可以看到后续创建的对象继续在Eden区分配。
运行结果解释

  • GC (Allocation Failure)
  • 表示这是一次YGC,括号里面表示GC的原因,否则就是FGC
  • PSYoungGen total 76288K, used 13799K
    年轻代总共76288K大小,已使用13799K
  • eden space 65536K, 19% used
    Eden区大小和已使用百分比
  • from space 10752K, 8% used
    survivor区空间大小和已使用百分比
  • to space 10752K, 0% used
    另一块survivor区
  • ParOldGen total 175104K, used 60008K
    老年代空间大小,和已使用空间

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

大对象就是需要连续使用内存空间的对象,如数组对象。JVM参数 -XX:PretenureSizeThreshold=(单位字节)可以设置大对象的大小,如果对象大小超过设置的参数,则直接进入老年代。这个参数只在 Serial 和ParNew两个收集器下有效。
  为了避免为大对象分配内存时的复制操作而降低效率。

4、长期存活的对象进入老年代

JVM虚拟机采用了分代收集的思想来管理内存, 那么在进行内存回收时,就需要识别哪些对象放到年轻代,哪些放到老年代。为了做到这一点,虚拟机给每个对象设置了一个对象年龄(Age)计数器。
  如果一个对象经历了Minor GC还能存活,且Survivor区能够放得下它,那么该对象的年龄就记为1。在Survivor区中,每经历一次Minor GC还能存活的话,年龄就会加1。当对象的年龄增加到一定程度时(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会进入到老年代中。对象进入到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
  通过上文的对象头可以了解到,一个对象的年龄占4位(bit),所以一个对象的最大年龄位2^4-1=15。

5、对象动态年龄判断

当前的Survivor区域里,有一批不同年龄的对象,从最小年龄开始,年龄1+年龄2+年龄n,他们的大小总和超过该区域总大小的50%(-XX:TargetSurvivorRatio可以指定),那么年龄大于等于n的对象都要进入到老年代中区。
  这个规则的目的是希望可能长期存活的对象尽早进入老年代。
  对象动态年龄判断一般是在Minor GC后触发的

6、老年代空间分配担保机制

JVM有这么一个参数:-XX:-HandlePromotionFailure(1.8默认设置)
  年轻代每次GC前都,JVM都会计算老年代剩余可用空间,如果这个剩余空间小于年轻代里所有对象大小之和(包括垃圾对象),那么JVM就会看是否设置前面这个参数。如果设置这个参数,且老年代剩余空间是否小于之前每一次MInorGC后进入老年代对象的平均大小
  如果没设置参数,或者小于平均大小,会先触发一次FullGC,将老年代和年轻代的垃圾对象一起回收掉,如果回收后还是没有空间存放对象,则会发生OOM
在这里插入图片描述

三、对象内存回收

堆中存放着几乎所有对象的实例,对堆进行垃圾回收,首先就要判断哪些对象已死亡。判断对象是否为垃圾对象有两种方法:引用计数法、可达性分析算法

1、引用计数法

给对象添加一个计数器,每当有一个对象引用它时,计数器就加1,当引用失效,计数器减1,当计数器归0时,就表示这个对象没有任何引用,可以回收。
  这个方法在目前主流的虚拟机中并没有使用,主要还是互相引用的问题不好解决。

2、可达性分析算法

GC Root对象作为起点,从这些起点开始乡下搜索,找得到的对象都标记为非垃圾对象,其余未标记的对象都需要进行回收。

  • GC Root根节点:线程栈的本地变量、静态变量、本地方法栈的变量等

本文是在原文的基础上修改了一部分,原文链接信息如下:
————————————————
版权声明:本文为CSDN博主「温柔的皮卡丘」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44318459/article/details/127098466

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/812437.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

别再分库分表了,试试TiDB!

什么是NewSQL 传统SQL的问题 升级服务器硬件 数据分片 NoSQL 的问题 优点 缺点 NewSQL 特性 NewSQL 的主要特性 三种SQL的对比 TiDB怎么来的 TiDB社区版和企业版 TIDB核心特性 水平弹性扩展 分布式事务支持 金融级高可用 实时 HTAP 云原生的分布式数据库 高度兼…

RT1052 的四定时器

文章目录 1 Quad Timer,简称:QTMR2 单个通道的框图3 QTMR配置3.1 QTMR1 时钟使能。3.2 初始化 QTMR1。3.2.1 QTMR_Init 3.3 设置 QTMR1 通道 0 的定时周期。3.3.1QTMR_SetTimerPeriod 3.4 使能 QTMR1 通道 0 的比较中断。3.4.1 QTMR_EnableInterrupts 3.…

14. Spring AOP 的组成和实现

目录 1. Spring AOP 简介 2. AOP 的组成 2.1 切面(Aspect) 2.2 连接点(Join Point) 2.3 切点(Pointcut) 2.4 通知(Advice) 3. Spring AOP的实现 3.1 新建项目 3.2 添加 AOP …

Zebec Card 将在亚洲、拉美等地区推出,生态全球化加速

随着以Visa、特斯拉、BNY Mellon、BlackRock、Mastercard、Gucci等为代表的传统商业机构巨头,以及萨尔瓦多、中非共和国等为代表的国家不断的向加密货币领域布局,越来越多的投资者开始以新的眼光来看待加密货币,仅在2022年,加密货…

1400*B. I Hate 1111(思维+数学)

Example input 3 33 144 69 output YES YES NO 题意: 问一个数字是否可以由 11,111,1111,11111...... 任意倍数加和所得。 解析: 可以观察到 1111%110,11111%1110,而后面更大的11111111…

C++部署学习

gcc -E src/main.c -o src/main.i gcc -S src/main.c -o src/main.s gcc -C src/main.c -o src/main.o gcc src/main.c -o exec ./exec

Vue2第八节 收集表单数据

&#xff08;1&#xff09;文本类型/密码类型 v-model收集的是value值 用户输入的就是value值 <input type"text" v-model"usrInfo.account"> <input type"password" v-model"usrInfo.password"> &#xff08;2&#xf…

Java程序逻辑控制的几个小练习(熟悉一下Java的语法)

虽然都是一些很简单的题目&#xff0c;但是都是很经典的编程题&#xff0c;多写几道可以熟悉一些Java代码&#xff0c; 之前C语言敲多了一时半会有点不适应&#xff0c;敲起来怪怪的&#xff0c;感觉手不是自己的了哈哈 目录 1. 根据年龄, 来打印出当前年龄的人是少年(低于18)…

多臂治疗规则的 Qini 曲线(Stefan Wager)

英文题目&#xff1a; Qini Curves for Multi-Armed Treatment Rules 中文题目&#xff1a;多臂治疗规则的 Qini 曲线 单位&#xff1a;Stefan Wager 论文链接&#xff1a; 代码&#xff1a;GitHub - grf-labs/maq: Treatment rule evaluation via the multi-armed Qini …

软件测试面试【证券项目公司】

这家公司是做证券项目的&#xff0c;约的9点钟&#xff0c;路程还是有点遥远&#xff0c;转了一趟公交两趟地铁&#xff0c;精力都花在了路上&#xff0c;感觉有点累&#xff0c;以下是今天得面试流程。 到公司前台给我了一张面试表&#xff0c;写完之后就是等待面试。一共面试…

Python模块psycopg2连接postgresql

目录 1. 基础语法 2. 基础用法 3. 多条SQL 4. 事务SQL 1. 基础语法 语法 psycopg2.connect(dsn #指定连接参数。可以使用参数形式或 DSN 形式指定。host #指定连接数据库的主机名。dbname #指定数据库名。user #指定连接数据库使用的用户名。…

TMS XData v5.11 2023 crack,全功能查询机制

TMS XData v5.11 2023 crack,全功能查询机制 用于多层REST/JON-HTTP/HTTPS应用服务器开发和ORM远程处理的Delphi框架。 功能概述 基于REST/JONS架构风格的服务器 从不同的客户端平台(如.NET、Java、jаvascript)轻松访问&#xff0c;因为它是基于REST/JSON的 使用标准POST、GET…

【RabbitMQ】golang客户端教程2——工作队列

任务队列/工作队列 在上一个教程中&#xff0c;我们编写程序从命名的队列发送和接收消息。在这一节中&#xff0c;我们将创建一个工作队列&#xff0c;该队列将用于在多个工人之间分配耗时的任务。 工作队列&#xff08;又称任务队列&#xff09;的主要思想是避免立即执行某些…

[golang gin框架] 43.Gin商城项目-微服务实战之后台Rbac微服务之管理员的增删改查以及管理员和角色关联

上一节讲解了后台Rbac微服务角色增删改查微服务,这里讲解权限管理Rbac微服务管理员的增删改查微服务以及管理员和角色关联微服务功能 一.实现后台权限管理Rbac之管理员增删改查微服务服务端功能 1.创建Manager模型 要实现管理员的增删改查,就需要创建对应的模型,故在server/r…

使用Beego和MySQL实现帖子和评论的应用,并进行接口测试(附源码和代码深度剖析)

文章目录 小项目介绍源码分析main.gorouter.gomodels/user.gomodels/Post.gomodels/comment.gocontrollers/post.gocontrollers/comment.go 接口测试测试增加帖子测试查看帖子测试增加评论测试查看评论 小项目介绍 经过对需求的分析&#xff0c;我增加了一些额外的东西&#x…

Linux学习之脚本优先级控制

fork炸弹 在编写Shell脚本时不要写出不可控的死循环&#xff0c;比如func() { func | func& } ; func&#xff0c;简写版为.(){ .|.& };.。接下来见证一下这两条语句的威力。因为在root用户下许多资源没有限制&#xff0c;所以useradd userfork新建一个用户userfork&a…

fwrite函数

1、函数声明 size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream ); 2、参数说明 buffer 指向要写入的数据的指针。 size 项大小&#xff08;以字节为单位&#xff09;。 count 要写入的项的最大数量。 stream 指向 FILE 结构的指针。 3、…

OpenCloudOS 与PolarDB全面适配

近日&#xff0c;OpenCloudOS 开源社区签署阿里巴巴开源 CLA (Contribution License Agreement, 贡献许可协议), 正式与阿里云 PolarDB 开源数据库社区牵手&#xff0c;并展开 OpenCloudOS &#xff08;V8&#xff09;与阿里云开源云原生数据库 PolarDB 分布式版、开源云原生数…

AD21原理图的高级应用(四)线束的设计及应用

&#xff08;四&#xff09;线束的设计及应用 Altium Designer 21 可以使用 Signal Harnesses(信号线束)的方法来建立元件之间的连接,也可用于不同原理图间的信号对接。信号线束是一种抽象连接,操作方式类似于总线,但信号线束可对包括总线、导线和其他信号线束在内的不同信号进…

el-button增加下载功能

vue3和element-plus <el-uploadv-model:file-list="fileList"action="/api/upload"multiple:limit="1":headers="headers" ><el-button type="primary">选择文件</el-button><template #file