类加载过程
-
加载:获取类的二进制字节流,将字节流中代表静态存储结构转换成方法区的运行时数据结构,然后在内存中生成一个代表这个类java.lang.CIass对象
-
连接:将Java类的二进制代码合并到JVM的运行状态之中的过程。
- 验证:确保加载的类信息符合JVM规范,没有安全方面的问题
- 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
-
初始化:执行类构造器
<clinit>()
方法的过程。类构造器<clinit>()
方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
类加载的时机
java虚拟机规范没有强行约束什么时候进行加载,但规定了以下情况发生类初始化。
主动引用的时候如果类没有初始化则会初始化。有且只有以下几种情况。当然,最后一条没有说清楚,有兴趣的读者可以去研究一下。
-
当虚拟机启动,先初始化main方法所在的类
-
new一个类的对象
-
调用类的静态成员(除了final常量)和静态方法
-
对类进行反射调用
-
当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
-
jdk1.7动态语言有可能导致类的加载
除上面几种情况,其他引用类的时候都不会初始化(称作被动引用)。如:
- 通过子类引用父类的静态变量,不会导致子类初始化(父类会初始化)
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(常量在涟接阶段就存入调用类的常量池中了)
这些概念在周志明的《深入理解JVM虚拟机》中有详细解释。
类加载和实例化
我以前一直搞混两个概念:类加载和实例化。看下面代码,先说明一点,静态代码块的执行在初始化阶段,后文泛化到类加载阶段。注意和加载阶段的区别。
public class Percent {
public Percent(){
System.out.println("percent 构造方法");
}
{
System.out.println("percent 构造代码块");
}
static{
System.out.println("percent 静态代码块");
}
}
public class Child extends Percent {
public Child(){
System.out.println("child 构造方法");
}
{
System.out.println("child 构造代码块");
}
static{
System.out.println("child 静态代码块");
}
}
public static void main(String[] args) {
/**
* percent 静态代码块
* child 静态代码块
* percent 构造代码块
* percent 构造方法
* child 构造代码块
* child 构造方法
*/
Child child=new Child();
}
误区:我一直以为类加载过程中静态资源和非静态资源是一块加载的,所以我一直以为输出顺序如下。
加载一个类的时候如果父类没有加载则会先加载父类。实际上这个概念是对的,只是我搞混了类加载和实例化的概念。
public static void main(String[] args) {
/**
* percent 静态代码块
* percent 构造代码块
* percent 构造方法
* child 静态代码块
* child 构造代码块
* child 构造方法
*/
Child child=new Child();
}
泛泛的讲,类加载操作静态资源。实例化操作非静态资源。类加载过后才可以被实例化。
如果还是不理解可以看下面代码此处也执行了类的加载,却没有输出构造器的内容,因为没有进行实例化。
public static void main(String[] args) {
/**
* percent 静态代码块
* child 静态代码块
* child 静态变量
*/
System.out.println(Child.childStr);
}
执行顺序如图所示:
父类类加载 > 子类类加载 > 父类实例化 > 子类实例化
在实例化过程中,构造器后执行。
顺便提一下静态代码块可以给在之后的静态变量赋值,但是不能使用,idea会直接指出。
public class Main {
static {
m=2;
//System.out.println(m); 直接报错
}
//public static int m;静态变量即使没有赋值也会有默认初值0
public static int m=1;
public static void main(String[] args) {
System.out.println(m);//2
}
}
来个有意思的代码
public class Percent {
public int a=1;
public Percent(){
super();
fun();
}
public void fun(){
System.out.println("Percent fun");
}
}
public class Child extends Percent {
public int a=2;
public Child(){
super();
}
public void fun(){
System.out.println("Child fun");
System.out.println(a);
}
public static void main(String[] args) {
/**
* Child fun
* 0
*/
Child child=new Child();
}
}
按照上图给的流程,我们走一遍。
- 类加载完后初始化父类非静态资源
- 执行父类构造函数
- 由于多态调用子类fun()方法
- 输出“Child fun”
- 输出a的值,此时子类a仅仅有默认值0,还未赋值2,所以输出0
我疑惑此时为什么会找到子类的fun()方法,打断点调试一下,为此特意把构造器的super();语句加上方便调试,即使不加虚拟机也会执行的。
断点的执行如下:
- 14行–此时子类的a赋完初值0,子类fun()也已经加载。
- 4行–此时父类的a赋完初值0,父类fun()也已经加载。
- 2行–给a赋1
- 5行
- 17行
到这里大家应该看懂了吧。