面试专题JavaSEJVM

JVM加载class文件的原理机制

2019-04-05  本文已影响90人  Djbfifjd

一、什么是类加载器?

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

JVM类加载.jpg
如上图所示,首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

二、有哪些类加载器?

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)方法。

类加载器的双亲委派机制.jpg

四、为什么需要双亲委派机制

为什么需要双亲委派模型?假设没有双亲委派模型,试想一个场景:
黑客自定义一个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()。
根加载器会默认加载系统变量 sun.boot.class.path 指定的类库(jar 文件和 .class 文件),默认是 $JRE_HOME/lib 下的类库,如 rt.jar、resources.jar 等,具体可以输出该环境变量的值来查看。除了加载这些默认的类库外,也可以使用 JVM 参数 -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 目录下的类库。
我们可以在启动时修改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会确保这个类已经被加载、连接(验证、准备和解析)和初始化。

JVM类的加载.jpg 类的正常加载过程.png

七、关于Java静态代码块执行时机的解析

加载与类加载是两个截然不同的过程。
Java的“类加载”是一个类从被加载到虚拟机内存中开始,到卸载出虚拟机内存为止的整个生命周期中的一个过程,包括加载、验证、准备、解析和初始化五个阶段。而“加载”指的是类加载的第一个阶段,加载阶段,虚拟机需要完成以下3件事情:

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

类中的静态块会在整个类加载过程中的初始化阶段执行,而不是在类加载过程中的加载阶段执行。初始化阶段是类加载过程中的最后一个阶段,该阶段就是执行类构造器<clinit>方法的过程,<clinit>方法由编译器自动收集类中所有类变量(静态变量)的赋值动作和静态语句块中的语句合并生成,一个类一旦进入初始化阶段,必然会执行静态语句块。所以说,静态块一定会在类加载过程中被执行,但不会在加载阶段被执行。

上一篇下一篇

猜你喜欢

热点阅读