JVM详解:类加载子系统
目录
参考:
- 链接1:https://blog.csdn.net/qq_48435252/article/details/123697193 (opens new window)
- 链接2:https://blog.csdn.net/qq_37967783/article/details/131468808 (opens new window)
# JVM详解:类加载子系统
- 类加载子系统负责从文件系统或者网络中加载Class文件(Class文件在开头有特定标识)。类加载器(Class Loader)只负责class文件的加载,至于是否可以运行,由执行引擎(Execution Engine)决定。
- 类加载子系统加载的类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
# 类加载的过程
类加载子系统包含三个部分:加载—>链接—>初始化
- 加载过程
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
链接过程
验证(Verify),目的:在于确保Class文件的字节流中包含信息符合当前JVM规范要求,保证被加载类的正确性,不会危害虚拟机自身安全。主要包括四种验证:文件格式验证、源数据验证、字节码验证、符号引用验证。
准备(Prepare),为类变量(static)分配内存并且设置初始值。这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;不会为实例变量分配初始化,类变量会分配在方法去中,而实例变量是会随着对象一起分配到java堆中。
解析(Resolve),将常量池内的符号引用转换为直接引用的过程。事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
初始化
初始化阶段就是执行类构造器方法clInit()的过程。 clInit是ClassInit缩写。此方法并不是程序员定义的构造方法。是javac编译器自动收集类中的所有类变量(Static)的赋值动作和静态代码块中的语句合并而来。构造器方法中指令按语句在源文件中出现的顺序执行若该类具有父类,jvm会保证子类的clinit()执行前,父类的clinit()已经执行完毕。
# 类加载器的分类
- 启动类加载器,负责加载JAVA_HOME/lib目录下的可以被虚拟机识别(通过文件名称,比如rt.jar``tools.jar)的字节码文件。与之对应的是java.lang.ClassLoader类
- 扩展类加载器,负责加载JAVA_HOME/lib/ext目录下的的字节码文件。对应sun.misc.Launcher类 此类继承于启动类加载器ClassLoader
- 应用程序类加载器,负责加载ClassPath路径下的字节码,也就是用户自己写的类。对应于sun.misc.Launcher.AppClassLoader类,此类继承于扩展类加载器Launcher
- 用户自定义加载器,需要继承系统类加载器ClassLoader,并重写findClass方法。负责加载指定位置的字节码文件。通过类中的path变量指定。
# 类加载的双亲委派机制
双亲委派机制(Parent Delegation Mechanism)是Java中的一种类加载机制。在Java中,类加载器负责加载类的字节码并创建对应的Class对象。双亲委派机制是指当一个类加载器收到类加载请求时,它会先将该请求委派给它的父类加载器去尝试加载。如果父类加载器能够成功加载,则加载过程结束,如果父类加载器无法加载且它没有父类,则子加载器会尝试加载,如果父类加载器无法加载且它还有父类,则进一步向上委托,依次递归,直到请求到达最顶层的引导类加载器。 如果顶层类的加载器加载成功,则成功返回。如果顶层类的加载器无法加载该类时,则会回到初始的加载器,尝试使用自身的加载机制加载该类。
这种机制的设计目的是为了保证类的加载是有序的,避免重复加载同一个类。Java中的类加载器形成了一个层次结构,根加载器(Bootstrap ClassLoader)位于最顶层,它负责加载Java核心类库。其他加载器如扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)都有各自的加载范围和职责。通过双亲委派机制,可以确保类在被加载时,先从上层的加载器开始查找,逐级向下,直到找到所需的类或者无法找到为止。
这种机制的好处是可以避免类的重复加载,提高了类加载的效率和安全性。同时,它也为Java提供了一种扩展机制,允许开发人员自定义类加载器,实现特定的加载策略。
双亲委派机制的优缺点
优点:
避免重复加载:通过委派给父类加载器,可以避免同一个类被多次加载,提高了加载效率。
安全性:通过双亲委派机制,核心类库由根加载器加载,可以确保核心类库的安全性,防止恶意代码替换核心类。
扩展性:开发人员可以自定义类加载器,实现特定的加载策略,从而扩展Java的类加载机制。
缺点:
灵活性受限:双亲委派机制对于某些特殊的类加载需求可能过于严格,限制了加载器的灵活性。
破坏隔离性:如果自定义类加载器不遵循双亲委派机制,可能会破坏类加载的隔离性,导致类冲突或安全性问题。
不适合动态更新:由于类加载器在加载类时会先检查父加载器是否已加载,因此在动态更新类时可能会出现问题,需要额外的处理。
总体而言,双亲委派机制通过层次结构和委派机制提供了一种有序、安全的类加载方式,但也存在一些限制和不适用的情况。
双亲委派模型的破坏者-线程上下文类加载器
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由AppClassLoader类加载器加载。由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由Bootstrap类加载器来加载的,但Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。 **线程上下文类加载器(contextClassLoader)**是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例:
从图可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。