[java基础-JVM篇]1_JVM自动内存管理

JVM内存管理涉及但不限于类加载、对象分配、垃圾回收等,本篇主要记录运行时数据区域与对象相关内容。
内容主要来源《深入理解Java虚拟机:JVM高级特性与最佳实践》与官方文档,理解与表述错漏之处恳请各位大佬指正。
目录

运行时数据区域

栈帧

栈异常

栈内存大小配置

配置参数

配置参数

字符串常量池(注意,字符串常量池自jdk8起在堆)

静态变量特别说明

方法区

运行时常量池

常量说明

常量分析实例

配置参数

程序计数器

直接内存

总结

对象

对象创建

内存分配(更新操作)的原子性原理

对象访问

内存泄漏分析(生产)

大对象

大对象的影响

解决


运行时数据区域

内存管理基于:内存动态分配与垃圾收集技术。
编译期间做的事情:给变量分配空间

一般讲的都是虚拟机栈,更多情况下指虚拟机栈中局部变量表部分。本地方法栈执行的都是本地的Native方法。

栈帧

每个线程都有自己的虚拟机栈,而方法作为执行的基本单元,在 每个方法执行的时候都会创建一个栈帧Stack Frame用于存储局部变量表、操作数栈、动态连接、方法出口信息等。
其中 局部变量表 存放了编译期可知的 基本数据类型、对象引用
(了解就行)栈帧中存储的具体信息包括:

  1. 方法参数:包括传递给方法的实际参数以及方法的隐式参数,如 this 指针。
  2. 局部变量:定义在方法中的变量,包括基本类型和对象引用。
  3. 操作数栈:用于存储方法执行过程中产生的临时数值,如算术运算的结果、方法调用的返回值等。
  4. 方法返回地址:指向方法调用者应该返回的下一条指令的地址。
  5. 异常处理器:存储方法对应的异常处理器的信息,用于处理方法执行中可能发生的异常。
  6. 其它:包括方法的访问标志、常量池引用等。

栈异常

StackOverflowError
栈溢出
OutOfMemoryError
对于栈的提示是 java.lang.OutOfMemoryError: unable to create new native thread。此时无法创建新线程

栈内存大小配置

HotSpot虚拟机的栈容量不可动态拓展,在配置JVM时通过-参数Xss=256k配置。
JVM的总内存大小会影响线程栈内存的可用量。如果堆内存或者其他内存区域占用过多,剩余内存可能不足以支持大量线程。

配置参数

-Xss栈大小设置,在64位Linux系统上,栈大小默认1M

是垃圾回收管理器管理的区域,储物理上可以不连续,逻辑上是连续的内存空间。因垃圾收集器基于分代收集理论设计,所以将 分代划分(方便回收和分配内存)。
"新生代、老年代、Eden空间、From Survivor空间、To Survivor空间"。
堆:1:2=新生代:老年代, 其中新生代8:1:1 eden:from:to
存放几乎所有对象实例与数组(基本类型数组与对象类型数组)、字符串常量、静态变量。

配置参数

-Xms堆内存起始大小 -Xmx最大堆内存

字符串常量池(注意,字符串常量池自jdk8起在堆)

专门用于存储字符串字面量的一个特殊区域。它的设计目的是通过缓存机制避免重复创建相同的字符串对象,从而节省内存并提高性能。
值得注意的是,HotSpotJVM自JDK8将字符串常量池实现为堆中的一部分,从方法区移出,以优化内存管理并减少方法区的压力。
资料来源:
JEP 122: Remove the Permanent Generation
Chapter 5. Loading, Linking, and Initializing

静态变量特别说明

有资料说在方法区、有资料说在堆,给我整不会了。直接看官网
JEP 122: Remove the Permanent Generation
结论:静态变量引用或基本类型值静态变量在元空间,对象实例(包括静态对象的值)在堆中

方法区

方法区 也是线程共享的内存区域,主要用于存储已被虚拟机加载的 类型信息、静态变量对象的引用与基本类型静态变量、即时编译器编译后的代码缓存等数据。是基于本地内存实现的,方法区内存回收主要目标是常量池的回收和对类型的卸载

运行时常量池

运行时 常量池是方法区的一部分,是常量池表(Class文件常量池)运行时的表示形式,其中常量池表存储的是 符号引用 (Symbolic References)和 字面量 (Literal Values),而 不是实际的数据存储位置。它的核心作用是:

  • 存储类、方法、字段的全限定名描述
  • 保存方法描述符(Descriptor)
  • 存储字符串常量字面量
  • 记录数值型常量的原始值

