继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

JVM加载class文件的原理机制

Oraclea
关注TA
已关注
手记 88
粉丝 37
获赞 353

什么是类加载器?

类加载器(ClassLoader)就是在系统运行过程中动态的将字节码文件加载到 JVM 中的工具,是一个类。基于这个工具的整套类加载流程,我们称作类加载机制。我们在 IDE 中编写的都是源代码文件,以后缀名为 .java 的文件形式存在于磁盘上,通过编译后生成后缀名为 .class 的字节码文件,ClassLoader 加载的就是这些字节码文件。

有哪些类加载器?

Java 默认提供了三个 ClassLoader,分别是根加载器(BootStrapClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader),依次前者分别是后者的「父加载器」。父加载器不是「父类」,三者之间没有继承关系,只是因为类加载的流程使三者之间形成了父子关系。
还有一种是用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了双亲委托机制(PDM),PDM更好的保证了Java平台的安全性。在该机制中,JVM自带的BootStrapClassLoader是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对BootStrapClassLoader的引用。

双亲委派机制工作过程

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
说明:
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。
我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。
类加载器的双亲委派机制
为什么需要双亲委派机制
为什么需要双亲委派模型?假设没有双亲委派模型,试想一个场景:
黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用。如果在这这个函数中,黑客加入一些“病毒代码”,并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类
或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

举个简单例子:
ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。

下面是关于几个类加载器的说明:

BootStrapClassLoader:根加载器,它是脱离 Java 语言,使用 C/C++ 编写的类加载器,所以当你尝试使用 ExtClassLoader 的实例调用 getParent() 方法,获取其父加载器时会得到一个 null 值,比如调用String.class.getClassLoader()。
boot1
  根加载器会默认加载系统变量 sun.boot.class.path 指定的类库(jar 文件和 .class 文件),默认是 $JRE_HOME/lib 下的类库,如 rt.jar、resources.jar 等,具体可以输出该环境变量的值来查看。
boot2
除了加载这些默认的类库外,也可以使用 JVM 参数 -Xbootclasspath/a 来追加额外需要让根加载器加载的类库。比如我们自定义一个 com.ganpengyu.boot.DateUtils 类来让根加载器加载。boot3
我们将其制作成一个名为 gpy-boot 的 jar 包放到 /Users/yu/Desktop/lib 下,然后写一个测试类去尝试加载 DateUtils。
boot4
运行这个测试类:boot5
可以看到输出为 true,也就是说加载 com.enjoy.boot.DateUtils 的类加载器在 Java 中无法获得其引用,而任何类都必须通过类加载器加载才能被使用,所以推断出这个类是被 BootStrapClassLoader 加载的,也证明了 -Xbootclasspath/a 参数确实可以追加需要被根加载器额外加载的类库。

总之,对于 BootStrapClassLoader 这个根加载器我们需要知道三点:

  1. 根加载器使用 C/C++ 编写,我们无法在 Java 中获得其实例
  2. 根加载器默认加载系统变量 sun.boot.class.path 指定的类库
  3. 可以使用 -Xbootclasspath/a 参数追加根加载器的默认加载类库

ExtClassLoader:扩展类加载器,它是一个使用 Java 实现的类加载器(sun.misc.Launcher.ExtClassLoader),用于加载系统所需要的扩展类库。默认加载系统变量 java.ext.dirs 指定位置下的类库,通常是 $JRE_HOME/lib/ext 目录下的类库。
ext1
我们可以在启动时修改java.ext.dirs 变量的值来修改扩展类加载器的默认类库加载目录,但通常并不建议这样做。如果我们真的有需要扩展类加载器在启动时加载的类库,可以将其放置在默认的加载目录下。

总之,对于 ExtClassLoader 这个扩展类加载器我们需要知道两点:

  1. 扩展类加载器是使用 Java 实现的类加载器,我们可以在程序中获得它的实例并使用。
  2. 通常不建议修改java.ext.dirs 参数的值来修改默认加载目录,如有需要,可以将要加载的类库放到这个默认目录下。

AppClassLoader:应用类加载器,它和 ExtClassLoader 一样,也是使用 Java 实现的类加载器(sun.misc.Launcher.AppClassLoader)。它的作用是加载应用程序 classpath 下所有的类库。它是应用最广泛的类加载器,是我们最常打交道的类加载器,我们在程序中调用的很多 getClassLoader() 方法返回的都是它的实例。在我们自定义类加载器时如果没有特别指定,那么我们自定义的类加载器的默认父加载器也是这个应用类加载器。

总之,对于 AppClassLoader 这个应用类加载器我们需要知道三点:

  1. 应用类加载器是使用 Java 实现的类加载器,负责加载应用程序 classpath 下的类库。
  2. 应用类加载器是和我们最常打交道的类加载器。
  3. 没有特别指定的情况下,自定义类加载器的父加载器就是应用类加载器。

自定义类加载器
除了上述三种 Java 默认提供的类加载器外,我们还可以通过继承 java.lang.ClassLoader 来自定义一个类加载器。如果在创建自定义类加载器时没有指定父加载器,那么默认使用 AppClassLoader 作为父加载器。

类加载器的启动顺序

上文已经提到过 BootStrapClassLoader 是一个使用 C/C++ 编写的类加载器,它已经嵌入到了 JVM 的内核之中。当 JVM 启动时,BootStrapClassLoader 也会随之启动并加载核心类库。当核心类库加载完成后,BootStrapClassLoader 会创建 ExtClassLoader 和 AppClassLoader 的实例,两个 Java 实现的类加载器将会加载自己负责路径下的类库,这个过程我们可以在 sun.misc.Launcher 中窥见。

原理:

JVM中类的加载是由类加载器(ClassLoader)和它的子类来实现的。Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。
一、类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后Class对象还不完整,所以此时的类还不可用。
二、当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。
三、最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。
类的正常加载过程

打开App,阅读手记
4人推荐
发表评论
随时随地看视频慕课网APP