Java框架技术核心基石系列教程(03)——类的加载过程
发布日期:2021-06-30 11:12:43 浏览次数:3 分类:技术文章

本文共 8511 字,大约阅读时间需要 28 分钟。


版权声明

  • 本文原创作者:谷哥的小弟
  • 作者博客地址:http://blog.csdn.net/lfdfhl

参考资料


类的生命周期

一个类型从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段。

在这里插入图片描述

其中验证、 准备、 解析三个部分统称为连接(Linking)。我们通常把加载(Loading)、连接(Linking)、初始化(Initialization)这三个阶段统称为类的加载过程,这亦是我们关注的重点。

加载(Loading)

该阶段的核心工作:类加载器将字节码文件.class加载进内存。

字节码文件.class通常有如下几种来源:

  • 1、从本地系统直接读取.class文件
  • 2、从zip,jar等文件中加载.class文件
  • 3、通过网络读取.class文件
  • 4、从数据库中读取.class数据

节码文件.class加载到内存后,JVM会将静态数据转换成方法区的运行时数据结构,并生成一个代表这个类的java.lang.Class对象。该对象作为方法区中类数据的访问入口,需要访问和使用类数据只能通过这个Class对象实现。

连接(Linking)

在此,概要介绍连接(Linking)的各阶段。

验证(Verification)

验证是连接阶段的第一步, 该阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》 的全部约束要求, 保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

主要验证如下:

文件格式验证

验证字节流是否符合Class文件格式的规范, 并且能被当前版本的虚拟机处理。例如:

  • 1、字节码文件是否以魔数0xCAFEBABE开头
  • 2、字节码文件主、次版本号是否在当前Java虚拟机接受范围之内
  • 3、字节码文件各个部分及文件本身是否有被删除的或附加的其他信息

元数据验证

元数据验证指的是对字节码描述的信息进行语义分析, 以保证其描述的信息符合《Java语言规范》的要求。例如:

  • 1、验证该类是否有父类(除java.lang.Object之外所有的类都应当有父类)
  • 2、验证该类的父类是否继承了不允许被继承的类(被final修饰的类)
  • 3、验证如果该类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
  • 4、验证该类中的字段、 方法是否与父类产生矛盾(例如:覆盖了父类的final字段, 或者出现不符合规则的方法重载(例如:方法参数都一致, 但返回值类型却不同等))

字节码验证

字节码验证指的是通过数据流分析和控制流分析, 确定程序语义是合法的、是否 符合逻辑。 在元数据验证阶段对元数据信息中的数据类型校验完毕以后, 该阶段对类的方法体(Class文件中的Code属性)进行校验分析, 保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。 例如:

  • 1、保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。例如:不会出现类似于“在操作栈放置了一个int类型的数据, 使用时却按long类型来加载入本地变量表中”这样的情况。
  • 2、保证任何跳转指令都不会跳转到方法体以外的字节码指令上
  • 3、保证方法体中的类型转换总是有效的。 例如:可以把一个子类对象赋值给父类数据类型;但是,不能把父类对象赋值给子类数据类型, 甚至把对象赋值给与它毫无继承关系、 完全不相干的一个数据类型。

符号引用验证

符号引用验证指的是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验。例如:

  • 1、校验符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 2、校验类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
  • 3、校验符号引用中的类、 字段、 方法的可访问性是否可被当前类访问。

符号引用验证的主要目的是确保解析行为能正常执行, 如果无法通过符号引用验证, Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError的子类异常, 典型的如:java.lang.IllegalAccessError、 java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

准备(Preparation)

该阶段的核心工作:为类中定义的变量(即被static修饰的静态变量)分配内存并设置类变量初始值。

在该阶段进行内存分配的仅包括类变量而不包括实例变量;实例变量将会在对象实例化时随着对象一起分配在Java堆中。 另外,请注意:在通常情况下变量的初始值为与其对应的数据类型的零值。

常见的零值如下:

在这里插入图片描述
示例代码:

public static final int number = 9527;

在该阶段number的初始值为0。直到,类的初始化(Initialization)阶段才会将其赋值为9527。

解析(Resolution)

该阶段的核心工作:Java虚拟机将常量池内的符号引用替换为直接引用。也可以通俗地理解为:将原本的符号(例如:字符串)表示映射成了指针(内存地址)。

符号引用(Symbolic References)

符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能无歧义地定位到目标即可。 符号引用与虚拟机实现的内存布局无关, 引用的目标并不一定是已经加载到虚拟机内存当中的内容。

直接引用(Direct References)

直接引用是可以直接指向目标的指针、 相对偏移量或者是一个能间接定位到目标的句柄。 直接引用是和虚拟机实现的内存布局直接相关的, 同一个符号引用在不同虚

拟机实例上翻译出来的直接引用一般不会相同。 如果有了直接引用, 那引用的目标必定已经在虚拟机的内存中存在。

初始化(Initialization)

类初始化(Initialization)阶段是类加载过程的最后一个步骤。直到初始化(Initialization)阶段,Java虚拟机才真正开始执行类中编写的Java程序代码。也正是在该阶段,Java虚拟机将主导权移交给了应用程序。

