JVM类加载机制详解

一个类从被加载到虚拟机内存中开始到卸载内存为止,它的整个生命周期会经历加载,验证,准备,解析,初始化,使用,卸载七个阶段,其中验证,准备,解析统称为连接

1、类的生命周期

2、类的加载过程

2.1、加载

加载阶段是整个类加载过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下3件事情。

1)通过一个类的全限定名来获取定义此类的二进制流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

2.2、验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合《JAVA虚拟机规范》的全部约束条件,保证这些信息被当成代码运行后不会危害虚拟机的自身安全。会有以下几种验证方式:

1)文件格式验证

第一阶段主要是验证字节流是否符合Class文件格式的规范,并且能被当前版本虚拟机处理。

比如:是否以魔数0xCAFEBABE开头,主次版本号是否在当前java虚拟机接受范围内等等。

2)元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证描述的信息符合《Java语言规范》

比如:这个类是否有父类(除了java.lang.Object所有的类都有父类)。这个类的父类是否继承了不允许被继承的类(被final修饰的类)

3)字节码验证

第三阶段是整个验证过程最复杂的一个阶段,主要是通过数据流的分析和控制流的分析,确定程序语义是否合法,符合逻辑。

对类的方法体进行验证分析,保证被验证的类方法在运行时不会危害虚拟机安全的行为。比如:

保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。不会出现栈里放一个int数据类型,使用时却按long类型来加载本地变量。保证方法体的类型转换总是有效的。

4)符号引用验证

最后一个阶段的校验行为发生在虚拟机符号引用转换为直接引用的时候,这个转换动作在解析的阶段。可以看做是对类自身以外的各类信息的进行匹配性校验,看类是否缺少或者禁止访问它依赖的某些外部类,方法,字段等资源。比如:

符号引用中通过字段串描述的全限定名是否能找到对应发类。符号引用中的类,字段,方法的可访问性。是否能被当前类访问。

2.2.3准备

准备阶段是正式为类中定义的变量(静态变量,static修饰的变量)分配内存并设置初始值。

public static int num = 666;

此时在准备阶段过后的初始值为0而不是666;将num赋值为123的putstatic指令是程序被编译后,存放于类构造器<client>方法之中.

public static final int num= 666;

加上final关键字后,此时num的值在准备阶段就是666.

基本数据的零值
数据类型 零值 数据类型 零值
int 0 float 0.0f
long 0L double 0.0d
char \u0000 reference null
byte byte(0) short short(0)
boolean false    

2.2.4解析

Java虚拟机把常量池内的符号引用转换为直接引用。

1)符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

2)直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在

主要有以下四种:

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

2.2.5初始化

类的初始化是类加载过程中的最后一步。

初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<client>方法执行之前,父类的<client>方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法。

java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始):

  1. 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
  2. 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
  3. 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
  4. 虚拟机启动时,用户会先初始化要执行的主类(含有main)
  5. jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。

3、类加载器

把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。

类加载器图:

1、启动类加载器

这个加载器负责加载存放在<JAVA_HOME>\lib目录,或者-Xbootclasspath参数所指定的路径中存放的。

2、扩展类加载器

这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的,负责加载<JAVA_HOME>/lib/ext目录。

或者被java.ext.dirs系统变量所指定的路径中所有的类库。

3、应用程序类加载器

这个加载器是由sun.misc..Launcher$AppClassLoader来实现。负责加载用户类路径上所有的类库。

如果程序中没有自定义自己的类加载器,默认就是使用这个。

4、双亲委派模型

双亲委派机制工作过程:

如果一个类加载器收到了类加载器的请求.它首先不会自己去尝试加载这个类.而是把这个请求委派给父加载器去完成.每个层次的类加载器都是如此.因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中.只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时.子加载器才会尝试自己去加载。

双亲委派模型的优点:java类随着它的加载器一起具备了一种带有优先级的层次关系.

例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类.最终都是双亲委派模型最顶端的Bootstrap类加载器去加载.因此Object类在程序的各种类加载器环境中都是同一个类.相反.如果没有使用双亲委派模型.由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中.那系统中将会出现多个不同的Object类.java类型体系中最基础的行为也就无法保证.应用程序也将会一片混乱.。

public class String {

    public static void main(String[] args) {
        System.out.println("我是天才");
    }
    
}

如果没有双亲委派机制,用户自定义一个Java.lang.String 类,那系统就会出现两个String类。类就会无法确认该加载哪个类。

有了双亲委派机制所以会报错。

5、破坏双亲委派机制

第一次:

  发生在双亲委派模型出现之前,即JDK1.2之前,由于双亲委派模型在JDK1.2之后才被引入,而类加载器和抽象类java.Lang.ClassLoader则在JDK1.0时代就已经存在了,面对已经存在的用户自定义类加载器的实现代码,java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK1.2之后的java.Lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户都是去重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass().

  我们之前也说了loadClass()方法的代码,双亲委派的具体逻辑就实现在这个方法之中,JDK1.2之后已不再提倡用户去覆盖loadClass方法,而是把自己的类加载逻辑 写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是复合双亲委派模型的。

第二次:

  是由这个模型的自身的缺陷导致的,双亲委派能够很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以成为基础,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办呢?

  那jdk又是怎么做的呢?他们引入了:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.Lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围都没有设置过的话,那这个类加载器默认就是应用程序类加载器。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等,这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。有了线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上已经打破了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。

发布了28 篇原创文章 · 获赞 24 · 访问量 1033

猜你喜欢

转载自blog.csdn.net/qq_42305209/article/details/104278714