1. 类加载阶段

1.1 加载阶段

  • 将类的字节码载入方法区中,内部采用 c++ 的 instanceklass 描述 java 类,它的重要 field 有:
    • _java_mirror 即 java 的类镜像,例如对 string 来说,就是 string.class,作用是把 klass 暴 露给 java 使用
    • _super 即父类
    • _fields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,则先触发父类的加载。
  • 加载和链接可能是交替运行的。

注意:

  • instanceklass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
  • 可以通过前面介绍的 hsdb 工具查看

1.2 链接阶段

验证

验证类是否符合 jvm规范,安全性检查,阻止不合法的类继续运行。用 ue 等支持二进制的编辑器修改 helloworld.class的魔数,在控制台运行:

e:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.helloworld
error: a jni error has occurred, please check your installation and try again
exception in thread "main" java.lang.classformaterror: incompatible magic value
3405691578 in class file cn/itcast/jvm/t5/helloworld
        at java.lang.classloader.defineclass1(native method)
        at java.lang.classloader.defineclass(classloader.java:763)
        at
java.security.secureclassloader.defineclass(secureclassloader.java:142)
        at java.net.urlclassloader.defineclass(urlclassloader.java:467)
        at java.net.urlclassloader.access$100(urlclassloader.java:73)
        at java.net.urlclassloader$1.run(urlclassloader.java:368)
        at java.net.urlclassloader$1.run(urlclassloader.java:362)
        at java.security.accesscontroller.doprivileged(native method)
        at java.net.urlclassloader.findclass(urlclassloader.java:361)
        at java.lang.classloader.loadclass(classloader.java:424)
        at sun.misc.launcher$appclassloader.loadclass(launcher.java:331)
        at java.lang.classloader.loadclass(classloader.java:357)
        at sun.launcher.launcherhelper.checkandloadmain(launcherhelper.java:495)

准备

为 static 变量分配空间,设置默认值:

  • static 变量在 jdk 7 之前存储于 instanceklass 末尾,从 jdk 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶 段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
  • 将常量池中的符号引用解析为直接引用

解析

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

/**
* 解析的含义
*/
public class load2 {
    public static void main(string[] args) throws classnotfoundexception,ioexception {
        classloader classloader = load2.class.getclassloader();
        // loadclass 方法不会导致类的解析和初始化
        class<?> c = classloader.loadclass("cn.itcast.jvm.t3.load.c");
        // new c();
        system.in.read();
    }
}
class c {
	d d = new d();
}
class d {
}

1.3 初始化阶段

< init()> v 方法

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

发生的时机

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

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • class.forname
  • new 会导致初始化

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

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadclass 方法

测试代码:

class a {
    static int a = 0;
    static {
    	system.out.println("a init");
    }
}
class b extends a {
    final static double b = 5.0;
    static boolean c = false;
    static {
    	system.out.println("b init");
    }
}

验证(测试时请先全部注释,每次只执行其中一个)

public class load3 {
    // main方法的所在类总会被先初始化
    static {
    	system.out.println("main init");
    }
    public static void main(string[] args) throws classnotfoundexception {
        // 1. 静态常量(基本类型和字符串)不会触发初始化
        system.out.println(b.b);
        // 2. 类对象.class 不会触发初始化
        system.out.println(b.class);
        // 3. 创建该类的数组不会触发初始化
        system.out.println(new b[0]);
        // 4. 不会初始化类 b,但会加载 b、a
        classloader cl = thread.currentthread().getcontextclassloader();
        cl.loadclass("cn.itcast.jvm.t3.b");
        // 5. 不会初始化类 b,但会加载 b、a
        classloader c2 = thread.currentthread().getcontextclassloader();
        class.forname("cn.itcast.jvm.t3.b", false, c2);
        // 1. 首次访问这个类的静态变量或静态方法时
        system.out.println(a.a);
        // 2. 子类初始化,如果父类还没初始化,会引发
        system.out.println(b.c);
        // 3. 子类访问父类静态变量,只触发父类初始化
        system.out.println(b.a);
        // 4. 会初始化类 b,并先初始化类 a
        class.forname("cn.itcast.jvm.t3.b");
    }
}

1.4 练习

从字节码分析,使用 a,b,c 这三个常量是否会导致 e 初始化:

public class load4 {
    public static void main(string[] args) {
        system.out.println(e.a);
        system.out.println(e.b);
        system.out.println(e.c);
    }
}
class e {
    public static final int a = 10;
    public static final string b = "hello";
    public static final integer c = 20;
}

典型应用 – 完成懒惰初始化单例模式:

public final class singleton {
    private singleton() { }
    // 内部类中保存单例
    private static class lazyholder {
        static final singleton instance = new singleton();
    }
    // 第一次调用 getinstance 方法,才会导致内部类加载和初始化其静态成员
    public static singleton getinstance() {
    	return lazyholder.instance;
    }
}

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的

2. 类加载器

以 jdk 8 为例:

名称 加载哪的类 说明
bootstrap classloader(启动类加载器) java_home/jre/lib 无法直接访问
extension classloader(扩展类加载器) java_home/jre/lib/ext 上级为 bootstrap,显示为 null
application classloader(应用程序类加载器) classpath 上级为 extension
自定义类加载器 自定义 上级为 application