类的初始化过程中会执行类构造器< clinit >( )方法,并完成以下主要工作:

  • 1、对静态变量显示赋值
  • 2、执行静态代码块

当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。

类加载器

之前,我们提到:类加载器将字节码文件.class加载进内存。在此,我们详细介绍类加载器。

类加载器的分类

Java的类加载器有如下四种:

  • 1、启动类加载器(Bootstrap Classloader)
  • 2、扩展类加载器(Extension ClassLoader)
  • 3、应用程序类加载器(Application Classloader)
  • 4、自定义类加载器(User Classloader)

启动类加载器(Bootstrap Classloader)

启动类加载器(Bootstrap Classloader)又称为引导类加载器、根类加载器。该类加载器负责加载Java的核心库,它负责加载JAVA_HOME/jre/lib/rt.jar或者sun.boot.class.path路径下的内容。

我们打开本地的Java安装目录来瞅瞅:

在这里插入图片描述

当然,也可以在项目中打开JRE System Library来看看:

在这里插入图片描述

从这里我们清楚地看到Java的核心类库,比如:java.lang、java.io、java.net等等;它们都是由启动类加载器(Bootstrap Classloader)加载进内存的。

启动类加载器(Bootstrap Classloader)并不是java.lang.ClassLoder的子类,而是使用C/C++实现的。所以,无法通过Java代码获取启动类加载器。

示例代码

//获取Bootstrap Classloaderpublic static void getBootstrapClassloader() {
String string = "https://blog.csdn.net/lfdfhl"; Class
stringClass = string.getClass(); ClassLoader classLoader = stringClass.getClassLoader(); System.out.println(classLoader);}

运行结果

在这里插入图片描述

扩展类加载器(Extension ClassLoader)

扩展类加载器(Extension ClassLoader)由sun.misc.Launcher$ExtClassLoader实现,它是java.lang.ClassLoader的子类。该类加载器负责加载Java的扩展库JAVA_HOME/jre/lib/ext/*.jar或者java.ext.dirs路径下的内容。

在这里插入图片描述

在此,我们尝试获取扩展类库。

示例代码

package com.classloader;import java.io.File;import java.util.StringTokenizer;/*** 本文作者:谷哥的小弟 * 博客地址:http://blog.csdn.net/lfdfhl* 示例描述:类加载器*/public class TestClassLoader1 {
public static void main(String[] args) {
File[] fileArray = getExtDirs(); for(File file:fileArray) {
System.out.println(file); } } // 获取扩展类库 public static File[] getExtDirs() {
String string = System.getProperty("java.ext.dirs"); File[] extDirs = null; if (string != null) {
StringTokenizer stringTokenizer = new StringTokenizer(string, File.pathSeparator); int number = stringTokenizer.countTokens(); extDirs = new File[number]; for (int i = 0; i < number; i++) {
String token = stringTokenizer.nextToken(); extDirs[i] = new File(token); } } else {
extDirs = new File[0]; } return extDirs; }}

运行结果

在这里插入图片描述

应用程序类加载器(Application Classloader)

应用程序类加载器(Application Classloader)由sun.misc.Launcher$AppClassLoader实现,它是java.lang.ClassLoader的子类。该类加载器负责加载Java应用程序类路径classpath或者java.class.path下的内容。也就是说:平常,我们在项目中自己写的类就是由应用程序类加载器加载进内存的。

示例代码

package com.classloader;import java.io.File;import java.util.StringTokenizer;/*** 本文作者:谷哥的小弟 * 博客地址:http://blog.csdn.net/lfdfhl* 示例描述:类加载器*/public class TestClassLoader1 {
public static void main(String[] args) throws ClassNotFoundException {
getApplicationClassloader(); } //获取Application Classloader public static void getApplicationClassloader() throws ClassNotFoundException {
Class
studentClass = Class.forName("com.classloader.Student"); ClassLoader classLoader = studentClass.getClassLoader(); System.out.println("classLoader=" + classLoader); } }

运行结果

在这里插入图片描述

自定义类加载器(User Classloader)

开发人员可通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足某些特殊的需求。例如:

  • 1、对字节码进行加密避免class文件被反编译
  • 2、所需要加载的字节码文件的路径不在常规路径。例如:在Tomcat中class文件就不在常规路径。此时,需要用自定义类加载器加载class文件。

类加载器之间的关系

在分别了解完四种类加载器之后,我们再来梳理它们之间的关系,请看官方文档。

在这里插入图片描述

在这里插入图片描述
小结

  • 1、启动类加载器(Bootstrap Classloader)是由C/C++实现的。
  • 2、自定义类加载器(User Classloader)继承自java.lang.ClassLoader
  • 3、应用程序类加载器(Application Classloader)和扩展类加载器(Extension ClassLoader)均是java.net.URLClassLoader的子类

双亲委派模型

在此,详细介绍双亲委派模型。

双亲委派模型概述

各种类加载器之间的层次关系被称为类加载器的双亲委派模型Parents Delegation Model 。 双亲委派模型要求除了顶层的启动类加载器外, 其余的类加载器都应有自己的父类加载器。

