一:运行内存示意图
深入学习JVM虚拟机第一步一定是内存划分,只有熟知内存区域结构划分才能更好理解垃圾回收、对象内存分配、参数优化等知识
二:堆内存
几乎所有对象实例都被放在堆内存区域存储,消耗内存最大,GC最频繁的一块内存。这里将是后续学习GC日志分析、垃圾回收策略、参数优化的重点区域
- Eden:实例分配内存区域首先考虑Eden,当然超过最大内存参数设定会放入老年代
- S区域:区域划分为两块,职能交替作用于新生代对象存放与复制算法存储存活对象
- 老年代:主要存放拥有一定GC年龄以及超大内存对象
三:元空间
版本变迁历史导致这块区域的总说纷纭,不同时间节点下结论拥有正确与错误两种答案。首先搞清楚元空间亦或是永久代都是虚拟机规范中方法区的实现,其次知道元空间用于存放类信息、静态变量、class常量池等。字符串常量池与运行时常量池已经移至堆中。更多关于三大常量池的讨论后续章节继续
四:虚拟机栈
- 虚拟机栈为每个执行线程开辟一块线程私有内存
- 虚拟机栈的线程空间中会被划分为一个又一个的栈帧结构,一个方法就是一个栈帧。方法的执行就是栈帧从入栈到出栈的过程
- 栈帧中主要包含局部变量表、操作数栈、返回地址、动态链接等信息
4.1 虚拟机栈内存
4.2 局部变量表
public void localVariationScale(byte b,int i,long l,
float f,double d,Object o){
// 虚拟机栈局部变量表示意
}
顾名思义局部变量表就是用于存储局部变量的区域,方法中的变量都被称之为局部变量,当然方法参数也会存储在这个位置,局部变量表采用数组结构
4.3 操作数栈
public void operandStack(){
int i1 = 20;
int i2 = 30;
int i3 = i2 - i1;
}
操作数栈用于记录数据计算过程量,如下所示过程就是体现操作数栈与局部变量表的数据交互
4.4 动态链接
每个栈帧都会持有当前方法所在类型的常量池引用,通过这个引用在如下两种方式下降引用转换为直接引用操作称之为动态链接:
- 静态解析:发生在类加载或者是第一次使用时转换为直接引用
- 动态解析:每一次运行期间转换为直接引用
4.5 返回地址
方法退出一般会有两种情况,抛出异常亦或是虚拟机解释遇到退出指令。如果是发生了异常返回地址就要异常处理表决定,正常返回一般来说会使用PC计数器。总结一下就是当方法结束时有可能进行下列三个操作:
- 恢复上层方法的局部变量表和操作数栈
- 把返回值压入调用者调用者栈帧的操作数栈
- 调整 PC 计数器的值以指向方法调用指令后面的一条指令
五:本地方法栈
与虚拟机栈类似,只不过栈帧主体有区别。本地方法栈中栈帧方法都是本地方法。这块内容没什么好说的,除非是牛逼到要自定义修改虚拟机实现可能需要这块的支持
六:程序计数器
线程私有的一块内存区域,为什么线程私有呢?这里记录的是线程方法执行的位置,当多线程状态下若线程失而复得CPU资源就需要程序计数器告诉线程已经执行到的位置。并且这块内存区域消耗的内存量及少,基本不会存在OOM等错误,所以研究的意义不大
七:常量池辨析
class常量池、字符串常量池、运行时常量池很多人都没有彻底厘清三者的区别。元空间中提到得移至堆内存区域的常量池就是字符串常量池和运行时常量池,class常量池还是继续分布在元空间中
7.1 class常量池
常量池的每一项常量都是一个表,不同表拥有各自实现。class常量池中主要包含以下三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
7.2 运行时常量池
class常量池产生于类加载时间,类运行期间产生的常量存放于运行时常量池。当然最终要的一点就是class常量池在运行期间会放置到运行时常量池中
7.3 字符串常量池
使用StringTable实现,可以理解为记录字面量字符串内存地址的一张表,其存储内容并不是字符串本身,而是其引用地址。运行时常量池中的符号引用在运行期间转换为直接引用就会查询这张表,保持引用一致。前面文章提到过的intern()实现就是检查字符串常量池表中是否有这个引用,如果没有就加入
7.4 字符串常量池验证
前面讲到运行时常量池以及字符串常量池已经移至堆内存,接下来使用代码查看OOM信息验证结论。分为三步:
- 设置堆内存区域大小
- 产生大量字面量字符串并保持字符串强引用避免回收
- 查看最后OOM信息为Java Heap Space区域抛出的异常