深刻理解Java虚拟机读书笔记-第7章 虚拟机类加载机制

第7章 虚拟机类加载机制

7.1 概述

类加载机制:从Class文件到内存中Java类型的过程。各个阶段时间段上能够有重叠。 类加载是在运行期间执行的,也描述为动态加载和动态链接。java

7.2 类加载时机

截屏2020-08-24下午1.28.17.png 对于何时开始加载, 《Java虚拟机规范》没有强制约束。可是严格规定了有且只有六种状况,若是类没有初始化,必须当即对类进行"初始化" (加载、链接必然会先执行),称之为主动引用:数据库

  • 遇到new、getstatic、putstatic、invokestatic字节码指令时
    • 指令new实例化对象。
    • 指令getstatic/putstatic 访问其静态对象(被final修饰,编译期已放入常量池的除外)。
    • 指令invokestatic,调用其静态方法。
  • 反射调用
  • 子类初始化时,先触发父类的初始化
  • 虚拟机启动时,初始化用户指定的要执行的主类
  • MethodHandle解析结果为REF_getStatic,REF_setStatic,REF_invokeStaitc,Ref_newInvokeSpecial
  • 当一个接口定义了JDK 8新加入的默认方法(default修饰)

不会触发类初始化的几个场景举例:数组

  • 经过子类引用父类的静态字段,不会致使子类初始化
  • 经过数组定义引用类,不会触发此类的初始化
  • 引用在编译期已被放入常量池的常量。
//场景一 经过子类引用父类的静态字段,不会致使子类初始化
public class SuperClass {
    static { System.out.println("SuperClass init!");}
    public static int value=123;
}

public class SubClass extends SuperClass{
    static { System.out.println("SubClass init!");}
}

public class NoInitialization1 {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

复制代码
//场景二 经过数组定义引用类,不会触发此类的初始化
public class NoInitialization2 {
    public static void main(String[] args) {
        SuperClass[] scarray=new SuperClass[10];
    }
}
复制代码
//场景三 引用在编译期已被放入常量池的常量。
public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }
    public static final String HELLOWRLD="hello world";
}
public class NoInitialization3 {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWRLD);
    }
}
复制代码

对于场景三咱们查看NoInitialization3的字节码发现,“hello world”已经在其常量池中,使用 ldc指令将常量压入栈中。而System.out则是使用getstatic指令。这个地方不涉及到ConstClass的初始化安全

Constant pool:
  #4 = String             #25            // hello world
  #25 = Utf8               hello world
{
  public static void main(java.lang.String[]);
    Code:
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #4                  // String hello world
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}
复制代码

7.3类加载过程

7.3.1 加载

加载(loading):从静态文件到运行时方法区。完成三件事情:markdown

  • 经过一个类的全限定名获取定义此类的二进制字节流。
    • 能够从ZIP包中读取(JAR,WAR等等)
    • 从网络中获取,好比Web Applet
    • 运行时计算生成,好比动态代理技术, “*$Proxy”代理类
    • 数据库中读取
    • 加密文件中读取
    • ......
  • 将该字节流的类静态存储结构转化成方法区的运行时数据结构。
  • 在内存中生成一个表明这个类的java.lang.Class对象,做为方法区中这个类的各类数据的访问入口。

使用Java虚拟机内置的引导类加载器,或者用户自定义的类加载器。网络

7.3.2 验证

确保字节流符合《Java虚拟机规范》的约束,代码安全性问题验证。验证是重要的,但不是必须的。 四个阶段:数据结构

  • 文件格式验证,此阶段经过后,会存储到方法区。后面阶段基于方法区数据进行验证,再也不读取字节流。
  • 元数据验证,类元数据信息语义校验
  • 字节码验证,最复杂,对类的Code部分进行检验分析。程序语义合法性,安全性等等
  • 符号引用验证,在解析过程当中发生,若是没法经过符号引用验证,Java虚拟机会抛出java.lang.IncompatibleClassChangeError的子类异常,如 NoSuchFieldError,NoSuchMethodError等等。

7.3.3 准备

一般状况下,为类变量(静态变量),分配内存并设置初始值(零值)。初值并非代码中赋的值123。123要等到初始化阶段。多线程

public static int value = 123;
复制代码

编译成class文件app

public static int value;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
...
 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
         0: bipush        123
         2: putstatic     #2                  // Field value:I
         5: return     
...
复制代码

某些状况下,设置初始值为456。好比final修饰的变量。由于变量值456,会提早加入到常量池。ide

