目录
java类加载机制原理 https://blog.csdn.net/xushiyu1996818/article/details/102826295
注意:本文参考了 https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html
例子1 单类 类初始和对象初始
public class Book {
public static void main(String[] args)
{
System.out.println("Hello ShuYi.");
}
Book()
{
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);
}
{
System.out.println("书的普通代码块");
}
int price = 110;
static
{
System.out.println("书的静态代码块");
}
static int amount = 112;
}
思考一下上面这段代码输出什么?
给你5分钟思考,5分钟后交卷,哈哈。
怎么样,想好了吗,公布答案了。
书的静态代码块
Hello ShuYi.
怎么样,你答对了吗?是不是和你想得有点不一样呢。
下面我们来简单分析一下,首先根据触发初始化的5种情况的第4种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类),我们会进行类的初始化。
那么类的初始化顺序到底是怎么样的呢?
重点来了!
重点来了!
重点来了!
在我们代码中,我们只知道有一个构造方法,但实际上Java代码编译成字节码之后,是没有构造方法的概念的,只有类初始化方法 和 对象初始化方法 。
那么这两个方法是怎么来的呢?
类初始化方法。编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。
上面的这个例子,其类初始化方法就是下面这段代码了:
static
{
System.out.println("书的静态代码块");
}
static int amount = 112;
对象初始化方法。编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。
对象初始化方法一般在实例化类对象的时候执行。
上面这个例子,其对象初始化方法就是下面这段代码了:
{
System.out.println("书的普通代码块");
}
int price = 110;
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);
类初始化方法 和 对象初始化方法 之后,我们再来看这个例子,我们就不难得出上面的答案了。
但细心的朋友一定会发现,其实上面的这个例子其实没有执行对象初始化方法。
因为我们确实没有进行 Book 类对象的实例化。如果你在 main 方法中增加 new Book() 语句,你会发现对象的初始化方法执行了!
感兴趣的朋友可以自己动手试一下,我这里就不执行了。
通过了上面的理论和简单例子,我们下面进入更加复杂的实战分析吧!
例子2 祖孙 3类 父类成员变量
class Grandpa
{
static
{
System.out.println("爷爷在静态代码块");
}
}
class Father extends Grandpa
{
static
{
System.out.println("爸爸在静态代码块");
}
public static int factor = 25;
public Father()
{
System.out.println("我是爸爸~");
}
}
class Son extends Father
{
static
{
System.out.println("儿子在静态代码块");
}
public Son()
{
System.out.println("我是儿子~");
}
}
public class InitializationDemo
{
public static void main(String[] args)
{
System.out.println("爸爸的岁数:" + Son.factor); //入口
}
}
思考一下,上面的代码最后的输出结果是什么?
最终的输出结果是:
爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25
也许会有人问为什么没有输出「儿子在静态代码块」这个字符串?
这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)。因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
对面上面的这个例子,我们可以从入口开始分析一路分析下去:
首先程序到 main 方法这里,使用标准化输出 Son 类中的 factor 类成员变量,但是 Son 类中并没有定义这个类成员变量。于是往父类去找,我们在 Father 类中找到了对应的类成员变量,于是触发了 Father 的初始化。
但根据我们上面说到的初始化的 5 种情况中的第 3 种(当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)。我们需要先初始化 Father 类的父类,也就是先初始化 Grandpa 类再初始化 Father 类。于是我们先初始化 Grandpa 类输出:「爷爷在静态代码块」,再初始化 Father 类输出:「爸爸在静态代码块」。
最后,所有父类都初始化完成之后,Son 类才能调用父类的静态变量,从而输出:「爸爸的岁数:25」。
例子3 祖孙 3类 实例化孙子
class Grandpa
{
static
{
System.out.println("爷爷在静态代码块");
}
public Grandpa() {
System.out.println("我是爷爷~");
}
}
class Father extends Grandpa
{
static
{
System.out.println("爸爸在静态代码块");
}
public Father()
{
System.out.println("我是爸爸~");
}
}
class Son extends Father
{
static
{
System.out.println("儿子在静态代码块");
}
public Son()
{
System.out.println("我是儿子~");
}
}
public class InitializationDemo
{
public static void main(String[] args)
{
new Son(); //入口
}
}
输出结果是:
爷爷在静态代码块
爸爸在静态代码块
儿子在静态代码块
我是爷爷~
我是爸爸~
我是儿子~
怎么样,是不是觉得这道题和上面的有所不同呢。
让我们仔细来分析一下上面代码的执行流程:
首先在入口这里我们实例化一个 Son 对象,因此会触发 Son 类的初始化,而 Son 类的初始化又会带动 Father 、Grandpa 类的初始化,从而执行对应类中的静态代码块。因此会输出:「爷爷在静态代码块」、「爸爸在静态代码块」、「儿子在静态代码块」。
当 Son 类完成初始化之后,便会调用 Son 类的构造方法,而 Son 类构造方法的调用同样会带动 Father、Grandpa 类构造方法的调用,最后会输出:「我是爷爷~」、「我是爸爸~」、「我是儿子~」。
例子4 单类 复杂分析
public class Book {
public static void main(String[] args)
{
staticFunction();
}
static Book book = new Book();
static
{
System.out.println("书的静态代码块");
}
{
System.out.println("书的普通代码块");
}
Book()
{
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);
}
public static void staticFunction(){
System.out.println("书的静态方法");
}
int price = 110;
static int amount = 112;
}
上面这个例子的输出结果是:
书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法
下面我们一步步来分析一下代码的整个执行流程。
在上面两个例子中,因为 main 方法所在类并没有多余的代码,我们都直接忽略了 main 方法所在类的初始化。
但在这个例子中,main 方法所在类有许多代码,我们就并不能直接忽略了。
当 JVM 在准备阶段的时候,便会为类变量分配内存和进行初始化。此时,我们的 book 实例变量被初始化为 null,amount 变量被初始化为 0。
当进入初始化阶段后,因为 Book 方法是程序的入口,根据我们上面说到的类初始化的五种情况的第四种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类)。所以JVM 会初始化 Book 类,即执行类构造器 。
JVM 对 Book 类进行初始化首先是执行类构造器(按顺序收集类中所有静态代码块和类变量赋值语句就组成了类构造器 ),后执行对象的构造器(按顺序收集成员变量赋值和普通代码块,最后收集对象构造器,最终组成对象构造器 )。
对于 Book 类,其类构造方法()可以简单表示如下:
static Book book = new Book();
static
{
System.out.println("书的静态代码块");
}
static int amount = 112;
于是首先执行static Book book = new Book();
这一条语句,这条语句又触发了类的实例化。于是 JVM 执行对象构造器 ,收集后的对象构造器 代码:
{
System.out.println("书的普通代码块");
}
int price = 110;
Book()
{
System.out.println("书的构造方法");
System.out.println("price=" + price +", amount=" + amount);
}
于是此时 price 赋予 110 的值,输出:「书的普通代码块」、「书的构造方法」。而此时 price 为 110 的值,而 amount 的赋值语句并未执行,所以只有在准备阶段赋予的零值,所以之后输出「price=110,amount=0」。
当类实例化完成之后,JVM 继续进行类构造器的初始化:
static Book book = new Book(); //完成类实例化
static
{
System.out.println("书的静态代码块");
}
static int amount = 112;
即输出:「书的静态代码块」,之后对 amount 赋予 112 的值。
到这里,类的初始化已经完成,JVM 执行 main 方法的内容。
方法论
从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:
确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。
如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。