首页 > 编程知识 正文

java类的加载机制及加载过程,类加载器把类加载到哪里

时间:2023-05-03 20:31:04 阅读:32279 作者:2189

班级加载器看这个就足够了。 面试问班装载机吗? 看看这个,就足以让我们思考我们平时写的代码和程序是如何工作的。 例如,我在开发java语言。 源代码是. java文件,但他们不能运行。 通常,我们会将它制成jar包并部署到服务器上,但实际上软件包就是编译。 这意味着将java文件编译为. class字节码文件。 那么,如何运行这些. class字节码文件呢? 使用java -jar命令运行这些. class文件。 实际上,java -jar命令启动jvm进程,jvm进程执行这些字节码文件

jvm如何加载这些class文件? jvm说要运行这些. class字节码文件,是怎么加载的呢?

当然通过了类加载器。 类加载器加载. class文件的过程如下

加载-验证-准备-分析-初始化

本节分析加载的整体进程,但在分析整个进程之前,我们将讨论以下类的加载条件

类加载条件一般来说,我们的程序中有很多class文件,jvm是否会无条件加载这些文件?

不,不是那样的。 实际上,jvm只是“使用”http://www.Sina.com/http://www.Sina.com /主动**,只有在以下情况下才使用主动:

1 .创建类的实例,包括new关键字、反射、克隆和反序列化

2 .调用类的静态方法时,请使用字节码invodestatic命令

使用类或接口的静态字段(final常量除外),如getstatic或putstatic指令

使用java.lang.reflect包中的方法反射类方法时

5 .初始化子类时,必须首先初始化父类

6 .作为启动虚拟机,类包含main ()方法

除了上述6点是主动使用以外,其他都是被动使用

积极使用的例子

