JVM(三)堆内存与对象

1 堆内存

image

1.1 栈、堆、方法区的简易关系示例

image

1.2 堆内存的大小

used‌:表示当前已经使用的堆内存大小
total‌:表示Java虚拟机当前已经分配的可用堆内存大小
‌max‌:表示Java虚拟机可以分配的最大堆内存大小
image
每次添加对象时,used 值会增加,表示当前堆中已经被占用的内存变多了。当 used 接近 total 时,JVM 会继续分配内存给堆,total 值会随之增加。但 total 最多只能等于 max
image

1.2.3 堆的大小的设置

image

  • 在实际应用中,为了减少GC压力和提高性能,通常会将 -Xms 和 -Xmx 设置为相同的值,使 total 等于 max。
1.2.3.1 示例:-Xms600m Xms600m

image

1.2.3.2 示例:查看堆内存

jvm参数:-Xms2048m -Xmx2048m -XX:+PrintGCDetails

image

1.2 分代划分

image
image

  • 上图为各区的默认比例

1.3 对象分配

image

1.3.1 对象分配过程

新建的对象先分配在eden,eden快满时,触发垃圾回收——YoungGC(即MinorGC),通过可达性分析算法判断哪些是垃圾,哪些不是,不是的对象,其年龄计数器加一,放到S0,此时eden是清空的,注意S1也是空的;后续eden又快满时,非垃圾放到S1,此时eden和S0是空的;后续又快满时,重复上述过程,直到达到阈值。S0、S1中,空的区称为to区,不空的称为from区。
image
image
对象达到默认值15时,会晋升到老年区。
image

1.3.2 对象分配的特殊情况:

eden放不下,比如YGC后eden是空的,但是eden区只有5M,对象6M,直接放到老年区;
old放不下,比如老年代200M,剩2M,对象5M,触发FullGC;
image

1.3.3 内存分配策略总结

image

  • 大对象通常指在内存空间中连续分布的对象,比如很长的字符串、数组

1.3.4 测试大对象直接分配到老年代

eden区16M、s1区2M、S2区2M、老年代40M
image
image

1.3.5 TLAB

JVM为每个线程分配一个私有缓存区域,它包含在eden区中,默认情况下是开启的。
image
image
image

1.4垃圾回收初步

1.4.1 YGC、Major GC 、Full GC

  • Young GC,也称 ‌Minor GC‌,仅针对 ‌新生代(Young Generation)‌ 进行回收。

  • Major GC 指仅回收 ‌老年代(Old Generation / Tenured Space)‌ 的垃圾回收行为。

    • 在 HotSpot JVM 中,‌除 CMS 外,大多数收集器在触发老年代回收时,会同时执行 Full GC‌,因此“Major GC”极少独立发生。
    • 仅在使用 CMS 收集器时,可能出现纯老年代并发回收。
  • Full GC,回收整个堆内存‌,包括,新生代(Eden + Survivor)老年代、元空间(Metaspace,JDK 8+)

  • 在 HotSpot JVM 的大多数垃圾收集器(如 Parallel Scavenge/Parallel Old, G1)中,当老年代需要回收时,通常会直接触发一次 ‌Full GC‌(即整堆回收,包括新生代、老年代和元空间),而不会单独进行一次只回收老年代的 Major GC。因此,在这些收集器下,“Major GC” 和 “Full GC” 这两个术语常常被混用。

1.4.2 YGC

image

  • 触发YGC时,幸存区也会跟着(一并)被回收。

1.4.3 Major GC

image
触发条件:

  • 1、老年代空间不足‌

    • 对象晋升失败‌:在 Young GC (Minor GC) 后,存活下来的对象需要从新生代晋升到老年代。如果此时老年代没有足够的连续空间来存放这些晋升对象,就会触发 Major GC。
    • 大对象直接分配‌:应用程序创建了体积非常大的对象(例如大数组),这些对象根据 JVM 的策略(如 -XX:PretenureSizeThreshold 参数设置)会绕过新生代,直接在老年代分配。如果老年代空间不足以容纳这样的大对象,也会触发 Major GC。
  • 2、特定垃圾收集器的行为

    • CMS 收集器的并发回收‌:在使用 CMS (Concurrent Mark-Sweep) 收集器时,当老年代空间使用率达到某个阈值(可通过 -XX:CMSInitiatingOccupancyFraction 设置)时,会启动一个‌并发‌的 Major GC 过程。这是 Major GC 独立发生的最典型场景。

