JVM类加载过程
当 Java 虚拟机将 Java 源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析、运行等整个过程,这个过程我们叫:Java 虚拟机的类加载机制。JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。
加载
加载阶段用一句话来说就是:把代码数据加载到内存中。这个过程对于我们解答这道问题没有直接的关系,但这是类加载机制的一个过程,所以必须要提一下。
验证
只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。
- JVM规范校验。
- 代码逻辑校验。确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。
准备(重点)
准备的过程主要是做两件事:内存分配的对象以及初始化的类型。
- 内存分配的对象。 Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。
public static int factor = 3;
public String website = "www.cnblogs.com/chanshuyi";
- 初始化的类型。 在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。
public static int sector = 3;
但是如果是final修饰的类变量则会直接被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。
public static final int number = 3;
解析
当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。
其实这个阶段对于我们来说也是几乎透明的,了解一下就好。
初始化(重点)
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
使用
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。
卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个阶段也只是了解一下就可以。
实际操作
题目一
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
解析:
- 初始化主方法所在的类(InitializationDemo),但是没有类变量。
- 在执行到Son.factor时候,Son中没有factor变量,就会找到factor变量(Father类中),并从这个类(Father)的最父类初始化加载类变量。
注意:如果Son类中有factor就会先初始化Son类,即也会打印“儿子在静态代码块”。如果只有Grandpa类中有factor,则不会打印“爸爸在静态代码块”。
构造方法是在创建对象时被调用的。
题目二
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;
}
打印结果:
书的静态代码块
Hello ShuYi.
执行过程:
- 初始化主方法所在的类(Book)
- 加载类类变量
注意:
Java代码编译成字节码之后,是没有构造方法的概念的,只有类初始化方法 和 对象初始化方法 。
- 类初始化方法。编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。
上面的这个例子,其类初始化方法就是下面这段代码了:
static
{
System.out.println("书的静态代码块");
}
static int amount = 112;
- 对象初始化方法。编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。
类初始化时只加载一次,对象每次初始化( new())时都会加载一次。
上面这个例子,其对象初始化方法就是下面这段代码了:
{
System.out.println("书的普通代码块");
}
int price = 110;
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);
题目三
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 类构造方法的调用,最后会输出:「我是爷爷」、「我是爸爸」、「我是儿子~」。
注意:类的初始化会先初始化父类,new 类时,也会先调用父类的构造方法。
题目四
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
书的静态代码块
书的静态方法
执行过程:
- 初始化主方法所在的类(Book),依次执行类变量。
- 执行到静态new Book()时,初始化对象时会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码。
- 此时静态变量amount只是分配内存,给了默认值还没有赋值。
- 再继续依次加载类变量。
注意:如果将 static int amount = 112 移动到 位置一 就会打印 price=11,amount=112
题目五
package com.erp.payroll.test.treeDemo;
/**
* @author guang
* @version V1.0
* @Package com.erp.payroll.test.treeDemo
* @date 2022/3/1 13:49
*/
class Grandpa
{
static
{
System.out.println("爷爷在静态代码块");
}
public static int factor = 25;
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)
{
System.out.println("年龄="+Son.factor);
System.out.println("===");
Father son = new Son();//入口
}
}
执行结果:
爷爷在静态代码块
年龄=25
===
爸爸在静态代码块
儿子在静态代码块
我是爷爷~
我是爸爸~
我是儿子~
注意:类变量只加载一次
原文:JVM类加载