维基百科关于java虚拟机的描述:

    虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

    jvm的问世,使得代码编译的结果从本地机器码转变成字节码。字节码文件可以在任意安装了jvm的平台上运行,jvm在其中充当一个翻译官,为物理机解释字节码。java程序是跨平台的,但jvm并不是跨平台的。

    class文件从被加载到虚拟机到被卸载,会经历以下这些阶段。
    类加载机制 - 图1
    image.gif
    其中解析所处的位置并不是确定的,在某些情景下解析会在初始化之后开始,目的是支持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:

    1. public class SuperClass {
    2. public static String value = "value defined in super class";
    3. static {
    4. System.out.println("super class init");
    5. }
    6. }
    7. class SubClass extends SuperClass{
    8. static {
    9. System.out.println("sub class init");
    10. }
    11. }
    12. class Main {
    13. public static void main(String[] args) {
    14. System.out.println(SubClass.value);
    15. }
    16. }

    运行结果:

    1. super class init
    2. value defined in super class

    显然,子类的初始化没有被触发。对于静态字段的访问,只有直接定义该字段的类才会被初始化。

    Example2:

    1. class Main {
    2. public static void main(String[] args) {
    3. SuperClass[] superClasses = new SuperClass[10];
    4. }
    5. }

    运行这段程序没有任何输出。可见,新建数组对象,并不会触发数组元素类的初始化。

    Example3:

    进一步,把Example1中的value字段修饰为常量

    1. public class SuperClass {
    2. public static final String value = "value defined in super class";
    3. static {
    4. System.out.println("super class init");
    5. }
    6. }
    7. class SubClass extends SuperClass{
    8. static {
    9. System.out.println("sub class init");
    10. }
    11. }
    12. class Main {
    13. public static void main(String[] args) {
    14. System.out.println(SubClass.value);
    15. }
    16. }

    运行结果:

    1. 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()来获取。

    类加载器的继承关系如下图所示:

    类加载机制 - 图3image.gif

    除了引导类加载器外,其他的类加载器都有一个parent类加载器。这个“parent”并不是继承关系中的父类,而是通过组合的方式实现的,即类加载器都持有一个parent属性来指向它的parent类加载器,可以通过调用getParent()方法获取父加载器实例。类加载器A的parent类加载器是加载A的类加载器,因为类加载器本身也是Java类,它也需要被类加载器加载。用户自定义的类加载器的parent类加载器通常是系统类加载器。类加载器的组合关系如下:
    类加载机制 - 图5
    image.gif
    类的加载过程遵循双亲委派模型:如果一个类加载器收到类加载器请求,它首先将这个请求委托给parent类加载器,每一层类加载器都是如此,所有的类加载请求最终都会到达启动类加载器。只有当父加载器无法加载时,子加载器才尝试自己加载。双亲委派模式的好处是:保证了classpath中的类在各种类加载器环境中都是同一个类,除非是你自定义了类加载器并重写了loadClass方法去加载。来看看ClassLoader.loadClass方法的源码,可以更直观地理解双亲委派模型的执行过程:

    1. protected Class<?> loadClass(String name, boolean resolve)
    2. throws ClassNotFoundException
    3. {
    4. synchronized (getClassLoadingLock(name)) {
    5. // First, check if the class has already been loaded
    6. //检查类是不是已经被加载过,如果已经被加载过则直接返回,避免重复加载。任何类都只允许
    7. //被加载一次
    8. Class c = findLoadedClass(name);
    9. if (c == null) {
    10. long t0 = System.nanoTime();
    11. try {
    12. if (parent != null) {
    13. // parent类加载器不为空则交由parent加载
    14. c = parent.loadClass(name, false);
    15. } else {
    16. // 如果parent为null,则交由引导类加载器来加载
    17. c = findBootstrapClassOrNull(name);
    18. }
    19. } catch (ClassNotFoundException e) {
    20. // ClassNotFoundException thrown if class not found
    21. // from the non-null parent class loader
    22. }
    23. if (c == null) {
    24. // If still not found, then invoke findClass in order
    25. // to find the class.
    26. long t1 = System.nanoTime();
    27. // parent类加载器没有加载到,则自己动手
    28. c = findClass(name);
    29. // this is the defining class loader; record the stats
    30. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
    31. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    32. sun.misc.PerfCounter.getFindClasses().increment();
    33. }
    34. }
    35. if (resolve) {
    36. resolveClass(c);
    37. }
    38. return c;
    39. }
    40. }

    真正完成类的加载工作是通过调用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方法中,因此尽量避免重写该方法。示例:文件系统类加载器

    1. package com.wxy.popcorn.test.classloader;
    2. import java.io.ByteArrayOutputStream;
    3. import java.io.File;
    4. import java.io.FileInputStream;
    5. import java.io.IOException;
    6. import java.io.InputStream;
    7. public class FileSystemClassLoader extends ClassLoader {
    8. private String rootDir;
    9. public FileSystemClassLoader(String rootDir) {
    10. this.rootDir = rootDir;
    11. }
    12. protected Class<?> findClass(String name) throws ClassNotFoundException {
    13. byte[] classBytes = getClassBytes(name);
    14. if (classBytes == null) {
    15. throw new ClassNotFoundException();
    16. }
    17. else {
    18. return defineClass(name, classBytes, 0, classBytes.length);
    19. }
    20. }
    21. private byte[] getClassBytes(String className) {
    22. String path = resolveClassNameToPath(className);
    23. try {
    24. InputStream is = new FileInputStream(path);
    25. ByteArrayOutputStream baos = new ByteArrayOutputStream();
    26. int bufferSize = 4096;
    27. byte[] buffer = new byte[bufferSize];
    28. int bytesNumRead = 0;
    29. while ((bytesNumRead = is.read(buffer)) != -1) {
    30. baos.write(buffer, 0, bytesNumRead);
    31. }
    32. return baos.toByteArray();
    33. } catch (IOException e) {
    34. e.printStackTrace();
    35. }
    36. return null;
    37. }
    38. private String resolveClassNameToPath(String className) {
    39. return rootDir + File.separatorChar
    40. + className.replace('.', File.separatorChar) + ".class";
    41. }
    42. }

    双亲委派模式并不能解决所有的类加载问题。Java提供的API并不总是被用户调用,有时它也需要回调用户的代码。一个典型的例子是JNDI服务,其目的是对资源进行集中管理和查找。JNDI的代码由启动类加载器加载,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但是启动类加载器当然无法加载到这些代码。线程上下文类加载器就是为了解决这个问题而被设计出来的。

    线程上下文类加载器可以通过setContextClassLoader()方法设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在全局范围内都没有设置过,则默认是系统类加载器。JNDI服务便是使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器委托子类加载器去完成类加载,破坏了双亲委派模式。java中所有SPI加载动作都是采用这种方式,如JNDI、JDBC、JCE、JAXB等。参考JDBC驱动的注册和加载方式可以深入地理解上下文类加载器。

    获取MySQL数据库连接的步骤如下:

    1. // Class.forName("com.mysql.jdbc.Driver").newInstance();
    2. String url = "jdbc:mysql://localhost:3306/testdb";
    3. Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");

    在1.6以后,我们就不需要再写第一句来注册mysql驱动了,Java的SPI加载机制已经为我们自动完成了。其具体如何实现的呢?

    先看看DriverManager类,在其静态块里调用了loadInitialDrivers()。

    1. static {
    2. loadInitialDrivers();
    3. println("JDBC DriverManager initialized");
    4. }

    看看loadInitialDrivers()里做了些啥

    1. private static void loadInitialDrivers() {
    2. String drivers;
    3. try {
    4. // 从系统属性获取jdbc驱动类名
    5. drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
    6. public String run() {
    7. return System.getProperty("jdbc.drivers");
    8. }
    9. });
    10. } catch (Exception ex) {
    11. drivers = null;
    12. }
    13. // If the driver is packaged as a Service Provider, load it.
    14. // Get all the drivers through the classloader
    15. // exposed as a java.sql.Driver.class service.
    16. // ServiceLoader.load() replaces the sun.misc.Providers()
    17. // 通过SPI加载驱动
    18. AccessController.doPrivileged(new PrivilegedAction<Void>() {
    19. public Void run() {
    20. ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    21. Iterator<Driver> driversIterator = loadedDrivers.iterator();
    22. /* Load these drivers, so that they can be instantiated.
    23. * It may be the case that the driver class may not be there
    24. * i.e. there may be a packaged driver with the service class
    25. * as implementation of java.sql.Driver but the actual class
    26. * may be missing. In that case a java.util.ServiceConfigurationError
    27. * will be thrown at runtime by the VM trying to locate
    28. * and load the service.
    29. *
    30. * Adding a try catch block to catch those runtime errors
    31. * if driver not available in classpath but it's
    32. * packaged as service and that service is there in classpath.
    33. */
    34. try{
    35. while(driversIterator.hasNext()) {
    36. driversIterator.next();
    37. }
    38. } catch(Throwable t) {
    39. // Do nothing
    40. }
    41. return null;
    42. }
    43. });
    44. println("DriverManager.initialize: jdbc.drivers = " + drivers);
    45. if (drivers == null || drivers.equals("")) {
    46. return;
    47. }
    48. //加载通过系统属性设置的驱动类
    49. String[] driversList = drivers.split(":");
    50. println("number of Drivers:" + driversList.length);
    51. for (String aDriver : driversList) {
    52. try {
    53. println("DriverManager.Initialize: loading " + aDriver);
    54. Class.forName(aDriver, true,
    55. ClassLoader.getSystemClassLoader());
    56. } catch (Exception ex) {
    57. println("DriverManager.Initialize: load failed: " + ex);
    58. }
    59. }
    60. }

    DriverManager在初始化过程中,会通过SPI方式加载数据库驱动,同时也会加载系统参数指定的驱动。在调用ServiceLoader.load方法时,会去获取线程上下文类加载器。
    类加载机制 - 图7image.gif
    在调用driverIterator.hasNext()方法时,会去寻找META-INF/services/java.sql.Driver文件中的驱动名
    类加载机制 - 图9image.gif
    然后,在调用driverIterator.next()方法时,会使用线程上下文类加载器去加载驱动类。
    类加载机制 - 图11image.gif
    在获取数据库连接时,会再次使用线程上下文类加载器来进行校验驱动类是否能够被加载。

    1. private static Connection getConnection(
    2. String url, java.util.Properties info, Class<?> caller) throws SQLException {
    3. /*
    4. * When callerCl is null, we should check the application's
    5. * (which is invoking this class indirectly)
    6. * classloader, so that the JDBC driver class outside rt.jar
    7. * can be loaded from here.
    8. */
    9. ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    10. synchronized (DriverManager.class) {
    11. // synchronize loading of the correct classloader.
    12. // 优先使用caller的类加载器去加载驱动类 ,如果不存在则获取线程上下文类加载器
    13. if (callerCL == null) {
    14. callerCL = Thread.currentThread().getContextClassLoader();
    15. }
    16. }
    17. if(url == null) {
    18. throw new SQLException("The url cannot be null", "08001");
    19. }
    20. println("DriverManager.getConnection(\"" + url + "\")");
    21. // Walk through the loaded registeredDrivers attempting to make a connection.
    22. // Remember the first exception that gets raised so we can reraise it.
    23. SQLException reason = null;
    24. for(DriverInfo aDriver : registeredDrivers) {
    25. // If the caller does not have permission to load the driver then
    26. // skip it.
    27. // 验证驱动类是否能够被加载,这次加载会触发驱动类的初始化
    28. if(isDriverAllowed(aDriver.driver, callerCL)) {
    29. try {
    30. println(" trying " + aDriver.driver.getClass().getName());
    31. Connection con = aDriver.driver.connect(url, info);
    32. if (con != null) {
    33. // Success!
    34. println("getConnection returning " + aDriver.driver.getClass().getName());
    35. return (con);
    36. }
    37. } catch (SQLException ex) {
    38. if (reason == null) {
    39. reason = ex;
    40. }
    41. }
    42. } else {
    43. println(" skipping: " + aDriver.getClass().getName());
    44. }
    45. }
    46. // if we got here nobody could connect.
    47. if (reason != null) {
    48. println("getConnection failed: " + reason);
    49. throw reason;
    50. }
    51. println("getConnection: no suitable driver found for "+ url);
    52. throw new SQLException("No suitable driver found for "+ url, "08001");
    53. }

    至此我们发现DriverManager最终只是调用了Class.forName来加载驱动类,那么驱动类是在哪里实例化的呢?看看mysql的Driver就一目了然了

    1. public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    2. public Driver() throws SQLException {
    3. }
    4. static {
    5. try {
    6. DriverManager.registerDriver(new Driver());
    7. } catch (SQLException var1) {
    8. throw new RuntimeException("Can't register driver!");
    9. }
    10. }
    11. }

    在静态块里往DriverManager里注册了驱动类实例。

    更多类加载器的应用还可以参考osgi、tomcat的类加载机制。欢迎补充!

    问题:

    1. 非显式的类加载动作是何时触发的?

    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()

    1. 类加载过程的简单流程?

    比如,类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。