常量说明

在代码中,使用final声明的常量会放到方法区运行时常量池
方法内变量=临时变量,存储在虚拟机栈中

常量分析实例

配置参数

-XX:MetaspaceSize:初始触发 GC 的阈值,默认约 21MB。
-XX:MaxMetaspaceSize:最大大小,默认无上限(受本地内存限制)。
-XX:MinMetaspaceFreeRatio 和 -XX:MaxMetaspaceFreeRatio:控制 GC 后空闲比例,默认 40% 和 70%。

程序计数器

作为游标指示字节码命令执行,每个线程的程序计数器是独立的。
计数器的值是正在执行的虚拟机字节码指令的地址。

直接内存

直接内存是指Java应用程序在JVM堆之外直接向操作系统申请并管理的一段内存区域。堆外内存的一种具体实现形式。
使用Native函数分配的堆外内存,用与NIO等操作。
设置虚拟机内存时,需要小于物理内存(给直接内存留空间)。因为运行时JVM内存=设置的内存+直接内存,实际占用的比设置的要大。防止动态扩展时出现OutOfMemoryError异常。
堆外内存通过DirectByteBuffer进行操作,避免在Java堆中和Native堆中来回复制数据。
需要注意的就是物理内存应该>jvm内存

总结

对象

对象可以划分为:对象头(Header)、实例数据(Instance)、对齐填充(Padding).
对象头包括两类信息。一部分包括对象自行运行时数据:哈 希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部 分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它 为"Mark Word"。
另一部分是类型指针,用于JVM确认对象所属类实例。

对象创建

JVM执行new指令时,会区常量池定位这个类的符号引用,检查这个符号引用代表的类是否已经被加载、解析、初始化过,没有则进行类加载。
为新对象分配内存的方式大致分为两种:

  • 堆为对象分配内存的方式
    • "指针碰撞"------假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那 个指针向空闲空间方向挪动一段与对象大小相等的距离。
  • 空闲列表------假设Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那 就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分 配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
    JVM选择哪种依据其垃圾收集器决定。
内存分配(更新操作)的原子性原理

划分空间时,在并发情况下可能是不安全的。例如对象A与对象B同时使用指针分配内存。
JVM默认采用CAS(Compare And Swap)搭配失败重试的方式保证操作的原子性。

对象访问

HotSpot主要使用直接指针访问对象。

内存泄漏分析(生产)


JVM配置参数-XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常的时候Dump当前内存堆转储快照。
也可以使用JMap命令导出Dump。
然后使用mat工具进行分析,如下:

大对象

占用较大连续内存空间的对象,一般是大数组、包含大量字段或嵌套对象的对象(复杂对象结构)、大型字符串对象。
将大对象直接分配到老年代可以避免"占用新生代内存导致提前GC带来的性能损耗"、"大对象在垃圾回收时的复制带来的性能损耗",是一种JVM优化策略。
值得注意的是,JDK8默认收集器是Parallel Scavenge与Parallel Old,大对象不会直接放到老年代。会照常进行回收流程。
JVM在配置使用Serial和parNew两款收集器时,可以通过参数-XX:PretenureSizeThreshold来设定一个阈值,如果对象的大小超过阈值会被视为"大对象",并被直接分配到老年代。
对于G1收集器,其引入了"HUmongous对象"的概念,任何超过Region大小一半的对象都会被视为巨型对象直接分配到老年代。

大对象的影响

  • 内存分配:大对象可能需要分配较大的连续内存块,这可能导致堆内存碎片化问题。
  • 垃圾回收:某些垃圾回收器(如 G1 GC)对大对象有特殊处理机制,可能会直接分配到老年代(而不是新生代)。

解决

在日常编码中要尽量避免大对象,不可避免时尽量复用,如使用对象池。
对于大数组对象,进行分割、将其分割为小对象进行操作。
或者使用手动释放的堆外内存。

try {

ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);

// 使用 buffer

} finally {

if (buffer != null && buffer.isDirect()) {

((sun.misc.Cleaner) sun.misc.Unsafe.getUnsafe().invokeCleaner(buffer)).clean();

}

}

相关推荐
Scott9999HH3 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔3 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社4 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海4 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖5 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区7 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大8 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai8 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说8 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json