3.类加载器
3.1 类的生命周期
类的生命周期描述了一个类加载、使用、卸载的整个过程。
(1)类的加载阶段
- 加载阶段第一步是类加载器通过类的全限定名以不同的渠道以二进制流的方式获取字节码信息。
- 类加载器加载完类之后,JVM会将字节码中的信息保存到内存的方法区中(生成一个InstanceKlass对象,其中保存类的所有信息)。同时,JVM还会在堆中生成一份与方法区中数据类似的java.lang.Class对象(这个类比InstanceKlass中保存的信息要少一些,作用是使开发者能够在Java代码中获取类的信息以及存储静态字段的数据)。InstanceKlass对象包含着java.lang.Class的引用。
为何需要一个java.lang.Class对象?直接使用内存方法区中的InstanceKlass对象不就ok了吗?还可以节省一点内存?
原因是InstanceKlass对象使用c++编写的一个对象,Java并不能直接访问这个对象。另外,java.lang.Class对象保存着更加精简的信息,JVM能够很好控制开发者访问数据的范围。
总结一下:字节码文件被输入JVM时,类加载器会首先会或者字节码文件中类或接口的各种信息,然后为类或者接口在内存方法区和堆区中创建两个对象保存字节码信息。 这样一来,一个类或者接口就这样被JVM加载到内存中了。
(2)类的连接阶段
连接阶段可以分为三个步骤:验证、准备、解析。
- 验证:验证内存中的字节码信息是否遵守了《Java虚拟机规范》。
- 准备:为类中的静态变量赋初值(赋初值不是最终值,int就赋值0)。如果静态变量被final修饰,这个阶段会为静态变量赋最终值。
- 解析:将常量池中的符号引用替换为直接引用。
(3)类的初始化阶段
初始化阶段会执行静态代码块中的内容,并为静态变量赋值。
静态代码块在字节码文件“方法”中,是作为一个叫做“clinit”的方法的。
来做3道题:
第一道,输出结果是xcbcb
,因为要执行main方法,Test类必须被加载。加载后就连接,然后初始化。初始化的过程中会执行静态代码块,输出x
,然后进入main方法。类进入“使用”这一生命周期,new出一个对象,此时非静态代码块首先被执行,输出c
,然后构造方法被执行,输出b
。
第二道,输出结果是2。
第三道,输出结果是1。
3.2 类加载器的作用和应用场景
类的加载需要使用类加载器。类加载器是Java虚拟机提供给应用程序实现获取类和接口字节码数据的技术。
3.3 类加载器的分类
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现。
JDK8及之前,和8之后的版本对于类加载器的设计差别较大。
启动类加载器: 属于底层的加载器,用于加载Java中的一些核心类(比如String、Integer、Long、Thread等等)。这个类加载器存在于JVM之中,开发者不能够直接获取(如果发现一个类的加载器是null,那就是启动类加载器了)。启动类加载器在程序运行时会首先把Java安装目录下/jre/lib下的jar包加载进来,比如rt.jar(这个jar包中存放着核心类),resource.jar等。
虽然不能直接获取启动类加载器,但是可以让启动类加载器除了加载核心类以外,加载用户的jar包。第一种方法,是直接将jar包放到jre/lib目录下,但是这种方法会污染Java环境,所以不推荐。第二种是使用参数的方式加载jar包。使用-Xbootclasspath/a:
参数。-Xbootclasspath/a:jar包目录/jar包名
。
扩展类加载器和应用程序类加载器:都是jdk中提供的用Java编写的类加载器。
扩展类加载器(Extension Class Loader)默认加载Java安装目录/jre/lib/ext下的类文件。想要加载用户的文件,同样默认有两种方式,第一种是直接将jar包放到jre/lib/ext目录下,这种方式不推荐,因为会污染Java的原生环境。第二种是使用参数的方式进行加载,使用参数-Djava.ext.dirs=jar包目录
进行扩展,会覆盖掉默认目录,所以要使用分号;
进行分隔,然后追加上默认目录/jre/lib/ext。
应用程序类加载器(Application Class Loader)默认加载classpath下的类文件,就是用户自己编写的类文件。使用arthas查看应用程序加载的类的时候,会发现它同时加载了启动类加载器和扩展类加载器加载的内容。这种叫做双亲委派机制。
扩展类加载器或者应用程序类加载器除了默认根据路径加载类以外,可以在代码中主动要求进行加载。
3.4 双亲委派机制
由于Java虚拟机中有多个类加载器,双亲委派机制的核心是解决一个类到底由谁加载的问题。比如说,有一个用户自己写的类,打成jar包通过参数同时配置给三个类加载器,到底由哪个类加载器来加载这个类呢?
双亲委派机制指的是:**当一个类接收到加载类的任务时,会自底向上查找该类是否被加载过,再自顶向下进行加载。**就是说,一个类接收到加载类的任务时,它优先将加载请求委托给父类加载器进行加载,父类加载器都加载不了,才会尝试自己加载。
- 每个类加载器都有一个父加载器,在类加载的过程中,会从底部开始,检查是否加载过该类,如果加载过,直接返回该类,这样能避免一个类被重复加载。如果没有加载过,会将该类的加载请求委派给父类加载器。(向上查找)
- 如果所有的父类加载器都无法加载该类,则由最顶上的加载器尝试自己加载,但是会出现该类不在该加载器的加载路径中,所以该类无法被当前加载器加载。此时,当前加载器会将加载请求委派给下级加载器,直至到达应用程序加载器。这样的作用是让加载具有一定的优先级(向下加载)
举个例子说明一下双亲委派机制:
用户编写了一个main.java.person.fjsp.algorithm.ga.utils.GAUtils类。当程序启动,该类第一次要被加载,如何进行加载呢?首先,自底向上查找,从应用程序类加载器找起,发现没有被加载过,委派给扩展类加载器,扩展类加载器当然也是没有加载过这个类的,所以继续委派给启动类加载器,启动类加载器也没有加载过这个类。
然后,自顶向下加载,启动类加载器发现无法加载该类,委派给扩展类加载器,扩展类加载器当然也无法加载这个类,继续委派给应用程序类加载器,应用程序类加载器发现可以加载该类,于是该类被加载。
3.5 打破双亲委派机制
双亲委派机制是为了保证类的安全性,防止类被重复加载。但是在某些情况下也要打破双亲委派机制才能实现想要的功能。
- 自定义类加载器
如果不打破双亲委派机制,tomcat的多个应用程序之间同时拥有一个全限定名相同的类时,tomcat只会加载其中一个,对于另一个由于双亲委派机制的存在,会返回第一个类的信息,这样会出现信息错误。
tomat使用了自定义类加载器来实现应用之间的隔离,每一个应用都会有自己的类加载器,这样就打破了双亲委派机制,确保所有的类都能被正确加载。
想要打破双亲委派机制,就要重写ClassLoader。先来分析一下ClassLoader的原理,它主要有4个核心方法。双亲委派机制的代码就位于loadClass这个方法之中。
所以,只需要让用户自己的类继承ClassLoader这个类,成为自定义类加载器,然后重写loadClass这个方法,就会覆盖掉双亲委派机制。之后,使用自定义类加载器加载类,就不会走双亲委派机制。(虽然用了自定义类加载器覆盖了双亲委派机制,但是如果想用自定义类加载器加载诸如java.lang.String
的核心类,还是不行的,因为ClassLoader之中的代码限定了以java.
开头的不能用自定义类加载器进行加载,这也是为了安全吧。)
- 线程上下文类加载器
以JDBC来说明。JDBC使用了DriverManager这个类来管理不同数据库的驱动,比如mysql驱动,oracle驱动等等。DriverManager属于Java核心类,在rt.jar包中,由启动类加载器进行加载。mysql驱动属于依赖,需要用应用程序类加载器进行加载。
//fixme 为何JDBC打破了双亲委派机制?
首先,了解一下DriverManager如何知道要加载的驱动的类在哪儿?
这需要用到JDK内置的一种机制:SPI(Service Provider Interface)机制,这是一种服务提供发现机制。
简单了解一下SPI机制的原理:在classpath路径下的META-INF/services文件夹中有一个java.sql.Driver文件,文件中记录着驱动类的全限定名。之后JVM就会扫描这个文件夹并把文件记录的驱动进行加载。
类加载器有个规则,如果一个类由类加载器A加载,那么该类的依赖默认都会由相同的类加载器进行加载。在我们编写的类中,使用的依赖诸如String类都是已经被启动类加载器加载过的,双亲委派机制会保证这些类能够被找到。但是在DriverManager类中,由于其一开始就被启动类加载器加载,当DriverManager扫描到有驱动需要被加载时,启动类加载器会首先向上委托给父类,但是启动类加载器没有父类,所以接着启动类加载器尝试加载这个驱动,很明显,由于这个类并不在启动类加载器的加载路径/jre/lib之中,所以加载不了。这时候就会抛出异常。所以双亲委派机制在这种情况下无法起作用,需要打破,让启动类加载器能够将加载请求委托给下级的应用程序类加载器才行。
问题来了,JDBC是如何打破双亲委派机制,让应用程序类加载器加载驱动的呢?
DriverManager的解决方案就是,在DriverManager初始化的时候,得到「线程上下文加载器」,加载驱动的时候,是使用「线程上下文加载器」进行加载的,而这里的「线程上下文加载器」就是应用程序类加载器。换而言之,DriverManager直接指定了加载驱动的加载器。
通过Thread.currentThread().getContextClassLoader()
就可以获取到上下文类加载器了。一般而言,如果不自己手动设置的话,上下文类加载器中保存的还是应用程序类加载器。
关于JDBC是否打破了双亲委派机制,一直都是众说纷纭,从上面的分析来看,说它打破了双亲委派机制没错,因为它不遵循双亲委派,反而用起了反向委派。说它没打破双亲委派也没错,它只是在DriverManager加载完毕之后触发了驱动类的加载,并且它本身加载依然遵循双亲委派机制。两种说法都有一定的正确性。