JVM(Java Virtual Machine) 即 Java 虚拟机, 是一种用于计算设备的规范, 用于运行 Java 程序编译后得到的字节码文件(Class 文件)
一. JVM 的内存区域
1. 程序计数器(Programing Counter Register)
用于选取需要 JVM 执行的字节码指令, 最简单的一种方法就是通过修改程序计数器的值来达到选取下一条需要执行的字节码指令的目的.
每个线程都会有一个独立的程序计数器, 即这块内存是属于线程私有 (或者说线程隔离) 的. 若是执行的是 Native 修饰的方法, 则计数器值为空(Undefined).
此外, 程序计数器所在的这块内存区域是唯一一个在 JVM 规范中没有任何 OutOfMemoryError 情况的区域
2.Java 虚拟机栈(也就是我们常说的栈内存)
是线程私有的. 每个方法在执行时都会创建一个栈帧(方法运行时的基础数据结构), 用来存储局部变量, 动态链接等.
其中, 局部变量表中存放: 基本数据类型 (boolean,byte,char,short,int,long,float,double) 和对象引用类型(reference 类型, 如存储引用的变量?)
在栈帧中, 64 位长度的 long,double 都会占用两个局部变量空间(Slot), 其余数据类型均只占用 1 个 Slot
虚拟机栈会有两种异常情况:(1)线程请求的栈深度大于虚拟机所允许的深度, 会抛出 StackOverflowError 异常
(2)当虚拟机栈可以动态扩展时, 若扩展时无法申请到足够内存, 则会抛出 OutOfMemoryError 异常
3.Java 堆(Java Heap)
是所有线程共享的. 同时也是 JVM 管理内存中最大的一块. JVM 规范规定: Java 堆可以处于物理上不连续的内存空间, 只要逻辑上是连续的即可.
JVM 规范同时规定: 所有对象实例以及数组都要在堆上分配. 即 Java 堆是用来存储对象实例和数组.
Java 堆也是垃圾收集器管理的主要区域, 因此有别名 "GC 堆"(Garbage Collection Heap).
当 Java 堆没有内存分配实例对象, 且堆再也无法扩展时, 会抛出 OutOfMemoryError 异常
4. 方法区
是所有线程共享的. JVM 规范中将方法区描述为堆的一个逻辑部分, 但是却有一个别名叫 Non-Heap.
方法区是用来存储 JVM 加载的类信息(即类的模板?), 常量, 静态变量, 即时编译器编译后的代码等数据的.
和 Java 堆一样不需要连续的内存, 且可以选择固定大小或扩展, 还可以选择不实现垃圾回收(该区域的回收效率低下, 且条件苛刻; 但这区域回收是必要的).
这区域的垃圾回收主要是针对常量池的垃圾回收和对类型的卸载.
当方法区无法满足内存分配需求时, 会抛出 OutOfMemoryError 异常.
5. 运行时常量池(Runtime Constant Pool)
方法区的一部分.
用于存放编译期生成的各种字面量和符号引用(如字符串常量); 具备动态性, 运行期间也可能将新的常量放入池中.
String 类的 intern()方法 ----- 去常量池中寻找当前字符串; 若存在, 返回找到与当前字符串值一样的字符串地址; 若不存在, 则将当前字符串存入常量池, 再返回该地址.
当常量池无法再申请到内存时, 会抛出 OutOfMemoryError 异常.
总结: 线程隔离的有: 程序计数器, Java 虚拟栈. 虚拟栈有一个 StackOverflowError 异常
线程共享的有: Java 堆, 方法区. 方法区中有一个运行时常量池.
二. 对象的创建
1. 虚拟机遇到一条 new 指令时, 首先进行类加载检查(是否能在常量池中找到类的符号引用, 符号代表的类是否已被加载, 解析, 初始化等).
2. 通过类加载检查后, 在 Java 堆中分配新内存. 分配方式有:(1)指针碰撞: 已用内存和空闲内存分置两边, 中间设置一个指针作为分界点, 每次通过移动指针来分配内存
(2)空闲列表: 已使用内存和空闲内存相互交错, JVM 维护一个列表, 记录哪些内存块是可用的, 分配时划分一块足够大的空间给对象
除了两种分配方式外, 考虑到并发情况下的线程安全问题, 提出了两种解决方案:
(1)同步处理
(2)本地线程分配缓冲(Thread Local Allocation Buffer, TLAB) : 每个线程在 Java 堆中预先分配一小块内存, 当某线程的 TLAB 用完后再分配新的 TLAB 时, 才需要同步锁定
3. 内存分配完成后, JVM 将分配到的内存空间初始化为零值.--- 以保证对象的实例字段可以不赋初始值就能直接使用(这一块没弄懂)
4. 对对象进行必要的设置. 如对象头中设置对象的哈希码, 对象的 GC 分代年龄, 对象是哪个类的实例的标志等
三. 对象的内存布局
对象在内存中布局可以分为三块
1. 对象头: 可分为两部分 :(1)第一部分用于存储对象自身的运行数据. 如哈希码, GC 分代年龄, 锁状态标志, 线程持有的锁等
(2) 第二部分是类型指针, 即对象指向它的类元数据的指针. JVM 可通过这个指针确定对象是哪个类的实例
但并不是每个 JVM 都会在对象数据上保留类型指针, 因为还有其他方法可以查找对象的元数据.
2. 实例数据: 对象真正存储的有效信息, 即程序代码中定义的各种类型的字段.
如父类继承的, 子类重定义的等
3. 对齐填充: 这部分不是必然存在的. 仅仅起着占位符的作用. 以 HotSpot 虚拟机为例, 对象起始地址必须是 8 字节的整数倍, 即对象大小必须是 8 字节的整数倍.
而对象头部分正好是 8 字节, 当实例数据部分没有对齐时, 则需要通过对齐填充来补全.
此外, 对象的主流访问定位方式有两种:(1)使用句柄: 优点: 引用中存储的是稳定的句柄地址, 句柄中存放对象实例数据指针和对象类型指针, 当对象移动时(垃圾回收时的普遍行为), 只需改变句柄中的实例数据指针.
(2)直接指针: 优点 -- 快, 节省时间开销 -- 地址指向的一块内存中既有对象实例数据, 也有对象类型数据指针.
使用句柄访问时, 会在 Java 堆中划分出一块内存出来作为 句柄池
即句柄池也在 Java 堆中.
来源: https://www.cnblogs.com/xiang9286/p/10477970.html