JVM系列1-类加载机制

类加载机制

java语言规范( Java Language Specification)
查看官方文档可以知道Java类加载步骤为 Loading(装载)、Linking(链接)、Initialization(初始化)

装载

步骤

查找和导入class文件, 分为以下3个步骤

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

类加载器

在装载(Load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的。

JVM里面有三种类加载器。BootstrapClassLoader(引导类加载器)ExtClassLoader(扩展类加载器)AppClassLoader(系统类加载器) JVM采用双亲委派模型来加载class文件.
类加载器在加载class类的时候会先委托给上一层的类加载器进行加载,除非上层类加载器无法加载这个类才会自己创建

image

双亲委派机制:这样的好处是避免了class类的重复创建,保护核心资源不被篡改

链接

链接分为3个步骤:验证(Verify) --》准备(Prepare) --》解析(Resolve)

验证

保证被加载类的正确性

  • 文件格式验证:验证字节流是否符合Class文件规范,能否被当前版本虚拟机处理

  • 元数据验证:对字节码描述的信息进行语义分析 如:这个类是否有父类(除了java.lang.Object之外,所有的类都应该父类,没有显式继承父类的类,都默认继承java.lang.Object类),这个类是否继承了不允许被继承的类,非抽象类是否实现了父类中或接口中的所有抽象方法,类中的字段、方法是否与父类产生矛盾等

  • 字节码验证:这是整个验证阶段过程中最复杂的一个阶段,主要是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段的基础上,此阶段对类的方法体进行校验分析,保证被校验类不会在运行时作出危害虚拟机的行为,如:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,保证任何跳转指令都不会跳转到方法体以外的字节码指令上等

  • 符号引用验证:这个验证发生在虚拟机将符号引用化为直接引用的时候,这个转化发生在解析阶段。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配校验。如:符号引用种通过字符串描述的全限定名是否能找到对应的类,符号引用中的类、字段、方法、的可访问性是否可被当前类访问。

准备(Prepare)

为类中定义的变量,即静态变量(static修饰的)分配内存并设置变量的初始值。如果是final类静态变量,也可能在准备阶段直接赋值。注意此阶段内存分配仅包括类变量,而实例变量会在对象实例化时随着对象一起分配在堆中

解析(Resolve)

在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段

  • 符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。

  • 直接引用: 直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。、

直接引用可以是:
1. 直接指向目标的指针。(指向对象,类变量和类方法的指针)
2. 相对偏移量。(指向实例的变量,方法的指针)
3. 一个间接定位到对象的句柄。

初始化

初始化是类加载的最后一步,在之前的几步里,只有“加载”阶段用户可以自定义类加载器参与其中,后面都是由JVM主导控制的,直到初始化开始,JVM才开始真正执行Java程序代码,将主导权移交给应用程序

运行时数据区

class文件被类加载器装载进JVM虚拟机,需要将类文件的信息存储起来,存储的位置肯定是在JVM中有对应的空间。
JVM将运行时数据区分成了方法区、堆、本地方法栈、Java虚拟机栈、程序计数器,

方法区

方法区是各个线程共享的内存区域,在虚拟机启动时创建。
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError(OOM)异常。

方法区在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space

Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。
Java对象实例以及数组都在堆上分配。

Java虚拟机栈

虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。
调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

本地方法栈

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。调用的是底层C语言的方法

程序计数器

程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。

字节码文件

通过命令javap反编译得到字节码文件

javap -c Person.class Person.txt

官网反编译指令说明:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

参考文章:

java类加载和初始化的区别:https://blog.csdn.net/weixin_30894765/article/details/114257957
java8官方文档:https://docs.oracle.com/javase/8/

posted @ 2021-09-14 22:43  狻猊的主人  阅读(66)  评论(0编辑  收藏  举报