从零开始的jvm之内存管理机制

该篇源自于对《深入理解java虚拟机》的学习和总结。大牛拍砖请轻点。

1、运行时数据区域

1.1 程序计数器

定义:当前线程所执行的字节码的行号指示器。

设计目的:为了线程切换后能恢复到正确的执行位置,所以需要每个线程都拥有一个独立的程序计数器。

注意:

a、该内存区域线程私有,各线程的程序计数器互不影响,独立储存;

b、执行的是java方法,则计数器记录的是正在执行的指令码的地址。执行native方法,则计数器为空;

c、是唯一一个没有规定oom的内存区域。

1.2 虚拟机栈

定义:用以描述java方法执行的内存模型。即每个方法在执行时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接,方法出口等信息。

局部变量表存放基本数据类型、对象引用、returnAddress类型。

注意:

a、通常说到的java中的栈指的是这里的局部变量表;

b、局部变量表用到的内存空间在编译时完成分配,在方法运行时期不会改变局部变量表的大小。

1.3 本地方法栈

定义:用以描述本地方法执行的内存模型。

区别:与虚拟机栈的区别在于本地方法栈为虚拟机使用到的native方法服务,而虚拟机栈则是为虚拟机使用到的java方法服务。

1.4 堆

定义:所有线程共享,用以存放实例对象的内存区域。

设计目的:唯一目的是存放实例对象。

注意:

a、该内存区域所有线程共享;

b、我们常指的gc一般都是指的对堆的回收,该处的回收率是最高的。

1.5 方法区

定义:用以储存被加载的类信息、常量、静态变量、即时编译器编译后的代码数据的内存区域。

注意:该内存区域所有线程共享。

1.5.1 运行时常量池

方法区的一部分。保存类加载后class常量池的内容和运行时可能产生的新常量。

释义:

class常量池指的是编译器生成的各种字面量和符号引用。

2、对象

2.1 对象的创建

2.1.1 检查

虚拟机遇到一条new指令时,先检测这个指令的参数能否在常量池中定位到一个类的符号引用,再检测这个类符号引用代表的类是否被加载、解析、初始化。如果没有,则先执行类加载过程。

2.1.2 分配内存

检查完后,为对象分配内存(该大小在类加载完成时就能完全确定),如果内存规整,适合使用“内存碰撞”分配内存,否则适合使用“空闲列表”。其中内存是否规则由垃圾回收器是否带有压缩整理功能决定。

注意:

这里的内存分配存在着安全问题,对并发的考虑和解决方案。

解决方案:

1、对分配内存空间的操作进行同步处理;

2、把内存分配的操作按照线程划分到不同的空间中进行,即每个线程预先在堆中分配一块内存,本地线程分配缓冲(TLAB),哪个线程要分配内存,在对应的TLAB中进行分配,只有TLAB用完需要再分配新的TLAB时,才进行同步处理。

2.1.3 初始化

内存分配完成后,将分配的内存空间初始化为零值。

设计目的:保证对象的实例字段在java代码中可以不赋初始值就可以直接使用。

2.1.4 对对象进行设置

主要是对对象头进行一些设置。

2.1.5 执行<init>方法

按照程序员的意愿进行初始化操作,执行完后,才算是创建出一个真正可用的对象。

2.2 对象的内存布局

2.2.1 对象头

对象头由两部分信息。

“Mark Word”:

其中一部分存储对象的运行时数据,具体结构如图:

类型指针:

对象指向它的类元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。

2.2.2 实例数据

实例数据是对象真正储存有效信息的区域,即代码中定义的各种类型的字段内容。

注:储存顺序受虚拟机分配策略参数和字段在java源码中定义顺序的影响。

2.2.3 对齐填充

不是必然存在,仅起占位符的作用。

注意:

对象必须是8字节的整数倍,对象头是8字节的整数倍,所以实例数据没有对齐8的整数倍时,就需要对其填充来补全。

2.3 对象的访问定位

对象访问方式,取决于虚拟机的实现而定。

2.3.1 句柄访问

堆中划分一块内存作为句柄池,reference中储存的是对象的句柄地址,句柄中包含了实例数据和类型数据各自的地址信息。

优势:reference中储存的是稳定的句柄地址。

2.3.1 直接指针访问

reference中直接储存的是对象地址。

优势:访问速度快。

猜你喜欢

转载自blog.csdn.net/qq_35559358/article/details/84972830