深入理解Java虚拟机,第二章,java内存区域与内存溢出异常

时间:2022-12-28 12:58:44

Java 虚拟机的内存空间分为 5 个部分:

  • 程序计数器     
  • Java 虚拟机栈
  • 本地方法栈
  • 方法区

深入理解Java虚拟机,第二章,java内存区域与内存溢出异常

JDK 1.8 同 JDK 1.7 比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

 

1、程序计数器(线程私有)

一小块内存空间,是当前线程所执行的字节码的行号指示器。为了线程切换后能恢复到正常的位置,每个线程都有一个独立的程序计数器。

线程执行java方法,记录的是正在执行的字节码指令地址。

本地native方法,计数器为空。

虚拟机中唯一没有规定任何OutOfMemoryError的区域

2、虚拟机栈(线程私有,生命周期与线程相同,因此内存回收主要发生在堆中)

Java 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息,如:

  • 局部变量表  (朝生夕死,对象引用在栈里,对象实例则在堆里,需要垃圾回收机制进行回收)
  • 操作数栈
  • 动态链接
  • 方法出口信息
  • ......

深入理解Java虚拟机,第二章,java内存区域与内存溢出异常

 

深入理解Java虚拟机,第二章,java内存区域与内存溢出异常

 

Java 虚拟机栈的栈顶的栈帧是当前正在执行的活动栈,也就是当前正在执行的方法,PC 寄存器也会指向这个地址。

只有这个活动的栈帧的本地变量可以被操作数栈使用,当在这个栈帧中调用另一个方法,与之对应的栈帧又会被创建,新创建的栈帧压入栈顶,变为当前的活动栈帧。

Java 虚拟机栈的特点

  • 局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。
  • Java 虚拟机栈会出现两种异常:*Error 和 OutOfMemoryError。
    • *Error 若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 *Error 异常。
    • OutOfMemoryError 若允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OutOfMemoryError 异常。
  • Java 虚拟机栈也是线程私有,随着线程创建而创建,随着线程的结束而销毁。

3、本地方法栈(线程私有)

非java方法,如c的方法,也会抛出*Error 和OutOfMemoryError 

4、堆(线程共享,垃圾回收的主要区域,分代收集,新生代和老年代

存放对象实例

(1)线程共享

整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个的。

(2)在虚拟机启动时创建。

(3)垃圾回收的主要场所。

(4)可以进一步细分为:新生代、老年代。

 

5、方法区(线程共享)

类信息,常量(final),静态变量(static),编译后的代码

  特点

  1. 线程共享 方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。
  2. 永久代 方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为老年代。
  3. 内存回收效率低 方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。 对方法区的内存回收的主要目标是:对常量池的回收 和 对类型的卸载。
  4. Java虚拟机规范对方法区的要求比较宽松。 和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。

 

6、常量池(方法区一部分)

存放类的版本、字段、方法、接口等,编译期生成的各种字面量和符号引用。

 

二、对象的创建

 

当虚拟机遇到一条含有new的指令时,会进行一系列对象创建的操作:

 

(1)检查常量池中是否有即将要创建的这个对象所属的类的符号引用;

 

  • 若常量池中没有这个类的符号引用,说明这个类还没有被定义!抛出ClassNotFoundException;(类的加载,说明new的时候会去加载目标类)
  • 若常量池中有这个类的符号引用,则进行下一步工作;

 

(2)进而检查这个符号引用所代表的类是否已经被JVM加载;

 

  • 若该类还没有被加载,就找该类的class文件,并加载进方法区;
  • 若该类已经被JVM加载,则准备为对象分配内存;

 

(3)根据方法区中该类的信息确定该类所需的内存大小;

 

一个对象所需的内存大小是在这个对象所属类被定义完就能确定的!且一个类所生产的所有对象的内存大小是一样的!JVM在一个类被加载进方法区的时候就知道该类生产的每一个对象所需要的内存大小。

 

(4)从堆中划分一块对应大小的内存空间给新的对象;分配堆中内存有两种方式:

 

  • 指针碰撞 如果JVM的垃圾收集器采用复制算法或标记-整理算法,那么堆中空闲内存是完整的区域,并且空闲内存和已使用内存之间由一个指针标记。那么当为一个对象分配内存时,只需移动指针即可。因此,这种在完整空闲区域上通过移动指针来分配内存的方式就叫做“指针碰撞”。
  • 空闲列表 如果JVM的垃圾收集器采用标记-清除算法,那么堆中空闲区域和已使用区域交错,因此需要用一张“空闲列表”来记录堆中哪些区域是空闲区域,从而在创建对象的时候根据这张“空闲列表”找到空闲区域,并分配内存。 综上所述:JVM究竟采用哪种内存分配方法,取决于它使用了何种垃圾收集器。

 

(5)为对象中的成员变量赋上初始值(默认初始化);   成员变量初始化值,局部变量没有初始化值

 

(6)设置对象头中的信息;

 

(7)调用对象的构造函数进行初始化;

 

此时,整个对象的创建过程就完成了。

 

 

三、对象模型

对象头 :哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

实例数据  : 成员变量的值

对齐填充 :确保对象的总长度为8字节的整数倍

 

四、对象访问()

句柄访问方式

堆中需要有一块叫做“句柄池”的内存空间,句柄中包含了对象实例数据与类型数据各自的具体地址信息。

引用类型的变量存放的是该对象的句柄地址(reference)。访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址找到对象。

深入理解Java虚拟机,第二章,java内存区域与内存溢出异常

直接指针访问方式

引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。但对象所在的内存空间需要额外的策略存储对象所属的类信息的地址。

深入理解Java虚拟机,第二章,java内存区域与内存溢出异常

需要说明的是,HotSpot 采用第二种方式,即直接指针方式来访问对象,只需要一次寻址操作,所以在性能上比句柄访问方式快一倍。但像上面所说,它需要额外的策略来存储对象在方法区中类信息的地址。

时间空间互相转换,达到目的

 

参考:https://zhuanlan.zhihu.com/p/33542222