在这里插入图片描述
但是,请注意:此处类加载器之间的父子关系一般不是以继承Inheritance 的关系来实现的, 而是通常使用组合Composition关系来复用父加载器的代码。这一点,也可以从刚才类加载器之间的关系得以印证。

接下来,我们通过代码获取类加载器的父类加载器。

示例代码

package com.classloader;/*** 本文作者:谷哥的小弟 * 博客地址:http://blog.csdn.net/lfdfhl* 示例描述:获取类加载器的父类加载器*/public class TestClassLoader2 {
public static void main(String[] args) {
getParent(); } public static void getParent() {
ClassLoader classLoader = TestClassLoader2.class.getClassLoader(); System.out.println(classLoader); classLoader = classLoader.getParent(); System.out.println(classLoader); classLoader = classLoader.getParent(); } }

运行结果

在这里插入图片描述

结果分析

从该结果我们可以看出:

  • 1、AppClassLoader的父亲是ExtClassLoader
  • 2、虽然ExtClassLoader的父亲是BootstrapClassloader;但是,我们无法获得BootstrapClassloader

双亲委派模型工作机制

在此,详细介绍双亲委派模型工作机制及其特点。

双亲委派模型工作机制概述

类加载器查找字节码文件.class时,是通过"双亲委派模式"进行的。

如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此;所以,所有的加载请求最终都会被传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。

通俗地来说:儿子都很懒惰,一旦遇到事情就委托自己的父亲帮自己做;只有父亲做不了了,才由自己完成。

双亲委派模型工作机制示例

接下来,我们通过一个示例体会双亲委派模型。

加载示例

AppClassLoader加载字节码文件.class。

过程分析

  • 1、AppClassLoader先去缓存中查看是有该字节码文件。如果缓存中有则取出并返回;否则,将加载工作委托给父加载器ExtClassLoader。
  • 2、ExtClassLoader先去缓存中查看是有该字节码文件。如果缓存中有则取出并返回;否则,将加载工作委托给父加载器BootstrapClassLoader。
  • 3、BootstrapClassLoader先去缓存中查看是有该字节码文件。如果缓存中有则取出并返回;否则,BootstrapClassLoader在自己负责的范围(JAVA_HOME/jre/lib/rt.jar或者sun.boot.class.path)中加载字节码文件.class。如果找到就返回;否则,通知子加载器ExtClassLoader加载字节码文件.class。
  • 4、ExtClassLoader在自己负责的范围(JAVA_HOME/jre/lib/ext/*.jar或者java.ext.dirs路径下)中加载字节码文件.class。如果找到就返回;否则,通知子加载器AppClassLoader加载字节码文件.class。
  • 5、AppClassLoader在自己负责的范围(类路径classpath或者java.class.path)中加载字节码文件.class。如果找到就返回;否则,抛出异常,例如:ClassNotFoundException、NoClassDefFoundError

双亲委派模型工作机制小结

  • 1、委托的方向是自下而上
  • 2、查找的方向是从上往下

双亲委派模型工作机制的优势

在此,简述双亲委派模型工作机制的优势。

避免类的重复加载

双亲委派模型工作机制存在一个显而易见的好处:Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,子ClassLoader就没有必要再加载一次。

保证类的唯一性

双亲委派模型工作机制可以确保类的唯一性。例如,java.lang.Object存放在rt.jar之中, 无论哪一个类加载器要加载这个类, 最终都是委派给处于模型最顶端的启动类加载器进行加载, 因此Object类在程序的各种类加载器环境中都能够保证是同一个类。 反之, 如果没有使用双亲委派模型, 都由各个类加载器自行去加载的话, 如果用户自己也编写了一个名为java.lang.Object的类, 并放在程序的classpath中, 那系统中就会出现多个不同的Object类, Java类型体系中最基础的行为也就无从保证, 应用程序将会变得一片混乱。

确保Java体系安全性

通过双亲委派模型可确保Java体系的安全。Java核心API中定义类型不能被随意替换。例如:尝试使用自定义的java.lang.Integer覆盖Java自身的核心类java.lang.Integer。在加载的过程中,启动类加载器(Bootstrap Classloader)发现该类已被加载;所以,虚拟机并不会重新加载自定义的java.lang.Integer,而直接返回已加载过的Java自身的核心类java.lang.Integer,由此便可防止核心API被随意篡改。

双亲委派模型工作机制的应用

Java虚拟机规范并没有要求类加载器的加载机制一定要使用双亲委派模式,只是建议采用这种方式而已。例如,在Tomcat中类加载器所采用的加载机制就和传统的双亲委派模型有一定区别:当类加载器接收到类的加载任务时,它首先会自行加载,当自身加载失败时才会将类的加载任务委派给其父类加载器去执行。

转载地址:https://it9527.blog.csdn.net/article/details/110148277 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Integer缓存IntegerCache详解
下一篇:PowerDesigner 15 概念数据模型字段不能重复的解决方案

发表评论

最新留言

做的很好,不错不错
[***.243.131.199]2024年04月29日 13时43分31秒