维基百科关于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 initvalue 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 statssun.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。