1.4.4 Full GC

触发条件
老年代空间不足‌:Young GC 后晋升的对象大于老年代剩余空间

元空间耗尽‌:类加载过多,元空间分配失败

晋升失败:Survivor 区对象晋升时老年代无足够连续空间

1.4.5 GC动态示例:

image

2 对象

2.1 对象创建的步骤:

  • 1、虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程

  • 2、创建对象于堆空间,为对象分配内存,对象此时能确定大小,比如int4个字节、long8个字节;分配的方式,一,如果内存规整,采用指针碰撞,二,如果内存不规整,占用的内存和未用的内存相互交错,采用空闲列表法,虚拟机会维护一份空闲列表,记录哪些是占用的,哪些是未用的,分配时找未用的,然后再更新列表。

    • 2.1 指针碰撞:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
      • 2.1.1 适用于使用复制算法或标记-整理算法的垃圾收集器‌,这些收集器在回收后会整理内存,使得堆内存连续,从而支持指针碰撞

image

    • 2.2 空闲列表:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录
      • 2.2.1 这种方式适用于如 CMS、ZGC 等不进行内存整理的垃圾收集器

image

  • 3、并发

    • CAS+失败重试:虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
    • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
  • 4、默认初始化,即所有属性设置默认值

  • 5、设置对象的对象头

  • 6、执行init方法初始化,即显式初始化或代码块初始化或构造器初始化。

2.2 对象的内存布局

2.2.1 即new出来的对象在堆内存里,由哪些结构。

对象头包括两部分信息:标记字段(Mark Word):用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。

类型指针(Klass pointer):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据:是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充:是为了满足JVM对对象大小的要求而添加的,确保对象的大小是8字节的整数倍。如果对象头和实例数据的总大小不是8的倍数,则会通过填充来补齐。

image

2.2.2 内存布局图示举例

image
image
image

  • 内存紧张的情况下,使用数字,推荐int,而非Interge(占用空间大)

2.2.3. 对象访问定位

句柄池,有个好处,体现在标记整理算法,还有S0、S1来回移动上
image

直接指针
image

3 直接内存

介绍:
直接内存是在Java堆外的、直接向系统申请的内存空间。
使用ByteBuffer.allocateDirect()分配的是‌堆外内存(off-heap memory)‌,这块内存由操作系统直接管理。

分配空间不占用 -Xmx 设置的堆内存上限。
但仍受系统物理内存和 -XX:MaxDirectMemorySize 参数限制,否则会抛出 OutOfMemoryError: Direct buffer memory
示例:
-XX:MaxDirectMemorySize=1g # 设置最大直接内存为1GB
-XX:MaxDirectMemorySize=512m # 设置为512MB

3.1 传统I/O传输文件

传输路径:
硬盘 --DMA--> 内核缓冲区 --CPU拷贝--> 应用程序内存 --CPU拷贝--> Socket缓冲区 --DMA--> 网卡

说明:
用户调用read()
→ 切换内核态(第1次切换)
→ DMA拷贝到内核缓冲区(第1次拷贝,硬件完成)
→ CPU拷贝到用户缓冲区(第2次拷贝,软件完成,耗时!)
→ 切换用户态(第2次切换)
用户处理数据(你的Java程序)
→ 切换内核态(第3次切换)
→ CPU拷贝到Socket缓冲区(第3次拷贝,软件完成,耗时!)
→ DMA拷贝到网卡(第4次拷贝,硬件完成)
→ 切换用户态(第4次切换)

耗时分析‌:
4次上下文切换:每次切换约0.1-0.2微秒,累积影响
2次CPU拷贝:1GB数据完全由CPU搬运,约0.8秒
2次DMA拷贝:硬件并行工作,不占CPU时间

3.2 使用直接内存优化

传输路径:
硬盘 --DMA--> 直接内存 --DMA--> 网卡

说明:
用户调用transferTo()
→ 切换内核态(第1次切换)
→ DMA从磁盘到直接内存(第1次拷贝,硬件完成)
→ DMA从直接内存到网卡(第2次拷贝,硬件完成)
→ 切换用户态(第2次切换)

耗时分析‌:
上下文切换:2次
CPU拷贝:2次

posted @ 2025-12-13 12:50  dvdhellohaha  阅读(22)  评论(0)    收藏  举报