前言
我决定停一段时间的framework,插播一些jvm的学习内容。首先,就是内存区域。学无止境,加油。
运行时的数据区域
根据jvm规范,java虚拟机所管理的内存区域包括图中的几个运行时数据区域,下面,来进行学习。
程序计数器
- 线程私有
程序计数器(Program Counter Register)是一块较小的内存空间,可以看成是线程执行的字节码的行号指示器(执行到多少行字节码指令),在概念模型中,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来实现。
如果虚拟机执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码的指令地址,如果是native方法,这为空。
Java虚拟机栈
- 线程私有
- 生命周期和线程相同
- StackOverflowError
- OutOfMemoryError
每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用到执行完成,就对应一个栈帧在虚拟机栈的入栈和出栈。
局部变量表存放了编译时期的一些数据:
- 8种基本数据类型,
- 引用类型
- 指向对象起点的引用指针
- 代表对象的句柄或其他与此对象相关的地址
- returnAddress类型(指向一条字节码指令的地址)
局部变量表所需要的空间在编译时完成分配,当进入一个方法时,这个方法说需要在帧中分配的空间时完全确定的,在方法运行时间不会修改局部变量表的大小。
本地方法栈
与虚拟机栈类似,只不过本地方法栈为虚拟机使用到的Native方法服务。
Java堆
- 大
- 线程共享
- OutOfMemoryError
几乎所有的对象实例都在这里分配内存(包括数组)
方法区
- 线程共享
- OutOfMemoryError
用于存储已经被虚拟机加载的类信息、常量、静态变量、即使编译后的代码等数据。
运行时常量池
- OutOfMemoryError
这是方法区的一部分,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
直接存储
并不是运行时数据区的一部分,如使用nio。
hotspot虚拟机对象
对象的创建
虚拟机遇到一条new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查是否经过了类加载,如果没有,则执行类加载过程。对象的大小在类加载完之后就确定了。
解析来虚拟机为对象分配内存,(从java堆中划分),有两种分配方式:
- 指针碰撞方式 内存是规整的,中间放着一个指示器,一边是分配了的,一边是空闲的,给对象分配内存的时候,只需要将指示器往空闲的一边移动对象大小的位置就好
- 空闲列表方式 虚拟机维护着一个列表,列表纪录着哪块内存可用,分配内存时,在列表中找一个足够大的划分给对象,更新列表。
这些分配方式,在并发的情况下并不是线程安全的。解决办法:
- cas配合失败重试的方式保持原子性
- 按线程分配在不同的空间中,每个线程在java堆预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
分配完毕之后,将分配到的内存空间初始化为0值(不包括对象头),如果使用了tlab,初始化过程将会提前到tlab分配时,
接下来对对象进行必要的设置,如是哪个对象的实例、对象的hash值、对象的gc年龄代、这些存在对象头中。
最后,进行对象的初始化。
总结下,对象的创建过程如下:
- 碰到new指令
- 执行类加载过程(如果没加载过)
- 在java堆上分配内存
- 内存初始化为0值
- 设置对象头
- 进行对象初始化
对象的内存布局
分为三块区域:
- 对象头 Header
- 对象自身的运行时数据 Mark World
- 类型指针
- 实例数据 Instance Data
- 对齐填充 Padding
Mark World
存储了如hashcode、gc年龄代、锁状态标志、线程持有的锁,偏向线程id,偏向时间挫等
类型指针
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。但是,并不是所有的都有,如数组。
实例数据
对象正在存储的有效信息
对其填充
起占位符的作用,保证对象的大小是8字节的整数倍。
对象的访问定位
目前访问对象有两种方式:
- 使用句柄
- 直接指针
使用句柄
java堆分配出一块区域当作句柄池,句柄中包含了对象实例数据与类型数据各自具体的地址信息。
- 稳定,不需要根据对象移动而修改
- 访问速度慢
直接地址
存储的直接是对象地址。
- 访问速度快
参考资料
- 深入理解java虚拟机