聚焦Java领域优质技术,备受关注
作者:tan 来自公众号:日工一兵
看完这篇文章,有大厂会面试你的“家长委托模式”。坦白说:你害怕什么?
当您阅读本文时,请保持IDE 打开并根据文章的内容和想法跟踪和探索您的代码。如果您手边没有IDE,请先保存它,然后再返回(如果系统询问您的话)。 (采访中有详细解释)。为了更方便地在手机上阅读,请点击文章末尾并复制代码。您可以放大特定代码。这篇文章比较长,但是如果你想更好地理解Java的类加载过程,同时提高你的面试技巧,请看一下。请耐心阅读.
双亲委派模型在介绍这个Java技术点之前,请考虑以下问题。
为什么我们不能为同名的String定义类,为什么在多线程时不会重复加载呢?下面的代码中,虚拟机如何初始化和注册? MySQL 连接驱动程序?
理解上述问题的前提是理解类加载的时机和过程。本文将非常详细地回答上述问题。
类加载时机和过程
一个类从加载到虚拟机内存到从内存中卸载的整个生命周期包括7个步骤:加载、验证、准备、解析、初始化、使用和卸载阶段。准备、验证、分析这三个部分统称为一个环节。正如照片所示
五个阶段的顺序是固定的:加载、验证、准备、初始化和卸载。尽管类加载过程必须按此顺序分阶段开始,但解析阶段不一定必须按此顺序开始。在某些情况下,必须启动分析阶段。从初始化阶段开始,这是为了支持Java语言中的运行时绑定(也称为动态或后期绑定)。
加载
在加载阶段(参见java.lang.ClassLoader中的loadClass()方法),虚拟机必须完成三件事:
使用类的完全限定名称获取定义此类的二进制字节流(虽然没有指定它必须来自类文件,但它可以用于网络、动态生成、数据库等) ; 转换该字节流。 所表示的静态存储结构被转换为方法区域中的运行时数据结构。在内存中创建一个表示该类的java.lang.Class 对象,作为访问该类的各种数据的入口。在方法区中,加载阶段和连接阶段(链接)的一些内容,比如一些字节码文件格式验证动作,是交叉执行的。虽然连接阶段可能已经开始,但是夹在加载阶段之间的这些动作仍然属于连接阶段,并且这两个阶段的开始时间仍然保持固定的顺序。
确认
验证是连接阶段的第一步。该阶段的目的是确保class文件的字节流中包含的信息满足当前虚拟机的要求,并且不会危及虚拟机本身的安全。
验证阶段涉及完成四项高级检查操作:
文件格式验证:验证字节流是否符合类文件格式规范。例如,它是否以神奇的0xCAFEBABE 开头(当您以二进制格式打开类文件时,您会看到这个文件头,cafebabe)?当前虚拟版本是否有主版本号和次版本号。常量池中的常量是否存在机器处理范围内不支持的类型。元数据验证:对字节码描述的信息进行语义分析(注:与Javac编译阶段的语义分析进行比较)。检查描述的信息是否符合Java语言规范的要求,例如该类是否有父类。非Java .lang.Object。字节码验证:通过分析数据和控制流来确定程序的语义是否有效和逻辑。验证符号引用:确保正确执行解析操作。验证阶段非常重要,但不是必需的。如果引用的类被重复验证,您可以考虑使用-Xverifynone 参数来关闭大多数类验证措施并缩短您的虚拟机。类加载时间。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量使用的内存分配在方法区中。目前内存分配只包括类变量(通过static修饰的变量),实例变量在对象实例化时随对象一起分配在堆上。其次,这里讨论的初始值通常是数据类型的零值。假设您的类变量定义如下:
有正常情况和特殊情况之分。这里的特殊情况是指:
来分析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。解析操作主要对七种类型的符号引用执行:类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用站点修饰符。
初始化
在介绍初始化的时候,首先需要介绍两个方法: 和:
编译生成class文件时会自动生成两个方法。一个是类初始化方法,另一个是实例初始化方法clinit:jvm 第一次加载类文件(包括静态变量)时调用。创建实例时会调用执行:这包括调用new 运算符或调用现有java.lang.reflect.Constructor 对象的Clone() 方法。 Object;该类的getObject() 方法通过java.io.ObjectInputStream 进行反序列化。类初始化阶段是类加载过程的最后一步,类中定义的Java 程序代码实际上开始执行。在极限准备中,变量一次被赋予系统所需的初始值,在初始化阶段,类变量和其他资源根据程序员通过程序创建的监控计划进行初始化。换句话说:在初始化阶段,会执行类的constructor()方法过程。
()方法是由编译器自动收集类中所有类变量的赋值动作并合并静态语句块static{}中的语句生成的。编译器收集它们的顺序由语句的顺序决定。如果源文件中存在静态语句块,则静态语句块只能访问静态语句块之前定义的变量。在前面的静态语句块中,可以赋值,但不能访问值。如下:
然后去掉错误语句,修改为:
输出结果:1
为什么输出结果是1呢?在准备阶段我们知道i=0,那么依次执行i=0的静态块,然后执行静态赋值操作。执行完毕,最后在main方法中取出i,值为1。
所谓双亲委托,是指每当收到类加载请求时,首先将该请求委托给父类加载器来完成(对于双亲来说,所有的加载请求最终都会到达最顶层的Bootstrap ClassLoader加载器)。类加载器无法完成加载(在加载器的搜索范围内没有找到对应的类)。如果未加载,则会抛出ClassNotFoundException 异常。文章开头提出的第一个问题是,不能定义同名的String Java文件,因为父加载器已经加载了JDK中的String.class文件。
为什么会有这样的规则?
这避免了重复加载,因此如果父级已经加载了该类,则ClassLoader 不必再次加载它。如果由于安全因素您不使用这种委托模式,想象一下您始终可以使用自定义String 来动态替换Java 核心API 中定义的类型。这带来了巨大的安全风险并导致家长委托。这种情况是可以避免的。因为Strings是在启动时由引导类加载器(Bootstrcp ClassLoader)加载的,所以用户定义的ClassLoader无法加载您创建的String,除非您修改JDK的ClassLoader。找到默认的算法类。
原来,除了引导类加载器(BootStrap ClassLoader)之外,每个类还有它的“父类”加载器。
其实这里的父子关系是join模型而不是继承关系。
从这个图中可以看到,AppClassLoader和ExtClassLoader这两个类都继承自URLClassLoader,URLClassLoader继承自ClassLoader,并且ClassLoader都有属性。
通过构造函数实例化AppClassLoader和ExtClassLoader时,必须将类加载器作为当前类加载器的父类传递。
顶层类加载器有几个重要的功能。首先,让我们看一下概述。
将ByteBuffer 的内容转换为Java 类,指定保护域(protectionDomain)。该方法被声明为Final。
将字节数组b 的内容转换为Java 类,关闭起始偏移量。该方法被声明为Final。
查找指定名称的类
链接类
类加载器的职责
上面我们解释了每个加载器都有一个对应的加载搜索范围。
Bootstrap ClassLoader: 该加载器不是Java类,而是用底层C++实现的,在虚拟加载时加载Jdk核心类库(rt.jar、resources.jar、charsets.jar等)。机器启动并且类加载器已加载后两次。这个ClassLoader完全由JVM本身控制。需要加载哪些类以及如何加载是由JVM本身控制的。扩展类ClassLoader:是一个继承自ClassLoader类的常规Java类。负责加载{JAVA_HOME}/目录jre/lib/ext/下的所有jar包。 App ClassLoader:这是Extension ClassLoader的子对象,负责加载应用程序类路径目录中的所有jar和类文件。
您可以自己运行该文件来查看每个类加载器加载的文件。
如何加载两个类
这两个方法通常用于动态加载Java 类Class.forName() 和ClassLoader.loadClass()。然而,这两种方法之间存在一些细微的差异。
Class.forName() 方法
如果我们看一下Class类的具体实现,我们可以发现这个方法本质上是调用了一个native方法。
形式上类似于Class.forName(name,true,currentLoader)。总之,对Class.forName 的成功调用如下所示:
验证Java 类是否已有效加载到内存中。这意味着执行内部静态块代码,默认初始化静态属性,并使用相应的类加载器。班级。 ClassLoader.loadClass方法
如果采用这种类加载策略,由于存在父托管模型,类加载任务最终会传递给Bootstrap ClassLoader进行加载。追踪源码最终会调用native方法。
同时,与之前的方法最重要的区别在于,类没有被初始化,而是只有在显式调用时才被初始化。总之,成功调用ClassLoader.loadClass 如下所示:
类被加载到内存中。该类未初始化,仅在第一次调用时进行初始化。这是为了让您能够灵活地加载类,您可以根据自己的需要来进行。要实现自定义类加载器来加载类,必须继承ClassLoader类。 (对于许多开源Web 项目来说都是如此,例如tomcat、struct2、jboss 等),因为根据Java Servlet 规范的要求,Web 应用程序自己的类比提供的类具有更高的优先级。由Web容器执行,但同时需要重写类加载器,使得Java核心类不被擅自覆盖)双亲委托模型Launcher源码分析
要分析类加载器源代码,请从sun.misc.Launcher.class 文件开始。还可以看到该类的ExtClassLoader和AppClassLoader的定义。如上所述,它是一种继承关系,但它是通过指定父属性形成的组合模型。
输入上面第25 行的loadClass 方法。
您可以看到该方法有一个同步块。这就解释了本文开头的第二个问题。多线程情况下不会出现重复读取的情况。同时询问父类加载器是否加载。如果没有加载,请尝试自行加载。
URLClassLoader的findClass方法:
为了更清楚地解释整个过程,让我们使用来自网络用户的加载序列图。
打破家长委托模式
Java本身有一套资源管理服务JNDI,位于rt.jar中,由启动类加载器加载。数据库管理以JDBC 为例,Java 为数据库操作提供了驱动接口。
然后提供DriverManager 来管理这些驱动程序的特定实现。
这里省略了大部分代码,但是您可以看到,在使用数据库驱动程序之前,必须首先使用DriverManager 的registerDriver() 注册它。之后就可以正常使用了。
不破坏父级的委托模型(不使用JNDI 服务)
我们来看看mysql驱动是如何加载的。
核心是Class.forName()触发mysql驱动加载。我们看一下mysql的Driver接口的实现。
正如你所看到的,Class.forName()实际上触发了一个静态代码块,该代码块向DriverManager注册mysql Driver实现。目前,当您通过DriverManager获取连接时,只需检查当前所有Driver实现并选择一个即可建立连接。
打破父委托模式的情况
从JDBC4.0开始,我们开始支持使用spi来注册这个驱动。具体做法是在mysql jar包的META-INF/services/java.sql.Driver文件中标明当前使用的驱动。然后运行这个:
可以看到这里直接进行了连接,绕过了上面的Class.forName()注册过程。
现在我们用这个spi服务模型:来分析一下,看看原来的流程是什么样的。
首先从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”。然后加载这个类。您只能使用class.forName(\\\’ com)。mysql.jdbc.Driver\\\’) 问题就出现在这里。加载Class.forName()使用调用Classloader,调用DriverManager在rt.jar中,ClassLoader启动类加载驱动,而com.mysql .jdbc.Driver肯定在/lib中,mysql中的这个类不应该被加载。因为它不在下面。这是父委托模型的限制。父加载器无法将类加载到子类加载器的路径中。
那么我们如何解决这个问题呢?目前这个mysql驱动只能由应用程序类加载器来加载,所以我们在启动类加载器中有一个方法来获取应用程序类加载器并通过它来加载它。这称为线程上下文加载器。
在文章前面,我们提到可以通过Thread.setContextClassLoaser() 方法设置线程上下文类加载器,但在没有任何特殊配置的情况下,通常使用应用程序类加载器。默认。
显然,线程上下文类加载器允许父类加载器调用子类加载器来加载类。这违反了父委托模型的原则。
接下来我们看看DriverManager是如何使用线程上下文类加载器将Driver类加载到第三方jar包中的。我们首先看一下源代码。
使用时直接调用DriverManager.getConnection()方法即可。这自然会触发静态代码块的执行并开始加载驱动程序。接下来我们看一下ServiceLoader.load()的具体实现。
继续往下看构造函数在实例化ServiceLoader时做了什么事情。
检查reload() 函数。
让我们继续看看LazyIterator 构造函数。该类还实现了Iterator 接口。
我们这里也实例化了上下文获取到的类加载器,回头看看ServiceLoader重写的iterator()方法。
上面的next() 方法调用lookupIterator.next()。这个lookupIterator就是刚刚实例化的LazyIterator()。我们来看看下面的方法。
我们继续看nextService方法。
最后,在上面的nextService函数的第8行,我们调用了c=Class.forName(cn, false,loader)方法,成功通过线程上下文获取了应用程序类加载器(或者自助类加载器)。类加载器被定义并注入到线程上下文中)。同时,我们还在子jar包中发现了厂商注册的驱动特定的实现类名。这样就可以成功加载到rt的DriverManager中了。jar包。放置在第三方应用程序包中的类在第16 行完成。这对应了new Driver();开头的问题。明白了这一点,你就可以轻松解决这篇文章了。
实现JAVA热部署首先我们来说说热部署(热插拔)。热部署允许您自动检测类文件的更改并更新运行时类行为,而无需重新启动Java 虚拟机。 Java类是通过Java虚拟机来加载的,特定类的类文件被类加载器加载后,就会生成对应的Class对象,并可以创建该类的实例。默认虚拟机行为是在启动时仅加载类。如果以后需要更新某个类,只需替换已编译的类文件即可,Java 虚拟机不会更新正在运行的类。实现热部署最基本的方法就是修改虚拟机的源代码,改变类加载器的加载行为,让虚拟机能够监听类文件更新并重新加载类文件。这种行为具有很强的破坏性,可能会导致后续问题。 JVM 升级填补了一个大漏洞。
另一种易于使用的方法是创建您自己的类加载器来加载您需要监视的类。这允许您控制类加载的时间并启用热部署。
热部署步骤:
销毁自定义类加载器(加载器加载的类也会自动卸载)。更新类并使用新的ClassLoader 加载该类。 JVM中的类只有满足以下三个条件才能被GC回收:即类被卸载(unloaded)。
此类的所有实例都经过GC。也就是说,JVM 中没有该类的实例。加载该类的ClassLoader已被GC。该类的java.lang.Class 对象没有在任何地方被引用。例如,无法通过自定义类加载器中任何位置的反射来访问此类的方法。
要创建您自己的类加载器,只需继承java.lang.ClassLoader 类并重写findClass(String name) 方法,该方法告诉您如何检索该类的字节码流。
如果想遵守双亲委托规范,可以重写findClass方法(用户自定义类加载逻辑),如果想放弃,可以重写loadClass方法(双亲委托具体逻辑实现)。
最近无意中发现了一篇巨大的人工智能教程,忍不住分享给大家。本教程不仅基础易懂,而且充满幽默感,就像在看小说一样!我觉得这很神奇,所以我把它分享给大家。单击此处跳转至教程。
https://www.captainbed.net/suga
本文和图片来自网络,不代表火豚游戏立场,如若侵权请联系我们删除:https://www.huotun.com/game/665879.html