维基百科关于java虚拟机的描述:
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
jvm的问世,使得代码编译的结果从本地机器码转变成字节码。字节码文件可以在任意安装了jvm的平台上运行,jvm在其中充当一个翻译官,为物理机解释字节码。java程序是跨平台的,但jvm并不是跨平台的。
class文件从被加载到虚拟机到被卸载,会经历以下这些阶段。
其中解析所处的位置并不是确定的,在某些情景下解析会在初始化之后开始,目的是支持java 的动态绑定(运行时绑定)特性。
虚拟机的规范严格规定了有且只有以下5种情形才会触发类的初始化(注意是“有且只有”)。
(1)触发new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有初始化,则先触发其初始化。对应的java代码场景是:使用new关键字实例化一个对象、读取或设置一个类的静态字段(被final修饰、已在编译器将结果放入常量池的静态字段除外)、调用类的静态方法的时候。
(2)使用java.lang.reflect包的方法对类进行反射调用时,如果类没有初始化,则先触发初始化。
(3)初始化一个类时,如果发现其父类还没有初始化,则需先初始化其父类。
(4)当虚拟机启动时,会触发主类( 包含main方法的类)的初始化。
(5)当使用jdk7的动态语言支持时,如果一个MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需先触发其初始化。
这五种触发类初始化的行为称为主动引用。除此之外的所有引用类的地方都不会触发类的初始化,称为被动引用。
Example1:
public class SuperClass {
public static String value = "value defined in super class";
static {
System.out.println("super class init");
}
}
class SubClass extends SuperClass{
static {
System.out.println("sub class init");
}
}
class Main {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
运行结果:
super class init
value defined in super class
显然,子类的初始化没有被触发。对于静态字段的访问,只有直接定义该字段的类才会被初始化。
Example2:
class Main {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[10];
}
}
运行这段程序没有任何输出。可见,新建数组对象,并不会触发数组元素类的初始化。
Example3:
进一步,把Example1中的value字段修饰为常量
public class SuperClass {
public static final String value = "value defined in super class";
static {
System.out.println("super class init");
}
}
class SubClass extends SuperClass{
static {
System.out.println("sub class init");
}
}
class Main {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
运行结果:
value defined in super class
可见,对静态常量的访问不会触发类的初始化。
接口也有初始化过程。其与类的初始化的不同之处体现在第三种情况:当一个接口在初始化时,并不要求其父接口完成初始化,只有在真正使用到父接口的时候(如引用父接口中定义的常量)才会触发父接口的初始化。
接下来重点聊聊类加载阶段中,“通过类全限定名来获取描述此类的二进制字节流”这个动作的实施者:类加载器。这个二进制字节流可能是classpath下的、可能是通过字节码工具动态生成的、还有可能是通过网络下载的。
一个类在虚拟机中的唯一性并不单单由类本身决定,还取决于加载类的类加载器。同一个.class文件,由不同的类加载器实例加载到虚拟机,得到的Class对象是不等的。这意味着类加载器给类添加了一个额外的命名空间。
几乎所有的类加载器都继承自java.lang.ClassLoader(除了引导类加载器)。Java提供的类加载器有三个:
引导类加载器(BootStrap class loader),用来加载核心库。它是由C++语言实现的,属于虚拟机的一部分,其它的类加载器都是独立于虚拟机外部的。BootStrap类加载器负责加载${JAVA_HOME}/lib目录或者被-Xbootclasspath参数指定的路径中的,并且是能被虚拟机识别的,如rt.jar。
扩展类加载器(Extension class loader),由ExtClassLoader实现,用来加载java的扩展库:${JAVA_HOME}/lib/ext目录中的,或者是被java.ext.dirs系统变量指定的类库。
系统类加载器(System class loader),也叫应用程序类加载器(Application class loader),由AppClassLoader实现,用来加载classpath下的类。可以通过ClassLoader.getSystemClassLoader()来获取。
类加载器的继承关系如下图所示:
除了引导类加载器外,其他的类加载器都有一个parent类加载器。这个“parent”并不是继承关系中的父类,而是通过组合的方式实现的,即类加载器都持有一个parent属性来指向它的parent类加载器,可以通过调用getParent()方法获取父加载器实例。类加载器A的parent类加载器是加载A的类加载器,因为类加载器本身也是Java类,它也需要被类加载器加载。用户自定义的类加载器的parent类加载器通常是系统类加载器。类加载器的组合关系如下:
类的加载过程遵循双亲委派模型:如果一个类加载器收到类加载器请求,它首先将这个请求委托给parent类加载器,每一层类加载器都是如此,所有的类加载请求最终都会到达启动类加载器。只有当父加载器无法加载时,子加载器才尝试自己加载。双亲委派模式的好处是:保证了classpath中的类在各种类加载器环境中都是同一个类,除非是你自定义了类加载器并重写了loadClass方法去加载。来看看ClassLoader.loadClass方法的源码,可以更直观地理解双亲委派模型的执行过程:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//检查类是不是已经被加载过,如果已经被加载过则直接返回,避免重复加载。任何类都只允许
//被加载一次
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// parent类加载器不为空则交由parent加载
c = parent.loadClass(name, false);
} else {
// 如果parent为null,则交由引导类加载器来加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// parent类加载器没有加载到,则自己动手
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
真正完成类的加载工作是通过调用defineClass方法完成的,而启动类的加载过程则是通过调用loadClass方法完成的。发起类加载过程的类加载器称为初始加载器,最终完成对类的定义的类加载器称为定义加载器。判断两个类是否相同时,参考的是定义加载器。一个类的定义加载器是这个类引用的其它类的初始加载器。findClass()方法抛ClassNotFoundException,defineClass()方法抛NoClassDefFoundError。
显式地加载类:
(1)Class.forName(String name) 或 Class.forName(String name, boolean initialize, ClassLoader loader)
(2)new UserClassLoader().loadClass(String name)
(3)Thread.getCurrent().getContextClassLoader().loadClass(String name)
自定义类加载器:
自定义类加载器通常只需要重写findClass方法。双亲委派模式以及避免类重复加载的逻辑都体现在loadClass方法中,因此尽量避免重写该方法。示例:文件系统类加载器
package com.wxy.popcorn.test.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes = getClassBytes(name);
if (classBytes == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classBytes, 0, classBytes.length);
}
}
private byte[] getClassBytes(String className) {
String path = resolveClassNameToPath(className);
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = is.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String resolveClassNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
双亲委派模式并不能解决所有的类加载问题。Java提供的API并不总是被用户调用,有时它也需要回调用户的代码。一个典型的例子是JNDI服务,其目的是对资源进行集中管理和查找。JNDI的代码由启动类加载器加载,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但是启动类加载器当然无法加载到这些代码。线程上下文类加载器就是为了解决这个问题而被设计出来的。
线程上下文类加载器可以通过setContextClassLoader()方法设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在全局范围内都没有设置过,则默认是系统类加载器。JNDI服务便是使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器委托子类加载器去完成类加载,破坏了双亲委派模式。java中所有SPI加载动作都是采用这种方式,如JNDI、JDBC、JCE、JAXB等。参考JDBC驱动的注册和加载方式可以深入地理解上下文类加载器。
获取MySQL数据库连接的步骤如下:
// Class.forName("com.mysql.jdbc.Driver").newInstance();
String url = "jdbc:mysql://localhost:3306/testdb";
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
在1.6以后,我们就不需要再写第一句来注册mysql驱动了,Java的SPI加载机制已经为我们自动完成了。其具体如何实现的呢?
先看看DriverManager类,在其静态块里调用了loadInitialDrivers()。
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
看看loadInitialDrivers()里做了些啥
private static void loadInitialDrivers() {
String drivers;
try {
// 从系统属性获取jdbc驱动类名
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
// 通过SPI加载驱动
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
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);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
DriverManager在初始化过程中,会通过SPI方式加载数据库驱动,同时也会加载系统参数指定的驱动。在调用ServiceLoader.load方法时,会去获取线程上下文类加载器。
在调用driverIterator.hasNext()方法时,会去寻找META-INF/services/java.sql.Driver文件中的驱动名
然后,在调用driverIterator.next()方法时,会使用线程上下文类加载器去加载驱动类。
在获取数据库连接时,会再次使用线程上下文类加载器来进行校验驱动类是否能够被加载。
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized (DriverManager.class) {
// synchronize loading of the correct classloader.
// 优先使用caller的类加载器去加载驱动类 ,如果不存在则获取线程上下文类加载器
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
// 验证驱动类是否能够被加载,这次加载会触发驱动类的初始化
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
至此我们发现DriverManager最终只是调用了Class.forName来加载驱动类,那么驱动类是在哪里实例化的呢?看看mysql的Driver就一目了然了
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
在静态块里往DriverManager里注册了驱动类实例。
更多类加载器的应用还可以参考osgi、tomcat的类加载机制。欢迎补充!
问题:
- 非显式的类加载动作是何时触发的?
even if your class A uses class B, class B will not be loaded until that line of code in A is actually run.
即使类A使用了类B,只有A的代码被真正执行,才会触发B的加载。如实例化:new A(),静态调用A.doSomething()
- 类加载过程的简单流程?
比如,类A被类加载器CLA加载,则CLA是类A的定义类加载器。类A使用了类B,当A中代码被执行时,触发类B的加载,这个加载过程由类A的加载器CLA发起,CLA先尝试用其parent CLB来加载B(如果CLA没有重写loadClass()方法中的双亲委派逻辑),如果CLB成功加载了B,那么CLB是B的定义类加载器,如果CLB没有加载到B,则CLA去加载,如果成功,则CLA是B的定义类加载器,如果失败,则抛出ClassNotFoundException或NoClassDefFoundError。