JVM 篇:类加载机制

概述

  Java 和 C/C++ 这种在编译时就开始连接的语言不同,Java 是在运行期动态加载,动态连接的。(以效率换灵活度)
  类的生命周期如下,其中 加载验证准备初始化卸载 这五步的执行顺序是确定的 (这里说的是开始的顺序,而不是等到前一个执行完再执行下一个,比如加载阶段还没结束时验证就开始了)解析 一般来说是在 初始化 之前开始的,但有些情况会在 初始化 之后才开始 (为了支持 Java 的运行时绑定)

在这里插入图片描述


加载
  1. 通过类的全限定名获取该类的二进制字节流(可以通过代码控制,ClassLoader
  2. 将字节流表示的静态存储结构转化为方法区的运行时数据结构
  3. 在方法区为加载的类生成 Class 对象
验证

可以使用 javap -v Xxx.class 来读 Class 文件的字节码

  1. 文件格式验证(对载入的字节流验证)
    1. 是否以魔数 0xCAFEBABE 开头
    2. 主,次版本是否在当前虚拟机处理范围之内
  2. 元数据验证
    1. 是否继承了不该继承的类(final)
    2. 如果继承了抽象类或接口,那么是否为方法提供实现
    3. 是否有不合理的重载(只有返回类型不同)
  3. 字节码验证(对方法体进行验证)
    1. 保证方法体中的类型转换是有效的
    2. 操作数栈的数据结构和指令序列代码能配合工作:操作数栈存着一个 int,却按 long 读入了本地变量表
  4. 符号引用验证(这个动作发生在解析阶段 — 将符号引用转换为直接引用)
    1. 符号引用通过字符串描述的全限定符是否能找到对应的类
    2. 符号引用中的类,字段和方法的访问性是否对当前类开放
准备

类变量 分配内存并设置 零值,为 常量 分配内存并 初始化

class Instance {
	static int i = 100;			// 准备阶段 i = 0,初始化阶段 i = 100
	static final int j = 200;	// static final 修饰的变量会被当成常量,准备阶段 j = 200
	......
}

解析

  解析阶段是将符号引用转换为直接引用的过程,虚拟机可以根据需要决定是在类加载的时候转换或是等到被使用的时候再转换。解析也被称为静态解析,特点为编译时可知,运行时不可变。只能对那些父类方法,实例构造器,final 方法,静态方法或私有方法这种不会变的方法可用,而对其他那些重写重载等的方法(虚方法)需要在运行时确定,这部分的符号引用在栈帧中的动态连接部分完成转换。
  在会触发解析的指令中,除了 invokedynamic 指令每次都会重新解析外,其他指令都会将转换后的直接引用缓存起来。(因为 invokedynamic 是用于动态语言的,如 Javascript,python 等,所以每次使用都要重新解析)
  符号引用的字节码格式为 CONSTANT_Xxx_info (如 CONSTANT_Class_info,CONSTANT_Fieldref_info 等)

初始化

  初始化阶段就是执行类的 <clinit> 方法,该方法由虚拟机生成,内容为类变量的赋值和 static{}。

  1. 执行子类的 <clinit> 方法之前会先完成父类的初始化
  2. 在多线程环境下,虚拟机会保证 <clinit> 方法会被同步执行

Example
class A {
    static int i = 100;
    static {
        System.out.println("A init");
    }
}

class B extends A {
    static int j = 300;
    static final int constant = 666;
    static {
        System.out.println("B init");
    }

	// 如果执行了这个方法会触发 B 的类初始化,进而触发 A 的初始化
	public static void main(String[] args) {
	}
}

class Main {

	@Test
	public void initA() {
		// 只使用到 A 的类变量,所以只会触发 A 的初始化
		// 结果:A init
		int i = B.i; 
	}

	@Test
	public void initAB() {
		// 使用到 B 的类变量,需要初始化 B
		// B 是 A 的子类,初始化 B 之前先初始化 A
		// 结果:A init \n B init
		int j = B.j;
	}

	@Test
	public void nothing() {
		// 使用的是 B 的常量,而常量放在运行时常量池中
		// 读取常量不需要经过类,所以 B 不会被初始化
		int constant = B.constant;
	}
	
	@Test 
	public void nothing2() {
		// 初始化的是 [LB(虚拟机中 B 的数组表达法),而不是 B
		// 所以 B 不会被初始化
		B[] array = new B[1];
	}
}

类加载器

  虚拟机在加载阶段中的 “ 通过一个类的全限定名来获取描述此类的二进制字节流 ” 这个动作放到虚拟机外部去实现,即通过外部 Java 代码来实现,这个实现被称作类加载器,在 Java 中只要继承 ClassLoader 就可以自定义类加载器。

  • 同一个类如果是不同的类加载器加载的,是互不相等的。
public class Hello {
	public void say() {
		System.out.println("hello world");
	}
}
// 自定义类加载器
public class CustomClassLoader extends ClassLoader{
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream in = getClass().getResourceAsStream(filename);
            if (in == null) return super.loadClass(name);
            byte[] bytes = new byte[in.available()];
            in.read(bytes);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new RuntimeException("Can not find the Class file !");
        }
    }

    public static void main(String[] args) throws Exception{
		// 结果:false,因为系统类加载器和自定义加载器中的 Hello 不一样
        System.out.println(new CustomClassLoader().loadClass("Hello").newInstance() instanceof Hello);
    }
}
  • 虚拟机默认存在三个类加载器:
    • 启动类加载器(Bootstrap ClassLoader):由 C++ 实现,是虚拟机的一部分,负责加载 $JAVA_HOME/lib 目录下的所有指定 jar 包(如 rt.jar)。是最顶级的类加载器。
    • 扩展类加载器(Extension ClassLoader):负责加载 $Java_HOME/lib/ext 目录下的类库。
    • 应用程序类加载器(Application ClassLoader),因为可以通过 ClassLoader::getSystemClassLoader 获取,也被称为系统类加载器。
  • 类加载器定义了 双亲委派模型,即加载一个类的时候会优先使用上一级的类加载器来加载,加载失败的话再转为当前类加载器来加载。
    • 如加载 Object 的时候 Application ClassLoader 会传递给 Extension ClassLoader 来加载,而 Extension ClassLoader 会传递给 Bootstrap ClassLoader 来加载,这样就保证了不管在哪一个类加载器中加载了 Class<Object> 加载的都是同一个。
    • 这部分逻辑定义在 ClassLoader::loadClass 中,所以实现自定义的类加载器的时候一般不建议重写该方法,应该重写 ClassLoader::findClass
  • 双亲委派模型 本身也是存在缺陷的
    • 现在的趋势流行模块化和热部署,OSGi 是公认的 Java 模块化标准,但在 OSGi 中,类加载器不再是 双亲委派模型 那样的树状图形,而是复杂的网状模型。
    • 如果基础类如 Object 要调用用户定义的接口时,就会出现类加载器不同而无法调用,为此 JDK 增加了 Thread::setContextClassLoader 来改变类加载器。
