JVM学习笔记(三):Java内存区域 && 内存溢出异常
Java 内存区域
Heap
线程公有
存放实例对象
是GC主要管理区域,因此可以更细致的划分为:新生代、老年代
再细致一点划分:Eden区、From Survivor区、To Survivor区
内存空间:可以物理上不连续、逻辑上连续即可。
Method Area
线程公有
主要存储:类信息、常量、静态变量、编译后的代码
运行时常量池
主要存储:编译期的字面量以及符号引用
具有动态性,即可以在运行时将常量放入池中。
VM Stack
线程私有
主要包括:
- 局部变量表:存放编译期的各种基本数据类型、对象引用、returnAddress类型
- 操作数栈:每一个元素可以为任意的java类型,32位数据类型所占容量为1,64位数据类型所占容量为2
- 动态连接:class文件的常量池中有大量的符号引用,这些符号引用有一部分是在类加载阶段或者在第一次使用的时候就转换为直接引用,这部分成为静态解析。另一部分是每一次运行的时候转化为直接引用,这部分即为动态连接。
- 方法出口:例如A方法中调用了B方法,B方法的返回值压入A方法的栈帧中。
Native Method Stack
线程私有
与VM Stack相似,唯一区别在于该栈为Native方法服务。
Hot Spot 将VM Stack 与 Native Method Stack 合而为一。
Program Counter Register
线程私有
用于记录线程执行字节码的指令的地址。
Direct Memory
NIO中使用直接内存,提高效率。
对象创建过程
- 首先当虚拟机遇到一条new指令时,先去检查该符号引用代表的类是否已经完成类加载,若未完成,则执行以下步骤
- 类加载
- 为对象分配内存
分配方式:指针碰撞/空闲列表
线程安全:CAS解决 - 虚拟机初始化内存空间
- 虚拟机对对象进行必要的设置
- 执行完成初始化
- 对象创建完成
对象内存布局
- 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。
- 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
- 如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。
在 32 位系统下,存放 Class 指针的空间大小是 4 字节,Mark Word 空间大小也是4字节,因此头部就是 8 字节,如果是数组就需要再加 4 字节表示数组的长度,如下表所示:
在 64 位系统及 64 位 JVM 下,开启指针压缩,那么头部存放 Class 指针的空间大小还是4字节,而 Mark Word 区域会变大,变成 8 字节,也就是头部最少为 12 字节,如下表所示:
压缩指针:开启指针压缩使用算法开销带来内存节约,Java 对象都是以 8 字节对齐的,也就是以 8 字节为内存访问的基本单元,那么在地理处理上,就有 3 个位是空闲的,这 3 个位可以用来虚拟,利用 32 位的地址指针原本最多只能寻址 4GB,但是加上 3 个位的 8 种内部运算,就可以变化出 32GB 的寻址。
对象访问定位
两种方式:
- 句柄池:引用中存储的是句柄地址,当实例对象移动时,只需要改变句柄对应的指针,不需要改变引用本身,比较稳定。
- 直接指针:速度快,节省了一次指针定位的开销。
常用指令
- invokeinterface:用以调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokevirtual:指令用于调用对象的实例方法,根据对象的实际类型进行分派
- invokestatic:用以调用类方法
- invokespecial:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
- invokedynamic:JDK1.7新加入的一个虚拟机指令,相比于之前的四条指令,他们的分派逻辑都是固化在JVM内部,而invokedynamic则用于处理新的方法分派:它允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断,从而达到动态语言的支持。
基于栈的指令集 && 基于寄存器的指令集
基于栈的指令集
java采用的是基于栈的指令集,这种指令集依赖操作数栈进行工作
优点:
- 可移植:由于寄存器是由硬件直接提供的,所以程序如果依赖寄存器不可避免的会受到硬件的约束
- 程序代码紧凑
- 编译器实现简单
缺点:
- 速度慢
- 指令数量多:完成相同功能所需的指令比寄存器架构多,因为光是入栈、出栈就已经很多指令了
- 内存访问多:频繁的栈访问意味着频繁的内存访问,而对于处理器来说,内存始终是速度的瓶颈。