JVM-类加载

1.类加载阶段

1.1 加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用

_super 即父类

_fields 即成员变量

_methods 即方法

_constants 即常量池

_class_loader 即类加载器

_vtable 虚方法表

_itable 接口方法表

如果这个类还有父类没有加载,先加载父类

加载和链接可能是交替运行的

JVM-类加载_第1张图片

 

注意:

instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中,可以通过HSDB 工具查看:

JVM-类加载_第2张图片

1.2 链接

1.验证:验证类是否符合 JVM规范,安全性检查

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

2.准备:为 static 变量分配空间,设置默认值

static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾

static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成

如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

3.解析:将常量池中的符号引用解析为直接引用

JVM-类加载_第3张图片

1.3 初始化

初始化即调用()方法,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

概括得说,类初始化是【懒惰的】

        main 方法所在的类,总会被首先初始化首次访问这个类的静态变量或静态方法时

        子类初始化,如果父类还没初始化,会初始化

        子类访问父类的静态变量,只会触发父类的初始化

        Class.forName

        new 会导致初始化

不会导致类初始化的情况:

        访问类的 static final 静态常量(基本类型和字符串)不会触发初始化

        类对象.class 不会触发初始化

        创建该类的数组不会触发初始化

        类加载器的 loadClass 方法

        Class.forName 的参数为 false 时

2. 类加载器

以 JDK 8 为例:

JVM-类加载_第4张图片

加载低层(如应用程序)加载器时 会向上层的询问 有没有加载 如果上层的有加载过的 那下面的就不用加载了

2.1 启动类加载器

用 Bootstrap 类加载器加载类:

public class F { 
    static {
        System.out.println("bootstrap F init");
    }
}

执行

public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException { 
        Class aClass = Class.forName("cn.itcast.jvm.t3.load.F"); 
        System.out.println(aClass.getClassLoader());
    }
}

输出

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. 
cn.itcast.jvm.t3.load.Load5
bootstrap F init 
null

JVM-类加载_第5张图片

2.2 扩展类加载器

public class G { 
    static {
        System.out.println("classpath G init");
    }
}

执行

public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException { 
        Class aClass = Class.forName("cn.itcast.jvm.t3.load.G"); System.out.println(aClass.getClassLoader());
    }
}

输出

classpath G init 
sun.misc.Launcher$AppClassLoader@18b4aac2

写一个同名的类

public class G { 
    static {
        System.out.println("ext G init");
    }
}

打个 jar 包

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext

重新执行 Load5_2

因为已经有扩展类加载器 就不会加载应用程序加载器

2.3 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

注意:

这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

例如:

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
    // 1. 检查该类是否已经加载
    Class c = findLoadedClass(name); 
    if (c == null) {
        long t0 = System.nanoTime(); 
        try {
            if (parent != null) {
                // 2. 有上级的话,委派上级 
                loadClass c = parent.loadClass(name, false);
            } else {
                // 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader 尝试加载
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
        }

        if (c == null) {
            long t1 = System.nanoTime();
            // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
            c = findClass(name);
    
            // 5. 记录耗时
            sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 
            sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 
            sun.misc.PerfCounter.getFindClasses().increment();
         }
    }
    if (resolve) { 
        resolveClass(c);
    }
    return c;
    }
}

执行流程为:

JVM-类加载_第6张图片

2.4 线程上下文类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让com.mysql.jdbc.Driver正确加载的,你知道是怎么做的吗?

让我们追踪一下源码:

public class DriverManager {
    // 注册驱动的集合
    private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();

    // 初始化驱动
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() { String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction
 
() {
 


public String run() {
return System.getProperty("jdbc.drivers");
 
}
});
} catch (Exception ex) { drivers = null;
}
// 1) 使 用 ServiceLoader 机 制 加 载 驱 动 , 即 SPI AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator(); try{
while(driversIterator.hasNext()) { driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);

// 2)使用 jdbc.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);
// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

JVM-类加载_第7张图片

这样就可以使用

JVM-类加载_第8张图片

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

·JDBC

·Servlet 初始化器

·Spring 容器

·Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

JVM-类加载_第9张图片

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类LazyIterator 中:

2.5 自定义类加载器

问问自己,什么时候需要自定义类加载器

1) 想加载非 classpath 随意路径中的类文件

2) 都是通过接口来使用实现,希望解耦时,常用在框架设计

3) 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

1. 继承 ClassLoader 父类

2. 要遵从双亲委派机制,重写 findClass 方法

注意不是重写 loadClass 方法,否则不会走双亲委派机制

3. 读取类文件的字节码

4. 调用父类的 defineClass 方法来加载类 解析

5. 使用者调用该类加载器的 loadClass 方法示例:

准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:

你可能感兴趣的:(jvm,java,开发语言)