问懵逼:你能讲讲 JVM 运行时内存空间吗?
JVM内存空间
JVM 规范在程序运行期间定义了不同的数据区域,有一些区域跟随 JVM 的创建销毁,而有些区域则是线程独有的,线程独有的区域会跟随线程的创建与销毁。
在不同版本和不同厂商的 JVM 版本中都会有较大差异,本文基于 JDK8 HotSpot 虚拟机进行的总结。
JVM 规范内的运行时数据区域
程序计数器(The pc Register)
JVM 支持多线程,每个线程都有自己的程序计数器。当线程执行中的时候,程序计数器会包含线程的执行点(前提是方法不是 native 方法)。
虚拟机栈(Java Virtual Machine Stacks)
在线程创建的同时,线程会拥有私有的虚拟机栈(线程不共享)。虚拟机栈中会储存栈帧(Frames)。栈帧中会持有本地变量,部分结果,方法的调用和返回。虚拟机栈只会对栈进行压栈和出栈操作,栈帧可能由堆(heap)分配。虚拟机栈在内存中并不需要是连续的。JVM 规范规定具体的虚拟机实现必须允许用户对虚拟机栈的大小进行调整。
堆(Heap)
堆内存在 JVM 内线程共享的。堆内存为所有的类示例和数组进行内存分配。堆内存创建于 JVM 的启动阶段,堆中的对象存储由垃圾收集器进行管理,对象从不会明确已经被回收。JVM 不承担对象的管理,具体实现由各厂商根据系统需求实现。堆内存的大小是可用动态扩展的,并且堆内存不必是连续的。
方法区(Method Area)
方法区也是 JVM 中的线程共享的。存储类的结构,例如运行时常量池,字段和方法数据和结构方法。
方法区创建于 JVM 的启动阶段。方法区在逻辑上是堆内存的一部分。方法区的简单实现可以选择不对其进行垃圾回收和整理。HotSpot 的实现会对此进行垃圾收集。方法区的大小应该设计为可调整,在内存中可以不连续。
运行时常量池(Run-Time Constant Pool)
运行时常量池是每个类与接口在运行时才能确定的常量。例如 Sting const = UUID.randomUUID().toString(); 每一个运行时常量池都是从方法区中分配的。
本地方法栈(Native Method Stacks)
本地方法栈在线程创建的时候进行分配,是线程独有的。JVM 规范规定本地方法栈可以是固定大小,也可以是可调整的。
HotSpot 虚拟机的运行时内存结构,jmc 查看内存划分:
在上图中可以看到内存区域主要分为两大类:heap、Non-heap。
虚拟机栈(Stack)
线程独有的内存空间,每个方法在执行时候会形成一个栈帧,用于存在这个方法的局部变量、操作数栈、动态链接、方法返回等信息。
程序计数器(The pc Register)
线程独有,描述的是 JVM 执行过程中程序的执行顺序,保证在 CPU 切换过程中对执行方法顺序的记录。
本地方法栈(Native Method Stacks)
native 方法的区域与虚拟机栈类似。HotSpot 的 JVM 中将本地方法栈和虚拟机栈合二为一了。
堆(Heap)
一般来说 new 出来的对象放在堆,但是由于 Java 中不能直接使用对象的,而是通过引用,而对象的引用放在虚拟机栈,是局部变量表中的一个局部变量。
在堆中创建的对象会有持有指向方法区的指针,因为对象的类信息存储在方法区中。由于 GC 都采用分代收集算法,所以堆内存的对象也进行了相应的划分。堆内存在物理上可以是连续,也可以是不连续的。
方法区(MethodArea)
在 HotSpot 的实现中将元空间(MetaSpace)放在方法区中,线程共享的区域,存在类的信息,常量,静态变量等。进行 GC 的可能性较低。
从 JDK8 后就已经没有了永久代,使用 metaSpace(元空间)。
官方说法:
-
JDK8 不再有永久代; -
类的元信息被存储在一个信息空间,称为 MetaSpace; -
与堆内存是不连续的; -
元空间是从本地内存中分配的; -
MetaSpace 最大可用空间就是系统可用内存; -
可以被 MaxMetaspaceSize 参数进行限制;
直接内存(Direct memory)
与 NIO 相关,JVM 通过堆上的 DirectByteBuffer 操作直接内存。
字节码缓存(code cache)
Non-heap 区域用于存储由 JIT 编译期生成的编译后代码。
-
由内存直接分配; -
由 Code Cache 清理器管理;
总结图
关于堆内存是线程共享的说法其实并不严谨,因为还存在 LTAB,在后续会继续学习记录。
创建对象的过程
使用 new 关键字创对对象实例。JVM 会判断这个对象对应的类是否已经被加载,如果没有则进行类加载过程。
-
1. 指针碰撞(内存中被占用与未占用内存空间分隔开); -
2. 空闲列表(内存中被占用和未被占用的空间交织在一起); 两种方法与 GC 的机制有关,要看具体 GC 发生后是否会对对象进行压缩和移动。
对象在内存中的布局
-
1. 句柄:堆数据一部分是对象在堆中的指针,另一部分是对象的类信息指针,类信息指针指向方法区; -
2. 指针:堆中一部分数据存放实例对象,另一部分存放类信息的指针;
JVM运行时数据区域
这个例子中生成了 2 部分的内存区域:
-
obj 这个引用变量,因为是方法内的变量,所以放到 stack 里面; -
真正 Object class 的实例对象,放在 Heap 里面;
这个例子中,一共消耗 12bytes,JVM 规定引用占 4 个 bytes,空对象占 8bytes。当方法执行结束后,对应 stack 中的变量马上回出栈,而 Heap 中的对象需要等待 GC 进行回收。
Compressed Class Space&UseCompressedOops
MetaSpace 默认会将元数据和类信息放在同一个区域,当 UseCompressedClassesPointers 启用后会将类信息和元数据分为两部分存储,MaxMetaspaceSize 将会设置两部分空间的上限。
启用前的存储形式:
启用后的存储形式:
虚拟机参数 -XX+UseCompressedOops 使用的效果是,当从 32bit 的虚拟机迁移到 64bit 的虚拟机上,JVM 会将部分的指针进行压缩,防止在 64bit 系统中占用更大的内存。
TLAB
TLAB(Thread Local Allocation Buffer) 是用于多线程分配对象的一个堆内存区域,能够提升多线程下并发创建对象造成竞争的性能下降,TLAB 位于年轻代:
空间分配担保
当发生 MinorGC 时,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那么 MinorGC 可以确保是安全的。
当大量对象在 MinorGC 后仍然存活,就需要老年代进行分空间分配担保,把 Survivor 无法容纳的对象直接进入老年代。
如果老年代判断剩余空间不足(根据以往每一次回收晋升到老年代对象容量的平均值作为判定值)进行 FullGC。
点击左下角阅读原文查看历史经典技术问题汇总,看完顺手走一波PYQ呀~