公共类parent { static } system.out.println (' parent init ); } publicclasschildextendsparent { static { system.out.println (' child init ' ); } public class main { publicstaticvoidmain (字符串[ ] args ) { Child child=new Child ); }复制代码在初始化Parent类时打印“Parent init”,在初始化Child类时打印“Child init”。 执行Main类的Main方法初始化Child类时,将打印以下内容:

Parent init Child init

根据打印结果,可以验证积极使用class文件的两个条件: 1和5成立

其他主动使用的情况不举例子,我们来看看被动使用的例子吧

被动使用示例

公共类父{公共静态intv=60; static { system.out.println (parent init ); } publicclasschildextendsparent { static { system.out.println (' child init ' ); } public class main { publicstaticvoidmain (string [ ] args ) system.out.println ) child.v ); }复制代码时,现在Parent类中添加了静态变量v,但不会添加到Child类中,而是在Main类中访问Child.v。 在这种情况下,是否要加载Parent类? 是否要加载Child类?

输出结果如下。

Parent init 60

您会发现只加载了Parent类,而没有加载Child类。 这里的“加载”意味着完成整个加载过程。 实际上,此时还加载了Child类,但可以通过添加-XX:TraceClassLoading参数进行验证。 (此处的加载是指整个加载过程的第一次加载,但未进行初始化。

-添加xx :跟踪类加载的输出结果

[ loaded JVM.load class.parent from file :/d :/workspace/study/study _ demo/target/classes/]

Loaded jvm.loadclass.Child from file:/D:/workspace/study/study_demo/target/classes/] Parent init 60

所以在使用一个字段时,只有直接定义该字段的类才会被初始化

在主动使用的第 3 点,很明确的指出,使用类的 final 常量不属于主动使用,也就不会加载对应的类,我们通过代码验证下

public class ConstantClass { public static final String CONSTANT = "constant"; static { System.out.println("ConstantClass init"); }}public class Main { public static void main(String[] args) { System.out.println(ConstantClass.CONSTANT); }}复制代码

输出结果如下:

[Loaded jvm.loadclass.Main from file:/D:/workspace/study/study_demo/target/classes/]

constant

通过结果,确实验证了 final 常量不会引起类的初始化,因为在编译阶段对常量做了优化(学名是“常量传播优化”),把常量值 "constant"直接存放到了 Main 类的常量池中,所以不会加载 ConstantClass 类

加载

加载是类加载过程的第一个阶段,在加载阶段,jvm 需要完成如下工作:

1.通过类的全限定类名获取类的二进制数据流

2.解析类的二进制数据流为方法区内的数据结构

3.创建 java.lang.Class 类的实例,表示该类型

获取类的二进制数据流的方式有很多,比如直接读入 .class 文件,或者从 jar 、zip、war等归档数据包中提取 .class 文件,然后 jvm 处理这些二进制数据流并生成一个 java.lang.Class 的实例,该实例是访问类型元数据的接口,也是实现反射的关键数据

验证

验证阶段是为了保证加载的字节码是符合jvm规范的,大体分为格式检查、语义检查、字节码检验证、符号引用验证,如下所示:

 

 

 

准备

准备阶段主要就是为类分配相应的内存空间,并设置初始值,常用的初始值如下表所示:

数据类型默认初始值int0long0Lshort(short)0char'u0000'booleanfaslefloat0.0fdouble0.0dreferencenull

如果类中定义了常量,如:

public static final String CONSTANT = "constant";复制代码

这种常量(查看字节码文件,含有 ConstantValue 属性)会在准备阶段直接存到常量池中

public static final java.lang.String CONSTANT; descriptor: Ljava/lang/String; flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: String constant复制代码 解析

解析阶段主要把类、接口、字段和方法的符号引用转为直接引用

符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时可以无歧义地定位到目标即可

直接引用:直接引用是可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄

解析阶段主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符

下面我们通过一个例子来简单解释下

public class Demo { public static void main(String[] args) { System.out.println(); }}复制代码

查看 main 方法中 System.out.println() 方法对应的字节码

3: invokevirtual #3 // Method java/io/PrintStream.println:()V复制代码

常量池第 3 项被使用,那我们去看常量池中第 3 项的内容,如下:

#3 = Methodref #17.#18 // java/io/PrintStream.println:()V复制代码

看来还要继续查找引用关系,第 17 项和第 18 项,如下:

#17 = Class #24 // java/io/PrintStream#18 = NameAndType #25:#7 // println:()V复制代码

其中第 17 项又引用到了第 24 项,第 18 项又引用了 第 25 和 7 项,分别如下:

#24 = Utf8 java/io/PrintStream#25 = Utf8 println#7 = Utf8 ()V复制代码

我们在一张图中表示上面的引用关系关系,如下所示:

 

 

其实上面的引用关系就是符号引用

但在程序运行时,光有符号引用是不够的,系统需要明确知道该方法的位置,所以 jvm 为每个类准备了一张方法表,将其所有的方法都列入到了方法表中,当需要调用一个类的方法时,只要知道这个方法在方法表中的偏移量就可以直接调用了。通过解析操作,符号引用可以转变为目标方法在类方法表中的位置,使得方法被成功调用。

初始化

初始化是类加载的最后一个阶段,只要前面的阶段都没有问题,就会进入到初始化阶段。那初始化阶段做什么工作呢?

主要就是执行类的初始化方法(该初始化方法由编译器自动生成),它是由类静态成员变量的赋值语句及 static 语句块共同产生的。这个阶段才是执行真正的赋值操作。准备阶段只是分配了相应的内存空间,并设置了初始值。

下面我们通过一个小例子来验证下

public class StaticParent { public static int id = 1; public static int num ; static { num = 4; }}复制代码

对应的部分字节码文件如下所示:

#13 = Utf8 <clinit>static {}; descriptor: ()V flags: (0x0008) ACC_STATIC Code: stack=1, locals=0, args_size=0 0: iconst_1 1: putstatic #2 // Field id:I 4: iconst_4 5: putstatic #3 // Field num:I 8: return复制代码

可以看到在 方法中,对类中的 static 变量 id 和 static语句块中的 num 进行了赋值操作

那编译器会为所有的类都生成方法吗?答案是否定的,如果一个类既没有赋值语句,又没有 static 语句块,这样即使生成了 方法,也是无事可做,所以编译器就不插入了。我们通过一个例子看下对应的字节码

public class StaticFinalParent { public static final int a = 1; public static final int b = 2;}复制代码 public jvm.loadclass.StaticFinalParent(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return复制代码

从字节码中没有发现 方法,因为我们前面说过,final 类型的常量是在准备阶段完成的初始化,所以在初始化阶段就不用再初始化了。

注意点

这里指的注意的一点是,jvm 会保证方法 的安全性,因为可能存在多个线程同时去初始化类,这样要保证只有一个线程执行 方法,而其他线程要等待,只要有线程初始化类成功,其他线程就不用再次进行初始化了

小总结

通过上面的介绍,我想大家应该了解了我们平时写的代码,最后到底是如何运行起来的了吧,总之一句话就是我们编写的 java 文件,会被编译成 class 字节码文件,然后由 jvm 把主动使用的类加载到内存中,然后开始执行这些程序。很重要的阶段就是加载类即从外部系统获得 class 文件的二进制流,而在该阶段起着决定性作用的就是下面要介绍的 类加载器

类加载器

ClassLoader 代表类加载器,是 java 的核心组件,可以说所有的 class 文件都是由类加载器从外部读入系统,然后交由 jvm 进行后续的连接、初始化等操作。

分类

jvm 会创建三种类加载器,分别为启动类加载器、扩展类加载器和应用类加载器,下面我们分别简单介绍下各个类加载器

启动类加载器

Bootstrap ClassLoader 主要负责加载系统的核心类,如 rt.jar 中的 java 类,我们在 Linux 系统或 Windows 系统使用 java,都会安装 jdk,lib 目录里其实里面就有这些核心类

扩展类加载器

Extension ClassLoader 主要用于加载 libext 中的 java 类,这些类会支持系统的运行

应用类加载器

Application ClassLoader 主要加载用户类,即加载用户类路径(ClassPath)上指定的类库,一般都是我们自己写的代码

双亲委派模型

在类加载时,系统会判断当前类是否已经加载,如果已经加载了,就直接返回可用的类,否则就会尝试去加载这个类。在尝试加载类时,会先委派给其父加载器加载,最终传到顶层的加载器加载。如果父类加载器在自己的负责的范围内没有找到这个类,就会下推给子类加载器加载。加载情况如下所示:

 

 

 

可见检查类是否加载的委派过程是单向的,底层的类加载器询问了半天,到最后还是自己加载类,那不白费力气了吗?这样做当然有它的好的,这样在结构上比较清晰,最重要的是可以避免多层级的加载器重复加载某些类

双亲委派模型的弊端

双亲委派模型检查类加载是单向的,但这样也有个弊端就是上层的类加载器无法访问由下层类加载器所加载的类。那如果启动类加载器加载的系统类中提供了一个接口,接口需要在应用中实现,还绑定了一个工厂方法,用于创建该接口的实例。而接口和工厂方法都在启动类加载器中。这时就会出现该工厂无法创建由应用类加载器加载的应用实例的问题。比如 JDBC、XML Parser 等

jvm 这么厉害,肯定会有办法解决这种问题的,没错,java 中通过 SPI(Service Provider Interface)机制解来解决这类问题

总结

本文主要介绍了 jvm 的类加载机制,包括类加载的全过程和每个阶段做的一些事情。然后介绍了类加载器的工作机制和双亲委派模型。更输入的知识点,希望你自己去继续研究,比如 OSGI 机制,热替换和热部署如何实现等

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。