一 , 什么是类加载机制
虚拟机把经过javac编译后的.class字节码文件加载进内存,并对数据进行链接–>初始化,最终形成能被虚拟机直接使用的二进制机器码。这就是JVM的类加载机制。
二 , 类加载全过程
当一个类被加载到虚拟机内存中,到卸载出内存位置,这个类的生命周期包括:加载,验证,准备,解析,初始化,使用,卸载 这7个阶段。其中:验证,准备,解析这三个部分归为链接。
下面通过一个简单的Java代码解析内存中发生的变化:
public class Demo01 {
public static void main(String[] args) {
A a = new A();
System.out.println(A.width);
}
}
class A{
public static int width = 100;
static {
System.out.println("静态块类A");
}
public A(){
System.out.println("类A构造器");
}
}
①加载:
其本质是将.class字节码加载到内存,其字节码的来源可以是各种形式,例如:本地的字节码,通过网络获取的字节码(例如通过leetCode在线编译获取字节码),jar包等。
②链接:
- 验证:确保加载的类是否符合JVM规范,没安全隐患。
- 准备:正式为类变量(static变量)分配内存并设置变量初始值(这里注意是初始默认值,静态变量赋值并不是在这里),这些内存都将在方法区中分配。
- 解析:JVM常量池的符号引用替换为直接引用。
③初始化:
- 执行类构造器()方法。类构造器方法由编译器自动收集类中所有静态变量的赋值动作和静态语句块中的语句合并产生的。
- 当初始化一个类的时候,如果父类没有进行初始化,则会先初始化其父类。
- 虚拟机会保证一个类的()方法在多线程环境下被正确加锁和同步
- 当访问一个Java类的静态域时,只有真正声明这个域的类才会被初始化。
④执行:…
⑤销毁:…
(对应以上过程)
①JVM将Demo01的.class字节码加载到内存后,
②再将Demo01的二进制静态字节码数据转换为方法区中的运行时数据结构,同时在堆中生成一个代表Demo01的java.lang.Class对象,这个Class对象能够获取方法区中Demo01的运行时数据。我们之所以能通过反射中的Class对象访问一个类的结构并进行操作的原因,都在这个过程体现了。
再以同样的过程加载类A的运行时数据和Class对象…
③成功加载 链接后,在初始化阶段:
执行由编译器自动收集的包含类A的静态语句块和静态变量赋值动作的clint类构造器方法,类A方法区中的运行时数据静态变量被赋予值,这时控制台会输出:静态块类A
④使用:
执行Demo01的main方法:
首先会在栈中开辟一个main方法栈帧,如果main中调用了其他方法,则会在其之上开辟一个新的栈帧,这也是为什么栈是先进后出的数据结构。
其次main栈帧中创建一个类A的引用,堆内存中创建一个A的对象:new A() 调用了A对象的无参构造方法,控制台打印类A构造器
,再将引用指向A对象的地址。
最后调用类A的静态属性的width并打印到控制台,这里的静态属性是从方法区中获取的。
⑤卸载
当代表类的Class对象不在被引用时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载,从而结束类的生命周期。由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
三 ,类的主动引用(也就是一定会发生类的初始化的情况):
1. new一个类的对象
public class Test {
public static void main(String[] args) {
A a = new A();//控制台打印 静态块类A 类A构造器
}
}
class A {
public static int width = 100;
static {
System.out.println("静态块类A");
}
public A(){
System.out.println("类A构造器");
}
}
2. 调用类的静态成员和静态方法(除了静态常量):
public class Test {
public static void main(String[] args) {
System.out.println(A.width);//控制台打印 静态块类A, 100
// System.out.println(A.MAX);//控制台只打印 200
}
}
class A {
public static int width = 100;
public static final int MAX = 200;
static {
System.out.println("静态块类A");
}
public A(){
System.out.println("类A构造器");
}
}
3. 当使用java.lang.reflect包的方法对类进行反射调用
public class Test {
public static void main(String[] args) throws Exception {
Class.forName("com.A");//控制台打印 静态块A
}
}
class A {
static {
System.out.println("静态块类A");
}
public A(){
System.out.println("类A构造器");
}
}
4. 调用main方法时,其所在的类一定为主动引用,会被初始化
public class Test {
static {
System.out.println("静态块Test");
}
public static void main(String[] args) {
//直接执行空的main方法 控制台打印 静态块Test
}
}
5. 当初始化一个类,如果父类没有被初始化,则会先初始化其父类
public class Test {
public static void main(String[] args){
System.out.println(A.width);//调用A的静态变量 主动引用 先初始化其父类
//控制台打印A_father静态块 静态块A 100
}
}
class A extends A_father{
public static int width = 100;
static {
System.out.println("静态块类A");
}
public A(){
System.out.println("类A构造器");
}
}
class A_father extends Object{
static {
System.out.println("静态块类A_father");
}
}
四 ,类的被动引用(一定不会发生类的初始化)
1. 当访问一个静态域时,只有真正声明这个域的类才会被初始化
public class Test {
public static void main(String[] args){
System.out.println(A_child.width);//A的子类继承了A的静态属性 但并没有再次定义,可以理解为我们操作的依旧是类A的静态域,得初始化类A
// 控制台打印 静态块类A 100
}
}
class A {
public static int width = 100;
static {
System.out.println("静态块类A");
}
public A(){
System.out.println("类A构造器");
}
}
class A_child extends A{
static {
System.out.println("静态块A_child");
}
}
2. 通过数组定义类引用,不会触发此类的初始化
不解释了…
3. 引用常量不会初始化
也不解释了,之前提到过,常量在编译阶段就存入调用类的常量池了。
此时又挖了一个JVM垃圾回收机制的新坑。。。。。
本人技术水平有限,若文中存在错误,欢迎指明误区开喷。