java虚拟机(JVM)堆、栈、方法区、类加载器的详解

1.JVM的基本结构图

jvm基本结构图
由上图可知JVM基本结构包括:类加载子系统、内存区、执行引擎、本地库接口 。

2.JVM的各区域详解图

在这里插入图片描述
通过该图,我们对jvm会有一些基础的认识,下面我们详细讲解一下各区域。

3.JVM运行时数据区各区域详解

3.1程序计数器(Program Counter (PC) Register)

内存空间小,线程私有。字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。

如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

3.2Java 虚拟机栈(Java Virtual Machine Stacks)

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

3.3 本地方法栈(Native Method Stacks)

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

3.4 堆内存 (Heap Memory)

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

3.5 方法区(Method Area)

属于共享内存区域,存储已被虚拟机加载的类信息(包括类的名称、方法信息、字段信息)、常量、静态变量、即时编译器编译后的代码等数据。

在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。

当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆)。

下面用一张图来介绍上述区域存储的内容。
在这里插入图片描述

4.虚拟机类加载机制

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
在 Java 语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。

4.1 类的生命周期

在这里插入图片描述
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)。

以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):

  1. 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
  3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
  4. 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。

4.2 类的加载过程

4.2.1 加载

  1. 通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。

数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:

  1. 如果数组的组件类型是引用类型,那就递归采用类加载加载。
  2. 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
  3. 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。

内存中实例的 java.lang.Class 对象存在方法区中。作为程序访问方法区中这些类型数据的外部接口。加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。

4.2.2 验证

是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。

文件格式验证

  1. 是否以魔数 0xCAFEBABE 开头
  2. 主、次版本号是否在当前虚拟机处理范围之内
  3. 常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
  4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
  5. CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
  6. Class 文件中各个部分集文件本身是否有被删除的附加的其他信息
  7. ……

只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。

元数据验证

  1. 这个类是否有父类(除 java.lang.Object 之外)
  2. 这个类的父类是否继承了不允许被继承的类(final 修饰的类)
  3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
  4. 类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)

这一阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。

字节码验证

  1. 保证任意时刻操作数栈的数据类型与指令代码序列都鞥配合工作(不会出现按照 long 类型读一个 int 型数据)
  2. 保证跳转指令不会跳转到方法体以外的字节码指令上
  3. 保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
  4. ……

这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。

符号引用验证

  1. 符号引用中通过字符串描述的全限定名是否能找到对应的类
  2. 在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
  3. 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
  4. ……
    最后一个阶段的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。
    符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

4.2.3 准备

这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。

public static int value = 1127;

这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 1127 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段才会对 value 进行赋值。

基本数据类型的零值

特殊情况:如果类字段的字段属性表中存在 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 1127。

4.2.4 解析

这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  1. 符号引用
    符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量。
  2. 直接引用
    直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和迅疾的内存布局实现有关

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。

4.2.5 初始化

前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

4.3 类加载器

通过一个类的全限定名来获取描述此类的二进制字节流。

4.3.1 双亲委派模型

从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)

1. 启动类加载器
加载 lib 下或被 -Xbootclasspath 路径下的类

2.扩展类加载器
加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类

3.引用程序类加载器
ClassLoader负责,加载用户路径上所指定的类库。

在这里插入图片描述
除顶层启动类加载器之外,其他都有自己的父类加载器。
工作过程:某一个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,则成功返回;如果父类加载器无法完成加载任务,将抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载,依次类推。

4.3.2 双亲委派模型的作用

1. 保证了JVM提供的核心类不被篡改,保证class执行安全

比如上文的string类,无论哪个加载器要加载这个类的话,由于双亲委派机制,最终都会交由最顶层的启动类加载器来加载,这样保证了string类在各种类加载器环境中,都是同一个类。试想下,没有双亲委派机制的话,各个加载器自己加载string类,有可能不同类加载器加载的string方法不一样,那样的话,我们的程序是不是就会一片混乱了。

2. 防止重复加载同一个class

猜你喜欢

转载自blog.csdn.net/cyb_123/article/details/108513384
今日推荐