Java类加载
1 概述
1.1 类加载机制
类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
1.2 类加载过程
Class类型通常以文件的形式存在(当然任何二进制流都可以是Class类型),只有被Java虚拟机装在的Class类型才能在程序中使用。
系统装在Class类型可以分为加载、连接和初始化3个步骤。其中,连接又可以分为验证、准备和解析3步。
1.2.1 加载类
加载类处于类加载的第一个阶段。在加载类时,Java虚拟机必须完成以下工作:
- 通过类的全限定名,获取类的二进制数据流;
- 解析类的二进制数据流为方法区内的数据结构;
- 创建java.lang.Class类的实例,表示该类型。
1.2.2 验证类
当类加载到系统后,就开始连接操作,验证是连接操作的第一步。目的是保证加载的字节码是合法、合理并符合规范的。
1.2.3 准备
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段虚拟机会为这个类分配相应的内存空间,并设置初始值。
1.2.4 解析类
解析阶段的工作是将类、接口、字段和方法的符号引用转为直接引用。
1.2.5 初始化
为类的静态变量赋予正确的初始值。
初始化的顺序:
1.3 类加载的条件
Class只要在必须要使用的时候才会被加载,虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或接口在初次使用前必须进行初始化。这里的“使用”指的是主动使用。分为以下几种情况:
- 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
- 当调用类的静态方法时,即当使用了字节码invokestatic指令。
- 当使用类或接口的静态字段时(final常量除外),比如使用getstatic或者putstatic指令。
- 当使用java.lang.reflect包中的方法反射类的方法时。
- 当初始化子类时,要求先初始化父类。
- 作为启动虚拟机,含有main()方法的那个类。
除了上述情况,其他情况都不会引起类的初始化。
2 类加载器
2.1 类加载器子系统
虚拟机设计团队将类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部实现,以便让应用程序自己去决定如何去获取所需要的的类。实现这个动作的代码模块称为类加载器。
2.2 类加载器的分类
在标准的Java程序中,Java虚拟机会创建3类ClassLoader为整个应用程序服务。分别是:
- BootStrap ClassLoader(启动类加载器):负责加载<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数路径指定的类库。
- Extension ClassLoader(扩展类加载器):负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量指定路径中的类库。
- App ClassLoader(应用类加载器,也称为系统类加载器):负责加载用户类路径中(ClassPath)的类库。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。
以上类加载器中,BootStrap ClassLoader由C++实现,是虚拟机自身的一部分。其余类加载器由Java实现,独立于虚拟机外,并且全都继承自抽象类java.lang.ClassLoader。
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求。
2.3 双亲委派模型
2.3.1 概念
在双亲委派模型中,除了引导类加载器之外,所有的类加载器都有一个父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而都是通过组合的关系来复用父加载器的代码。类加载器之间的组织结构如下图所示。
双亲委派模型的工作过程:如果一个类加载器接收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都会这么做,直到请求委派至启动类加载器。这时,父类加载器如果不能完成类加载请求,才会交由子类加载器去加载。
优点:双亲委派模式可以保证Java程序的稳定运作。
2.3.2 Tomcat案例
Tomcat等主流Java Web服务器实现了自定义的类加载器(不止一个),目的是为了解决一下问题:
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。(过多的重复类库使方法区容易出现过度膨胀)
- 服务器尽可能保证自身的安全不受部署的Web应用程序的影响。
- 支持JSP应用的Web服务器,大多支持HotSwap功能。
由于可能存在上述问题,单独的ClassPath就无法满足需求了,所以各种Web服务器都提供了好几个ClassPath路径提供用户存放第三方类库。
在Tomcat目录中,有3组目录:
- /common/* :类库可被Tomcar和所有Web应用程序共用。
- /server/* :仅Tomcat可用,对Web应用不可见。
- /shared/* :所有Web应用共用,对Tomcat不可见。
加上Web应用程序的目录:
- /WebApp/WEB-INF/* :仅对此Web应用可用,对Tomcat和其他Web应用不可见。
2.4 自定义类加载器
2.4.1 文件类加载器
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
测试:
public class Message {
public void echo(String message){
System.out.println(message);
}
}
public static void main(String[] args) throws Exception {
FileSystemClassLoader loader = new FileSystemClassLoader("F:");
Class<?> clazz = loader.findClass("Message");
Object o = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("echo",String.class);
method.invoke(o,"hello");
}
2.4.2 网络类加载器
public class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootUrl + "/"
+ className.replace('.', '/') + ".class";
}
}
public static void main(String[] args) throws Exception {
NetworkClassLoader loader = new NetworkClassLoader("file:///F:");
Class<?> clazz = loader.findClass("Message");
Object o = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("echo",String.class);
method.invoke(o,"hello");
}
2.5 打破双亲委派
2.5.1 SPI与线程上下文类加载器
2.5.1.1 SPI
SPI(Service Provider Interface,服务提供者接口):提供给服务提供厂商与扩展框架功能的开发者使用的接口。
有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
常见的SPI包括:JDBC、JCE、JNDI、JAXP和JBI等。
2.5.1.2 线程上下文类加载器
线程上下文类加载器是JDK1.2开始引入的,getContextClassLoader()和setContextClassLoader(ClassLoader cl)分别是获取和设置当前线程的上下文类加载器,如果当前线程没有上下文类加载器,那么它将和父线程保持同样的类加载器。
站在开发者的角度,其他线程都是由Main线程,也就是main()函数所在的线程派生的,它是其他线程的父线程或者祖父线程。
System.out.println(Thread.currentThread().getContextClassLoader());
out:
sun.misc.Launcher$AppClassLoader@18b4aac2
2.5.1.3 JDBC案例
Java中使用JDBC这个SPI完全透明了应用程序和第三方厂商数据库库驱动的具体实现,不管数据库如何切换,应用程序只需要替换JDBC的驱动jar包以及数据库的名称即可,而不需要进行任何更新。
不需要SPI,也可以直接加载数据库驱动,例如:
Class.forName("com.mysql.jdbc.Driver").newInstance();
下面是mysql注册驱动及获取connection的过程,没有Class.forName,但依然可以正常运行,这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。
String url = "jdbc:mysql://localhost:3306/testdb";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
那到底是在哪一步自动注册了mysql driver的呢?重点就在DriverManager.getConnection()中。我们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块,DriverManager的静态代码块就是:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
DriverManager静态代码块
初始化方法loadInitialDrivers()的代码如下:
private static void loadInitialDrivers() {
String drivers;
try {
// 先读取系统属性
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 通过SPI加载驱动类
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// 继续加载系统属性中的驱动类
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 使用AppClassloader加载
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
从上面可以看出JDBC中的DriverManager的加载Driver的步骤顺序依次是:
- 通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载;
- 通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载。
下面详细分析SPI加载的那段代码:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法,也就是最开始我们注释掉的那一句代码。好,那句因SPI而省略的代码现在解释清楚了,那我们继续看给这个方法传的loader是怎么来的。
因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader 。这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中。
再看下看ServiceLoader.load(Class)的代码,的确如此:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。
SPI机制直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。
Driver静态代码块
com.mysql.jdbc.Driver加载后运行的静态代码块:
static {
try {
// Driver已经加载到TCCL中了,此时可以直接实例化
java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
registerDriver方法将driver实例注册到系统的java.sql.DriverManager类中,其实就是add到它的一个名为registeredDrivers的静态成员CopyOnWriteArrayList中 。
到此驱动注册基本完成,接下来我们回到最开始的那段样例代码:java.sql.DriverManager.getConnection()。它最终调用了以下方法:
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/* 传入的caller由Reflection.getCallerClass()得到,该方法
* 可获取到调用本方法的Class类,这儿获取到的是当前应用的类加载器
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
SQLException reason = null;
// 遍历注册到registeredDrivers里的Driver类
for(DriverInfo aDriver : registeredDrivers) {
// 检查Driver类有效性
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
// 调用com.mysql.jdbc.Driver.connect方法获取连接
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
throw new SQLException("No suitable driver found for "+ url, "08001");
}
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
// 传入的classLoader为调用getConnetction的当前类加载器,从中寻找driver的class对象
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
// 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个
// driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
由于TCCL本质就是当前应用类加载器,所以之前的初始化就是加载在当前的类加载器中,这一步就是校验存放的driver是否属于调用者的Classloader。例如在下文中的tomcat里,多个webapp都有自己的Classloader,如果它们都自带 mysql-connect.jar包,那底层Classloader的DriverManager里将注册多个不同类加载器的Driver实例,想要区分只能靠TCCL了。