类加载机制之加载、初始化

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

                                                                                                   ——《深入理解java虚拟机》

 简单来说,类加载就是把类数据从class文件加载到内存。这里区分一个概念“懒加载”和“延迟加载”,经常在学习Spring的的时候会遇到“懒加载”的概念,它指的是对象的“懒加载”,而不是指类。而“延迟加载”是指类的加载阶段是在类被使用的时候。

类的生命周期
类加载过程
源文件执行过程

 

类加载的五个阶段:加载-验证-准备-解析-初始化,除解析外都是按顺序执行的,但却不是说执行完上一步才开始下一步,这里的顺序指的是开始的顺序。另外,也不是说开始了上一步下一步就马上开始。例如加载之后就不一定会立马初始化。

使用Class.forName加载类时就有“是否初始化”这个参数,默认是true
@CallerSensitive
public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)
    throws ClassNotFoundException

类什么时候会加载

类什么时候会被加载呢?这里的加载指的类加载的第一个步骤——加载阶段。

java虚拟机规范没有规定类什么时候要被加载,所以什么时候加载由具体的虚拟机开发商自由实现。但是,规范规定了类什么时候被初始化。类被初始化在类被加载之后,我们先来看看类初始化的时机。

经过下章的验证得出:在jdk1.8的hotspot虚拟机环境下运行,类会在被用到或可能被用到的时候加载。

类什么时候会初始化

对一个类进行主动引用的时候

上面的话说得通俗点就是用到这个类得时候才会进行类的初始化。这里也要与对象的初始化做区分,对象的初始化在创建对象实例的时候。

设置jvm参数:-XX:+TraceClassLoading,运行下面的例子

//测试类(无main方法)
public class NotMainTest {
    static {
        System.out.println("NotMainTest类被初始化了");
    }

    public NotMainTest() {
        System.out.println("NotMainTest对象初始化");
    }
}
//main方法所在的类(启动类)
public class MainTest {
    static{
        System.out.println("main方法所在的类被初始化了");
    }
    public static void main(String[] args) {
        System.out.println("开始执行main方法了");
        NotMainTest sa = new NotMainTest();
        System.out.println("new NotMainTest 之后");
        NotMainTest sa = new NotMainTest();
    }
}

// [Opened C:\Program Files\Java\jdk1.8.0_261\jre\lib\rt.jar]
// [Loaded java.lang.Object from C:\Program Files\Java\jdk1.8.0_261\jre\lib\rt.jar]
// ...省略其他类加载日志
// [Loaded com.xiaolin.MainTest from file:/D:/IdeaProject/Study/out/production/Study/]
// ...省略其他类加载日志
// main方法所在的类被初始化了
// 开始执行main方法了
// [Loaded com.xiaolin.NotMainTest from file:/D:/IdeaProject/Study/out/production/Study/]
// NotMainTest类被初始化了
// NotMainTest对象初始化
// new NotMainTest 之后
// NotMainTest对象初始化

上述例子中有两类主动引用:

1、mainTest作为拥有main方法的启动类,会被优先初始化

2、第一次使用new创建对象的时候

除上面的两类外还有:

3、当初始化一个类时,如果其父类还未初始化,先初始化父类

4、访问类的静态字段或静态方法时(被final修饰的字段除外)

5、使用java.lang.reflect包的方法对类进行反射调用的时候

与主动引用相对的被动引用有以下几种,它们都不会触发类的初始化:

1、通过子类引用父类的静态字段

2、通过数组定义来引用类

3、调用引用类中的常量 

// 测试类父类
public class TestParent {
    public static String str = "staticString";
    public static final String constStr = "constString";

    static {
        System.out.println("TestParent初始化");
    }
}
public class TestSon extends TestParent{
    static {
        System.out.println("TestSon类被初始化了");
    }
}

 

public class TestOneMain {
    static{
        System.out.println("main方法所在的类被初始化了");
    }
    public static void main(String[] args) {
        System.out.println("开始执行main方法了");
        //通过子类调用父类的静态字段
        System.out.println(TestSon.str);
    }
}

// main方法所在的类被初始化了
// 开始执行main方法了
// [Loaded com.xiaolin.TestParent from file:/D:/IdeaProject/Study/out/production/Study/]
// [Loaded com.xiaolin.TestSon from file:/D:/IdeaProject/Study/out/production/Study/]
// TestParent初始化
// staticString

子类虽没有被初始化,但是却被加载了 

public class TestTwoMain {
    static{
        System.out.println("main方法所在的类被初始化了");
    }
    public static void main(String[] args) {
        System.out.println("开始执行main方法了");
        // 定义数组
        TestSon[] arr = new TestSon[10];
    }
}

// main方法所在的类被初始化了
// 开始执行main方法了
// [Loaded com.xiaolin.TestParent from file:/D:/IdeaProject/Study/out/production/Study/]
// [Loaded com.xiaolin.TestSon from file:/D:/IdeaProject/Study/out/production/Study/]

数组的元素类虽然没被初始化,但是被加载了 

public class TestThreeMain {
    static{
        System.out.println("main方法所在的类被初始化了");
    }
    public static void main(String[] args) {
        System.out.println("开始执行main方法了");
        // 引用常量
        System.out.println(TestParent.constStr);
    }
}

// main方法所在的类被初始化了
// 开始执行main方法了
// constString

 常量类既没有被加载,也没有被初始化

 这里重点说一下第三类情况(我碰到过这方面的问题),第三类情况是我们生产过程中经常用到的,定义一个常量类,里面定义了一些常量,在使用类中去引用。常量类与使用类在编译过后没有任何的联系

在编译阶段,经过常量传播优化,在使用类中对常量类的常量池引用已经转化为对使用类本身的常量池引用,简单来说优化过后就像这样

System.out.println(TestParent.constStr);
优化成下面这样
System.out.println("constString");

 反编译后的字节码指令如下

    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "constString"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

我在工作中就遇到过这样一个情况:客户现场部署的程序有bug,通过排查后修改了代码,并将修改后的项目打包给现场,因为网络等种种客观原因限制,没有将整包提供给现场,只提供了修改过的几个文件。其中就有常量类的文件,常量类文件中的一个常量A的值改了。可是现场替换了文件后问题依然存在,后来经过前辈指点才知道要把所有用到常量A的文件都打包给现场,即使这些文件的源码并没有改动。

扩展 

public class TestFourMain {
    static{
        System.out.println("main方法所在的类被初始化了");
    }
    public static void main(String[] args) {
        System.out.println("开始执行main方法了");
        // 引用类作为泛型
        List<TestSon> list = new ArrayList<>();
    }
}

// main方法所在的类被初始化了
// 开始执行main方法了

 突发奇想将类作为泛型声明后会怎样?实测后发现根本连类都不加载。意外之下看了一下ArrayList的构造函数,发现仅仅是定义了elementData的大小而已,根本没用到指定的泛型类型。看了一下elementData的类型是个Object的数组,根本就是假泛型。想起自己好像看过类似的内容,百度查到了遗忘的知识点——类型擦除。感兴趣的可以看看这篇文章:为什么说java的泛型是“假泛型”

 用Object + 强转来实现泛型,哈哈哈

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值