一、类的加载过程
1、类加载三步曲
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM 将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。
2、类的加载
系统可能在第一次使用某个类时加载该类,但也可能采用预先加载机制来预加载某个类,不管怎样,类的加载必须由类加载器完成,类加载器通常由 JVM 提供,由 JVM 提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承 ClassLoader 基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:
(1)从本地系统直接读取 .class 文件,这是绝大部分类的加载方法;
(2)从 zip,jar 等归档文件中加载 .class 文件,这种方式也很常见;
(3)通过网络下载 .class 文件或数据;
(4)从专有数据库中提取 .class 数据;
(5)将 Java 源文件数据上传到服务器中动态编译为 .class 数据,并执行加载。
但是,不管类的字节码内存从哪里加载,加载的结果都一样,这些字节码内容加载到内存后,都会将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口(即引用地址),所有需要访问和使用类数据只能通过这个 Class 对象。这个加载的过程需要类加载器参与。
小结:类的加载过程就是为了将字节码文件读取到内存中。
3、类的链接
当类被加载之后,系统为之生成一个对应的 Class 对象,接着将会进入连接阶段,连接阶段将会负责把类的二进制数据合并到 JVM 的运行状态之中。类的连接又可以分为如下三个阶段:
(1)验证:确保加载的类信息符合 JVM 规范,例如:以 cafe 开头,没有安全方法的问题;
(2)准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配,给非 final 的赋默认值,如果是 final的,直接赋常量值;
(3)解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
小结:加载和链接后,在方法区中已经有一个能够代表当前类的 Class 对象。
4、类的初始化
(1)执行 类构造器<clinit>() 方法的过程。类构造器<clinit>() 方法是由编译期自动收集类中所有类变量的显示赋值动作和静态代码块中的语句合并产生的。(①静态变量的显式赋值;②静态代码块的内容的组成)
(类构造器是构造类信息的,不是构造该类对象的构造器)
(2)当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发父类的初始化;
(3)虚拟机会保证一个类的<clinit>() 方法在多线程环境中被正确加锁和同步,即每一个类在内存中都只有唯一的一个 Class对象;
类的初始化主要就是对静态的类变量进行初始化
二、什么时候会发生类初始化?
1、类的主动引用(一定会发生类的初始化)
(1)当虚拟机启动, 先初始化main方法所在的类;
(2)new一个类的对象;
(3)调用类的静态成员(除了final常量) 和静态方法;
(4)使用 java.lang.reflect 包的方法对类进行反射调用;
(5)当初始化一个类, 如果其父类没有被初始化, 则先会初始化它的父类;
2、类的被动引用(不会发生类的初始化)
(1)当访问一个静态域时, 只有真正声明这个域的类才会被初始化;
(2)当通过子类引用父类的静态变量, 不会导致子类初始化;
(3)通过数组定义类引用, 不会触发此类的初始化;
(4)引用常量不会触发此类的初始化( 常量在链接阶段就存入调用类的常量池中了);
3、案例
声明两个类:
1 class Father {
2 static int b = 2;
3 static {
4 System.out.println("父类被加载");
5 }
6 }
7
8 class A extends Father {
9 static {
10 System.out.println("子类被加载");
11 m = 300;
12 }
13
14 static int m = 100;
15 static final int M = 1;
16 }
测试:
1 public class ClassLoadingTest {
2 public static void main(String[] args) {
3 // 主动引用:一定会导致A和Father的初始化
4 // A a = new A();
5 // System.out.println(A.m);
6 // Class.forName("com.java2.A");
7
8
9 // 被动引用
10 A[] array = new A[5];//不会导致A和Father的初始化
11 // System.out.println(A.b);//只会初始化Father
12 //System.out.println(A.M); 不会导致A和Father的初始化
13 }
14
15 static {
16 System.out.println("main所在的类");
17 }
18 }
三、类的初始化
1、类的初始化
1 class Base{
2 private static int a = getNum();
3 static{
4 ++a;
5 System.out.println("(2)a = " + a);
6 }
7 static{
8 ++a;
9 System.out.println("(3)a = " + a);
10 }
11 public static int getNum(){
12 System.out.println("(1)a = " + a);
13 return 1;
14 }
15 }
16
17 class TestClinit extends Base{
18 private static int b = getNum();
19 static{
20 ++b;
21 System.out.println("(5)b = " + b);
22 }
23 static{
24 ++b;
25 System.out.println("(6)b = " + b);
26 }
27 public static int getNum(){
28 System.out.println("(4)b = " + b);
29 return 1;
30 }
31 public static void main(String[] args) {
32
33 }
34 }
运行结果:
(1)a = 0
(2)a = 2
(3)a = 3
(4)b = 0
(5)b = 2
(6)b = 3
注意:虽然类的加载大多数时候和类的初始化是一气呵成的,但其实类的加载不一定就会触发类的初始化。
2、类加载时会发生类的初始化情况
Java 程序首次通过下面5中方式来使用某个类时,系统就会初始化该类:
(1)当虚拟机启动,先初始化 main 方法所在的类
Demo:
1 //当虚拟机启动,先初始化main方法所在的类
2 public class A {
3 static{
4 System.out.println("init...A");
5 }
6 public static void main(String[] args) {
7
8 }
9 }
(2)new 一个类的对象
Demo:
1 //new一个类的对象
2 class B{
3 static{
4 System.out.println("init...B");
5 }
6 }
7 public class TestB{
8 public static void main(String[] args) {
9 new B();
10 }
11 }
(3)调用该类的静态变量(final的常量除外)和静态方法
Demo:
1 //调用该类的静态变量(final的常量除外)和静态方法
2 class C{
3 static{
4 System.out.println("init...C");
5 }
6 public static void test(){
7 }
8 }
9 public class TestC {
10 public static void main(String[] args) {
11 C.test();
12 }
13 }
14
15 //调用该类的静态变量(final的常量除外)和静态方法
16 class C{
17 public static int num = 10;
18 static{
19 System.out.println("init...C");
20 }
21 }
22 public class TestC {
23 public static void main(String[] args) {
24 System.out.println(C.num);
25 }
26 }
(4)使用 java.lang.reflect 包的方法对类进行反射调用
Demo:
1 //使用java.lang.reflect包的方法对类进行反射调用
2 class D{
3 static{
4 System.out.println("init...D");
5 }
6 }
7 public class TestD {
8 public static void main(String[] args) throws ClassNotFoundException {
9 ClassLoader cl = ClassLoader.getSystemClassLoader();
10 cl.loadClass("com.ks.loader.D");//该句不会造成类初始化,只是加载类
11 System.out.println("类加载已完成...");
12 Class.forName("com.ks.loader.D");//会导致类初始化
13 }
14 }
(5)当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
Demo:
1 //当初始化一个类,如果其父类没有被初始化,则先会初始化他的父类
2 class EBase{
3 static{
4 System.out.println("父类初始化");
5 }
6 }
7 public class TestE extends EBase {
8 static{
9 System.out.println("子类初始化");
10 }
11 public static void main(String[] args) {
12 }
13 }
(6)类的初始化
1 public class ClassLoadingTest {
2 public static void main(String[] args) {
3 System.out.println(A.m);
4 }
5 }
6 class A {
7 static {
8 m = 300;
9 }
10 static int m = 100;
11 }
第一步:类的加载
第二步:链接结束后 m = 0
第三步:初始化后,m 的值由 <clinit> 方法执行决定
这个 A的类构造器 <clinit>() 方法由类变量的赋值和静态代码块中的语句按照顺序合并产生,类似于:
<clinit>() {
m = 300;
m = 100;
}
3、类加载时不会发生类的初始化
(1)引用静态常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
Demo:
1 //引用静态常量不会触发此类的初始化
2 class NBase{
3 public static final int MAX_VALUE = 100;
4 static{
5 System.out.println("父类初始化");
6 }
7 }
8 class NSub extends NBase{
9 static{
10 System.out.println("子类初始化");
11 }
12 }
13 public class TestNoInitialize {
14
15 public static void main(String[] args) {
16 System.out.println(NSub.MAX_VALUE);
17 System.out.println(NBase.MAX_VALUE);
18 }
19 }
(2)当访问一个静态域时,只有真正声明这个域的类才会被初始化,当通过子类引用父类的静态变量,不会导致子类初始化;
Demo:
1 //当访问一个静态域时,只有真正声明这个域的类才会被初始化
2 // 当通过子类引用父类的静态变量,不会导致子类初始化
3 class NBase{
4 public static int num = 10;
5 static{
6 System.out.println("父类初始化");
7 }
8 }
9 class NSub extends NBase{
10 static{
11 System.out.println("子类初始化");
12 }
13 }
14 public class TestNoInitialize {
15
16 public static void main(String[] args) {
17 System.out.println(NSub.num);
18 }
19 }
(3)通过数组定义类引用,不会触发此类的初始化。
Demo:
1 //通过数组定义类引用,不会触发此类的初始化
2 NSub[] arr = new NSub[5];