public class DifferentHello implements Runnable {
    Object instance;

    @Override
    public void run() {
        try {
        	// 不能强制转换为 Hello 类型,因为系统类加载器与自定义的类加载器的 Hello 类不一样
        	// 所以 instance 只能是 Object 类型
            instance = Thread.currentThread().getContextClassLoader().loadClass("Hello").newInstance();
        } catch (Exception e) {
            throw new RuntimeException("Can not init DifferentHello.instance !");
        }
    }

    public static void main(String[] args) throws Exception {
    	// 如果基础类要使用用户定义的接口,可以使用类似的逻辑使用
        DifferentHello differentHello = new DifferentHello();
        Thread thread = new Thread(differentHello);
        thread.setContextClassLoader(new CustomClassLoader());
        thread.start();
        Thread.sleep(1000);
        
        // 只能使用反射来访问要使用的方法
        if (differentHello.instance != null)
        	differentHello.instance.getClass().getMethod("say").invoke(differentHello.instance);
    }
}


  以上内容为阅读 深入理解Java虚拟机(第2版)后的笔记及对 JDK8 的实践补充。看完这本书后最大的感觉就是,,,再看一遍,很多原来理解不了的知识点就可以看懂了,因为很多内容是前后呼应的。有兴趣的可以去阅读这本书,强推。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值