JVM自动内存管理
自动内存管理
- 运行时数据区域
- 程序计数器(线程私有)
- Java虚拟机栈(线程私有)
- 本地方法栈
- Java堆(GC堆)(各个线程共享) 对象实例
- 方法区(各个线程共享)类
- 运行时常量池(方法区的一部分)
- 直接内存
- HotSpot虚拟机对象探秘
- 对象的创建(new )
- 对象的内存布局(堆中)
- 对象头
- 实例数据
- 对齐填充
- 对象的访问定位
- 直接访问
- 句柄
- OutOfMemoryError异常
- Java堆溢出
- 虚拟机栈和本地方法栈溢出
- 方法区和运行时常量池溢出(是方法区的一部分)
运行时数据区域
Java线程隔离的数据区的生命周期与线程是相同的。
线程被创建时,这些数据区被分配;线程结束时,这些数据区也就被销毁回收了
对于线程私有的内存区域(程序计数器、虚拟机栈、本地方法栈),它们的大小和结构在编译期或类加载期就已经基本确定了,而不是在运行时动态决定的。
程序计数器(线程私有)
当前线程所执行的字节码的行号指示器
执行状态 | 程序计数器(PC Register)的值 | 原因 |
---|---|---|
执行 Java 方法 | 字节码指令地址 | 需要记录下一条要执行的字节码位置 |
执行 Native 方法 | 空(Undefined) | 执行的是本地机器指令,不在JVM字节码体系内,无需记录字节码地址。 |
它是一个用 native 关键字声明、但没有方法体的Java方法。它的实际实现是由非Java语言(通常是C或C++)编写的,并存在于本地库(如 .dll 文件 on Windows 或 .so 文件 on Linux)中。
Java虚拟机栈(线程私有)
局部变量表
作用:存储方法的参数和方法内部定义的局部变量。
存储单位:以变量槽 为单位。
注意:这里存储的是基本数据类型的值 和对象实例的引用(相当于C语言的指针)。对象实例本身存储在堆 中。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、
float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始
地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress
类型(指向了一条字节码指令的地址),知道接下来到哪里执行。
本地方法栈
和虚拟机栈类似,只是执行native方法
Java堆(GC堆)(各个线程共享) 对象实例
此内存区域的唯一目的就是存放对象实例用于存储所有通过 new 关键字创建的对象实例和数组,静态变量和字符串常量池在JDK 7及以后也移到了堆中
GC堆, “Garbage Collected Heap”——垃圾收集堆,
所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区
(Thread Local Allocation Buffer,TLAB)
虽然堆是共享的,但是为了优化对象分配的性能,JVM在堆中为每个线程划分了一小块私有的缓冲区(TLAB),线程分配对象时先在自己的TLAB中分配,从而避免了竞争。
方法区(各个线程共享)类
它用于存储已被虚拟机加载
的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区存储的是与类结构相关的数据,而不是对象实例。具体包括:
-
类型信息
- 类的完整有效名称(如
java.lang.String
) - 类的直接父类的完整有效名称
- 类的修饰符(
public
,abstract
,final
等) - 类直接实现的一个接口的列表
- 类的完整有效名称(如
-
运行时常量池
- 这是方法区中非常核心的一部分。它存储了:
- 编译期生成的各种字面量:如字符串字面量(
"Hello"
)、声明为final
的常量值。 - 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。这些符号引用在类加载的解析阶段会被转换为直接引用。符号引用 文本描述,逻辑上的引用 。直接引用,内存地址/偏移量,物理上的引用
- 编译期生成的各种字面量:如字符串字面量(
- 这是方法区中非常核心的一部分。它存储了:
-
字段信息
- 每个字段的名称、类型、修饰符(
public
,private
,static
等)。
类的字段指的就是成员变量
- 每个字段的名称、类型、修饰符(
-
方法信息
- 每个方法的名称、返回类型、参数数量和类型、修饰符。
- 方法的字节码、局部变量表大小、操作数栈大小。
- 异常表。
-
静态变量
- 用
static
修饰的变量。 - 特例:静态变量如果被声明为
final
,并初始化为一个编译时常量,则可能会被优化到运行时常量池中。
- 用
-
JIT编译器编译后的代码缓存
- 即时编译器(JIT)将“热点代码”编译成本地机器码后,也会存储在方法区。
- 生命周期:与JVM进程相同。JVM启动时创建,JVM关闭时销毁。
- 垃圾回收:方法区是可以被垃圾回收的,但条件苛刻,回收效率也远低于堆。
- 回收目标主要是:不再使用的类 和 废弃的常量。
- 一个类被判定为“不再使用”需要满足3个条件:
- 该类的所有实例都已被回收。
- 加载该类的
ClassLoader
已被回收。 - 该类对应的
java.lang.Class
对象没有被任何地方引用,无法通过反射访问该类的方法。
运行时常量池(方法区的一部分)
用于存放编译期生成的各种字面量与符号引用。
直接内存
位置:它位于JVM进程之外的本地内存(操作系统管理的内存)中。
分配方式:不是由JVM的垃圾回收器管理,而是通过 ByteBuffer.allocateDirect() 这样的方法,底层调用的是 malloc() 这样的操作系统原生调用来申请内存。
访问方式:在Java中,通过一个特殊的Java对象——DirectByteBuffer——来引用和操作这块内存。这个对象本身在JVM堆上,但它持有一个指向堆外内存地址的指针。
常量存在哪里?
字符串常量:在 堆 中的字符串常量池里。
static final 基本类型和字符串字段:在 方法区 的运行时常量池中。
其他对象类型的 static final 常量:引用在方法区,对象本身在堆中。
智慧:
固定不变的东西(static final 基本类型和字符串字段(实际的字符串常量在堆))放在方法区获得最佳性能
动态可变的东西(对象)放在堆中获得灵活性
引用本身是固定的,所以放在方法区
对象本身是动态的,所以放在堆中
(字符串可以理解成一个对象,字符串字段就是引用,这样就统一了,对象在堆,引用和基本类型在方法区)
Java 7之前字符串常量池在方法区,后来移到堆中以便更好地管理内存
HotSpot虚拟机对象探秘
探讨一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
对象的创建(new )
当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
接下来虚拟机将为新生对象分配内存,
- 分配方式的选择
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。
- 带压缩整理的 指针碰撞
假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一
边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer) - 不带压缩整理的 空闲列表
虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分
配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。
并发分配内存的线程安全问题
方法,:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation
Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值
对象的内存布局(堆中)
对象头,实例数据,对齐填充
对象头
- 存储对象自身的运行时数据,如哈
希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等, - 类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针
来确定该对象是哪个类的实例。
实例数据
在程序代码里面所定义的各种类型的字
段内容
对齐填充
对象的访问定位
直接访问
变量直接指向堆中的对象
对象的内存布局包含:
对象头:包含指向方法区类元数据的指针
实例数据:存储字段的实际值
句柄
句柄包含两个指针:
一个指向堆中的对象实例数据(存储"张三", 25这些具体数据)
一个指向方法区中的类元数据(存储User类的结构信息)
OutOfMemoryError异常
Java堆溢出
。出现Java堆内存
溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Java heap space”。