四、运行时内存
4.1程序计数器
定义
为了保证程序(在操作系统中理解为进程)能够连续地执行下去,CPU必须具有某些手段来确定下一条指令的地址。而程序计数器正是起到这种作用,所以通常又称为指令计数器。
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
作用
1.在程序开始执行前,必须将它的起始地址,即程序的一条指令所在的内存单元地址送入PC,因此程序计数器(PC)的内容即是从内存提取的第一条指令的地址。当执行指令时,CPU将自动修改PC的内容,即每执行一条指令PC增加一个量,这个量等于指令所含的字节数,以便使其保持的总是将要执行的下一条指令的地址。
2.由于大多数指令都是按顺序来执行的,所以修改的过程通常只是简单的对PC加1。
3.当程序转移时,转移指令执行的最终结果就是要改变PC的值,此PC值就是转去的地址,以此实现转移。有些机器中也称PC为指令指针IP( Instruction Pointer)。
PC寄存器
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。执行引擎的字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
PC寄存器为什么会被设定为线程私有?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
为什么执行native方法是,是undefined
native 本地方法是大多是通过C实现,并未编译成需要执行的字节码指令,所以在计数器中当然是空(undefined)。
4.2栈
它是一种运算受限的线性表,是线程私有的,主管程序运行,生命周期和线程同步,线程结束,栈内存就释放了。不存在垃圾回收问题。默认1024k
4.2.1栈帧
在这个线程上正在执行的每个方法都各自对应一个栈帧〈Stack Frame) .枝帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
4.2.2栈帧的结构
1.局部变量表
局部变量表也被称之为局部变量数组或本地变量表,局部变量表所需的容量大小是在编译期确定下来的
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型(8种)、对象引用(reference),以及returnAddress类型。
注意:非静态方法默认会有this存储在局部变量表中下标0的位置,double和long会占用两个slot(槽位)一个slot占用四个字节
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
2.操作数栈
在解释执行过程中,每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。
具体来说便是:执行每一条指令之前,Java 虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java 虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。
3.动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)
作用:将符号引用转换为调用方法的直接引用
4.方法返回地址
只要在本万法的异常表中没有搜索到匹配的异常处理器,就会导致万法退出。简称异常完成出口。
5.一些附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
4.3 本地方法栈
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
1.本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。·
2.甚至可以直接使用本地处理器中的寄存器
3.直接从本地内存的堆中分配任意数量的内存。
4.4堆
一个VM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
堆内存的大小是可以调节的。
堆,是GC ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
对象分配原则
1.优先分配到Eden
2.大对象直接分配到老年代
· 尽量避免程序中出现过多的大对象
3长期存活的对象分配到老年代
4.动态对象年龄判断
如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年
龄大于或等于该年龄的对象可以直接进入老年代,无须等MaxTenuringThreshold中要求的年龄。
5.空间分配担保
-XX: HandlePromotionFailure
空间分配担保(jdk6之后默认开启)
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
如果大于,则此次Ninor Gc是安全的
如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Ninor Gc,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
TLAB
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。(默认栈Eden空间的1%)
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
4.4方法区
HotSpot虚拟机:jdk1.7之前方法区称之为永久代,jdk8开始,使用元空间(方法区的具体实现)取代了永久代
元空间与永久代最大的区别在于:元空间不在虚打机设置的内存中,而是使用本地内存。|
方法区溢出测试
public class OOMTest extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
OOMTest test = new OOMTest();
for (int i = 0; i < 10000; i++) {
//创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指明版本号,修饰符,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
test.defineClass("Class" + i, code, 0, code.length);//Class对象
j++;
}
} finally {
System.out.println(j);
}
}
}
五、对象的内存布局
5.1对象创建的步骤
5.1.1 判断对象对应的类是否加载,连接,初始化
虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。(即判断类元信息是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以classLoader+包名+类名为Key进行查找对应的.class 文件。如果没有找到文件,则抛出classNotFoundException异常。·如果找到,则进行类加载,并生成对应的Class类对象。
5.1.2 为对象分配内存
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。 如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
- 指针碰撞(内存空间连续) 是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离。如果垃圾收集器选择的是Serial old、Par old这种基于压缩算法的,虚拟机采用这种分配方式
- 空闲列表(内存空间零散) 虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式称为“空闲列表(Free List)
5.1.3 处理并发问题
在分配内存空间时,另外一个问题是及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用了两种方式解决并发问题:
- CAS ( Compare And Swap )失败重试、区域加锁:保证指针更新操作的原子性;.
- TLAB 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区
5.1.4初始化分配到的空间
内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保正了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
5.1.5设置对象头的信息
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
5.1.6执行init初始化方法
初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
5.2对象的结构
5.2.1对象头
- 1)对象自身的运行时元数据(mark word)
- 哈希值(hashcode):对象在堆空间中都有一个首地址值,栈空间的引用根据这 个地址指向堆中的对象,这就是哈希值起的作用
- GC分代年龄:对象首先是在Eden中创建的,在经过多次GC后,如果没有被进行回收,就会在survivor中来回移动,其对应的年龄计数器会发生变化,达到阈值后会进入养老区
- 锁状态标志,在同步中判断该对象是否是锁·线程持有的锁
- 线程偏向ID
- 偏向时间戳
- 2)类型指针,指向元数据区的类元数据InstanceKlass,确定该对象所属的类型
5.2.1实例数据
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
放置原则:
- 相同宽度的字段总是被分配在一起
- 父类中定义的变量会出现在子类之前(因为父类的加载是优先于子类加载的>
- 如果CompactFields参数为true(默认为true):子类的窄变量可能插入间父类变量的空隙
5.2.3对齐填充
占位符的作用
5.3对象的访问定位
1.直接访问
2.句柄访问
- 实现:堆需要划分出一块内存来做句柄池,reference中存储对象的句柄池地址,句柄中包含对象实例与类型数据各自具体的地址信息。