类加载器的优先级(由高到低):启动类加载器 -> 扩展类加载器 -> 应用程序类加载器 -> 自定义类加载器

2.1 启动类加载器

用 bootstrap 类加载器加载类:

package cn.itcast.jvm.t3.load;
public class f {
    static {
   		system.out.println("bootstrap f init");
    }
}

执行:

package cn.itcast.jvm.t3.load;
public class load5_1 {
    public static void main(string[] args) throws classnotfoundexception {
        class<?> aclass = class.forname("cn.itcast.jvm.t3.load.f");
        // aclass.getclassloader():获得aclass对应的类加载器
        system.out.println(aclass.getclassloader());
    }
}

输出:

  • -xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
  • 可以有以下几个方式替换启动类路径下的核心类:
    • java -xbootclasspath: < new bootclasspath>
    • 前追加:java -xbootclasspath/a:<追加路径>
    • 后追加:java -xbootclasspath/p:<追加路径>

2.2 扩展类加载器

package cn.itcast.jvm.t3.load;
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 // 这个类是由应用程序加载器加载

写一个同名的类:

package cn.itcast.jvm.t3.load;
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 // 将g.class打jar包
已添加清单
正在添加: cn/itcast/jvm/t3/load/g.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到java_home/jre/lib/ext(扩展类加载器加载的类必须是以jar包方式存在),重新执行 load5_2

输出:

ext g init
sun.misc.launcher$extclassloader@29453f44 // 这个类是由扩展类加载器加载

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;
    }
}

例如:

public class load5_3 {
    public static void main(string[] args) throws classnotfoundexception {
        class<?> aclass = load5_3.class.getclassloader()
        			.loadclass("cn.itcast.jvm.t3.load.h");
        system.out.println(aclass.getclassloader());
    }
}

执行流程为:

  • sun.misc.launcher$appclassloader // 1 处, 开始查看已加载的类,结果没有
  • sun.misc.launcher$appclassloader // 2 处,委派上级 sun.misc.launcher$extclassloader.loadclass()
  • sun.misc.launcher$extclassloader // 1 处,查看已加载的类,结果没有
  • sun.misc.launcher$extclassloader // 3 处,没有上级了,则委派 bootstrapclassloader 查找
  • bootstrapclassloader 是在 java_home/jre/lib 下找 h 这个类,显然没有
  • sun.misc.launcher$extclassloader // 4 处,调用自己的 findclass 方法,是在java_home/jre/lib/ext 下找 h 这个类,显然没有,回到 sun.misc.launcher$appclassloader 的 // 2 处
  • 继续执行到 sun.misc.launcher$appclassloader // 4 处,调用它自己的 findclass 方法,在 classpath 下查找,找到了

2.4 线程上下文类加载器

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

class.forname("com.mysql.jdbc.driver")

也是可以让 com.mysql.jdbc.driver 正确加载的,你知道是怎么做的吗? 让我们追踪一下源码:

public class drivermanager {
    // 注册驱动的集合
    private final static copyonwritearraylist<driverinfo> 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<string>() {
            public string run() {
            	return system.getproperty("jdbc.drivers");
            }
        });
    } catch (exception ex) {
    	drivers = null;
    }
    // 1)使用 serviceloader 机制加载驱动,即 spi
    accesscontroller.doprivileged(new privilegedaction<void>() {
    	public void run() {
            serviceloader<driver> loadeddrivers = serviceloader.load(driver.class);
            iterator<driver> 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 包下,以接口全限定名名为文件,文件内容是实现类名称

这样就可以使用:

serviceloader<接口类型> allimpls = serviceloader.load(接口类型.class);
iterator<接口类型> iter = allimpls.iterator();
while(iter.hasnext()) {
	iter.next();
}

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

  • jdbc
  • servlet 初始化器
  • spring 容器
  • dubbo(对 spi 进行了扩展)

接着看 serviceloader.load 方法:

public static <s> serviceloader<s> load(class<s> service) {
    // 获取线程上下文类加载器
    classloader cl = thread.currentthread().getcontextclassloader();
    return serviceloader.load(service, cl);
}

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

private s nextservice() {
    if (!hasnextservice())
    	throw new nosuchelementexception();
    string cn = nextname;
    nextname = null;
    class<?> c = null;
    try {
    	c = class.forname(cn, false, loader);
    } catch (classnotfoundexception x) {
        fail(service, "provider " + cn + " not found");
    }
    if (!service.isassignablefrom(c)) {
        fail(service, "provider " + cn + " not a subtype");
    }
    try {
        s p = service.cast(c.newinstance());
        providers.put(cn, p);
        return p;
    } catch (throwable x) {
        fail(service, "provider " + cn + " could not be instantiated", x);
    }
    throw new error(); // this cannot happen
}

2.5 自定义类加载器

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

  • 1)想加载非 classpath 随意路径中的类文件
  • 2)都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  • 继承 classloader 父类
  • 要遵从双亲委派机制,重写 findclass 方法 注意不是重写 loadclass 方法,否则不会走双亲委派机制
  • 读取类文件的字节码
  • 调用父类的 defineclass 方法来加载类
  • 使用者调用该类加载器的 loadclass 方法

3.总结

到此这篇关于jvm入门之类加载与字节码技术(类加载与类的加载器)的文章就介绍到这了,更多相关jvm 类加载与字节码技术内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!