public static final int value2 = 456;
复制代码
public static final int value2;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 456
复制代码

7.3.4 解析

将常量池内的符号引用替换为直接引用的过程。 好比说这种,咱们要把 #2替换成实际的类引用,若是是未加载过的类引用,又会涉及到这个类加载过程。

getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
复制代码
  • 类或接口解析
  • 字段解析
  • 方法解析
  • 接口方法解析

7.3.5 初始化

执行类构造器()方法,非实例构造器()方法 。 ()方法:执行类变量赋值语句和静态语句块(static{})。顺序为其在源文件中顺序决定。 举例1:非法向前引用变量。 value的定义在 static{} 以后,只能赋值,不能读取值。

public class PrepareClass {
    static {
        value=3;
        System.out.println(value);// value: illegal forword reference
    }
    public static int value=123;
}
复制代码

可是下面就能够

public class PrepareClass {
    public static int value=123;
    static {
        value=3;
        System.out.println(value);// value: illegal forword reference
    }
}
复制代码

class文件参考

0: bipush        123
2: putstatic     #2                  // Field value:I
5: iconst_3
6: putstatic     #2                  // Field value:I
9: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
12: getstatic     #2                  // Field value:I
15: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
18: return
复制代码

举例2: ()执行顺序。子类初始化时,要先初始化父类

public class TestCInitClass2 {

    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }

    static class Sub extends Parent {
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}
复制代码

输出:

2
复制代码

Java虚拟机必须保证()方法在多线程环境下的同步问题。

7.4 类加载器

实现“经过一个类的全限定名来获取其二进制字节流”的代码,称之为“类加载器”(Class Loader)。

7.4.1 类与类加载器

类与其加载器肯定了这个类在Java虚拟机中的惟一性。

三层类加载器,绝大多数Java程序会用到如下三个系统提供的类加载器进行加载:

  • 启动类加载器(BootStrap Class Loader)
  • 扩展类加载器(Extension Class Loader)
  • 应用程序类加载器(Application Class Loader)

除了以上三个还有用户自定义的加载器,经过集成java.lang.ClassLoader类来实现。

启动类加载器

加载Java的核心库,native代码实现,不继承java.lang.ClassLoader

URL[]  urls= sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
    System.out.println(url);
}

结果输出:
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/resources.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jsse.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jce.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/charsets.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jfr.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/classes
复制代码

扩展类加载器

加载Java的扩展库,加载ext目录下的Java类

URL[]  urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}

结果输出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
复制代码

应用程序类加载器

加载Java应用的类。经过ClassLoader.getSystemClassLoader()来获取。

URL[]  urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}

结果输出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
...
file:/.../jdk1.8.0_73.jdk/Contents/Home/lib/tools.jar
file:/.../java_sample/out/production/java_sample/  //这是咱们的应用程序
file:/Applications/IntelliJ%20IDEA.app/Contents/lib/idea_rt.jar
复制代码

自定义类加载器

7.4.2 双亲委派模型截屏2020-08-25下午5.04.25.png

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) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        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.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
复制代码

AppClassLoader,ExtClassLoader都继承URLClassLoader。 URLClassLoader.findClass(name)

protected Class<?> findClass(final String name)throws ClassNotFoundException {
       // 一、安全检查
    // 二、根据绝对路径把硬盘上class文件读入内存
    byte[] raw = getBytes(name); 
    // 三、将二进制数据转换成class对象
    return defineClass(raw);
    }
复制代码

若是咱们本身去实现一个类加载器,基本上就是继承ClassLoader以后重写findClass方法,且在此方法的最后调包defineClass。 ** 双亲委派确保类的全局惟一性。 例如不管哪一个类加载器须要加载java.lang.Object,都会委托给最顶端的启动类加载器加载。

参考: 通俗易懂 启动类加载器、扩展类加载器、应用类加载器 深刻探讨 Java 类加载器

7.4.3 线程上下文类加载器

线程上下文类加载器(context class loader),能够从java.lang.Thread中获取。 双亲委派模型不能解决Java应用开发中遇到的全部类加载器问题。 例如,Java提供了不少服务提供者接口(Service Provider Interface,SPI),容许第三方提供接口实现。常见的SPI有JDBC,JCE,JNDI,JAXP等。SPI接口由核心库提供,由引导类加载器加载。 而其第三方实现,由应用类加载器实现。此时SPI就找不到具体的实现了。 SPI接口代码中使用线程上下文类加载器。线程上下文类加载器默认为应用类加载器。

相关文章
相关标签/搜索