参考:
JVM类加载机制:https://www.jianshu.com/p/a9d8c1a37b8c
JVM类加载机制及类加载器:https://www.jianshu.com/p/f997fa5d1ce9
浅谈双亲委派模型:https://www.jianshu.com/p/353c26c744df
一.JVM类加载机制
1.类加载的定义
- 把类型数据从Class文件加载到内存,并对数据进行校验、解析和初始化,最终形成JVM使用的Java类型
- Java中类型的加载、连接和初始化过程在程序运行期间完成,这将会令类加载时稍微增加一些性能开销,但也为Java提供了高度灵活性,实现了Java的动态扩展特性
2.类的生命周期
- 加载 - 连接(验证-准备-解析) - 初始化 - 使用 - 卸载
- 加载/验证/准备/初始化的顺序固定,但解析可能在初始化后开始,以支持Java的运行时绑定
- 此处的加载仅是类加载的一个步骤,不是类加载
3.类加载的条件
- 当且仅当类的5种主动引用场景,JVM触发类加载
1)创建:使用new关键字实例化对象,和读取(getstatic)或设置(putstatic)一个类的静态字段及调用(invokestatic)一个类的静态方法时
2)反射:使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行过初始化,会先触发类的初始化
3)继承:初始化一个类时,若该类的父类还未初始化,则触发父类的初始化
4)执行主类:JVM启动时,先初始化用户指定的执行主类(包含mian()方法的类)
5)动态语言(jdk1.7):如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 - 以下3种被动引用场景,不会触发JVM类加载
1)子类引用父类的静态字段,不会导致子类初始化
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 NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
//输出
SuperClass init!
123
2)通过数组定义引用类,不会触发此类的初始化
public class SuperClass {
static{
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] scs = new SuperClass[10];
}
}
//不会执行SuperClass类中static块里的System.out.println("SuperClass init!");
输出结果为空
3)常量在编译阶段会存入调用类的常量池,不会触发定义该常量的类的初始化
public class ConstClass {
static{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
hello world
上述代码在编译阶段经常量传播优化,已将ConstClass类的HELLOWORLD常量值存入NotInitialization类的常量池。以后NotInitialization对于常量ConstClass.HELLOWORLD的引用都被转化为NotInitialization类对自身常量池的引用了。实际上NotInitialization的Class文件之中已经不存在ConstClass类的符号引用入口了。
4.类加载过程
- 加载
1)通过类的全限定名获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转换为方法区内的运行时数据结构
3)在内存中生成一个代表此类的java.lang.Class对象,作为该类在方法区的各种数据的访问入口(HotSpot虚拟机特殊,将Class对象存于方法区) - 验证
1)文件格式验证:字节流是否符合Class文件的格式规范,是否能被当前版本的虚拟机处理
2)元数据验证:对字节码的描述信息进行语义分析,保证其描述信息符合Java语言规范。如这个类是否有父类?这个类的父类是否继承了不允许继承的final类?如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法等
3)字节码验证:通过分析数据流和控制流,确保程序语义合法且符合逻辑
4)符号引用验证:发生在解析阶段 - 准备
1)为且仅为类变量在方法区分配内存并设置初始值
2)通常情况类变量初始值为零值,若类变量的字段属性表存在ConstantValue属性,则将被初始化为ConstantValue属性指定的值 - 解析
1)JVM将常量池内的符号引用替换为直接引用的过程
2)符号引用以一组符号描述所引用的目标,直接引用是直接指向目标的指针(或间接定位到目标的句柄) - 初始化
1)对类变量进行赋值及执行静态代码块
2)本质是执行类构造器clinit方法,该方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而来
3)继承中,父类的clinit方法先于子类的clinit方法执行,意味着父类的静态变量赋值和静态语句块先于子类执行。如下例输出结果是2而不是1
public class Parent {
public static int A = 1;
static{
A = 2;
}
}
public class Sub extends Parent{
public static int B = A;
}
public class Test {
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
二.类加载器
- 分为两大类(启动类和其他类),三小类(其他类加载又分为扩展类加载器和应用程序类加载器)
1.启动类加载器(Bootstrap ClassLoader)
- 由C++语言实现(HotSpot虚拟机)
- 加载Java核心类到内存。包括<JAVA_HOME>\lib目录或-Xbootclasspath指定的路径中的类库
2.其他类加载器
- 由Java语言实现,继承自抽象类ClassLoader
- 分为扩展类加载器(Extension ClassLoader)和应用程序加载器(Application ClassLoader)
1)扩展类加载器负责加载Java扩展的核心类之外的类,包括<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库
2)应用程序类加载器负责加载用户类路径(classpath)上指定的类库,是默认的类加载器
3.双亲委派模型
- 双亲委派要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器
- 双亲委派使任意类都通过加载它的类加载器和该类本身一同确立其在虚拟机的唯一性
- 同时,类随它的类加载器一起具备了一种带有优先级的层次关系
- 双亲委派工作过程
1)类加载器收到类的加载请求时,首先会把请求为派给父类加载器,而非自己去加载
2)每个层级的类加载器都是这样,所有的加载请求最终都将传送到顶层的启动类加载器
3)仅当父加载器反馈自己无法完成该加载请求时,子加载器才会尝试自行加载
三.常见面试题
1.可不可以自己写一个String类?
- 不可以,根据双亲委派机制,会去加载父类加载器,父类发现存在冲突的String类时,将停止加载
2.能否在加载类时,对类的字节码进行修改?
- 可以,使用Java探针技术
常恐秋风早,飘零君不知