一、字节码与运行时程序的关系:
写好的代码经过编译变成了字节码,并且可以打包成Jar文件。 然后就可以让JVM去加载需要的字节码,变成持久代/元数据区上的Class对象,接着才会执行我们的程序逻辑。
二、类的生命周期和加载过程
一个类的生命周期包括以下几个方面:
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
在这些过程中,1-5步骤被称作类加载。
三、类加载时机
提到类加载时机,有个细节需要记录的是:类会在哪些情况下被初始化?这里记录下一些情况。
初始化何时会被触发:
- 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main方法所在的类;
- 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类,就是new一个类的时候要初始化。
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类
- 子类的初始化会触发父类的初始化
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射API对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化
- 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
同时以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取Class对象,不会触发类的初始化,Hello.class不会让Hello类初始化
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName(“jvm.Hello”)默认会加载Hello类。
- 通过ClassLoader默认的loadClass方法,也不会触发初始化动作(加载了,但是不初始化)
四、类加机制
类加载器有以下几个:
- 启动类加载器(BootstrapClassLoader)
- 扩展类加载器(ExtClassLoader)
- 应用类加载器(AppClassLoader)
其中扩展类加载器(ExtClassLoader)和 应用类加载器(AppClassLoader)都是继承RLClassLoader类。
这里有一个小细节就是,String类是由哪个加载器加载的。答案是:java.lang.String是由启动类加载器加载的,所以String.class.getClassLoader()就会返回null。
类加载机制有三个特点:
- 双亲委托:当一个自定义类加载器需要加载一个类,比如java.lang.String,它很懒,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载器如果发现自己还有父加载器,会一直往前找,这样只要上级加载器,比如启动类加载器已经加载了某个类比如java.lang.String,所有的子加载器都不需要自己加载了。如果几个类加载器都没有加载到指定名称的类,那么会抛出ClassNotFountException异常。
- 负责依赖:如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。
- 缓存加载:为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。
五、总结:
- 介绍了类加载的机制,以及类的生命周期。
- 接着我们介绍了,在那种情况下会初始化类,哪些情况下不会。
- 介绍了类加载器,这部分是老生常谈的东西。不过有一个小细节需要留心就是:String是谁加载的。
- 介绍了类加载特点。