java类加载阶段与初始化顺序

一、类加载概述

在JVM执行我们写好的代码的过程中,具体是在代码中用到这个类的时候将“.class”文件加载进JVM内存里,类的加载到使用具体经过下面这几个过程:

加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载。

各个将阶段的任务描述:

二、类的初始化顺序

Java类加载机制中最重要的就是程序初始化过程,其中包含了静态资源,非静态资源,父类子类,构造方法之间的执行顺序。

首先通过一个例子来分析java代码的执行顺序:

    public class CodeBlockForJava extends BaseCodeBlock {
        {
            System.out.println("这里是子类的普通代码块");
        }
        public CodeBlockForJava() {
            System.out.println("这里是子类的构造方法");
        }
        @Override
        public void msg() {
              System.out.println("这里是子类的普通方法");
        }

        public static void msg2() {
            System.out.println("这里是子类的静态方法");
        }

        static {
            System.out.println("这里是子类的静态代码块");
        }

        public static void main(String[] args) {
            BaseCodeBlock bcb = new CodeBlockForJava();
            bcb.msg();
        }
        Other o = new Other();
    }

    class BaseCodeBlock {

        public BaseCodeBlock() {
            System.out.println("这里是父类的构造方法");
        }

        public void msg() {
            System.out.println("这里是父类的普通方法");
        }

        public static void msg2() {
            System.out.println("这里是父类的静态方法");
        }

        static {
            System.out.println("这里是父类的静态代码块");
        }

        Other2 o2 = new Other2();

        {
            System.out.println("这里是父类的普通代码块");
        }
    }

    class Other {
        Other() {
            System.out.println("初始化子类的属性值");
        }
    }

    class Other2 {
        Other2() {
            System.out.println("初始化父类的属性值");
        }
    }

这个例子比较简单,在运行代码之前分析一下:带有static关键字的代码块应该是最先执行,其次是非static关键字的代码块以及类的属性(Fields),最后是构造方法。带上父子类的关系后,上面的运行结果为:

    这里是父类的静态代码块
    这里是子类的静态代码块
    初始化父类的属性值
    这里是父类的普通代码块
    这里是父类的构造方法
    这里是子类的普通代码块
    初始化子类的属性值
    这里是子类的构造方法
    这里是子类的普通方法

注意的是类的属性与非静态代码块的执行级别是一样的,谁先执行取决于书写的先后顺序。

结论1:

父类的静态代码块 --> 
子类的静态代码块 --> 
初始化父类的属性值/父类的普通代码块(自上而下的顺序排列) --> 
父类的构造方法 --> 
初始化子类的属性值/子类的普通代码块(自上而下的顺序排列) --> 
子类的构造方法

注:构造函数最后执行。

接下来再看一个比较复杂的例子:

    public class ClassloadSort1 {

        public static void main(String[] args) {
            Singleton.getInstance();
            System.out.println("Singleton value1:" + Singleton.value1);
            System.out.println("Singleton value2:" + Singleton.value2);
    
            Singleton2.getInstance2();
            System.out.println("Singleton2 value1:" + Singleton2.value1);
            System.out.println("Singleton2 value2:" + Singleton2.value2);
        }
    }
    
    class Singleton {
        static {
            System.out.println(Singleton.value1 + "\t" + Singleton.value2 + "\t" + Singleton.singleton);
            //System.out.println(Singleton.value1 + "\t" + Singleton.value2);
        }
        private static Singleton singleton = new Singleton();
        public static int value1 = 5;
        public static int value2 = 3;
    
        private Singleton() {
            value1++;
            value2++;
        }

        public static Singleton getInstance() {
            return singleton;
        }

        int count = 10;

        {
            System.out.println("count = " + count);
        }
    }
    
    class Singleton2 {
        static {
            System.out.println(Singleton2.value1 + "\t" + Singleton2.value2 + "\t" + Singleton2.singleton2);
        }

        public static int value1 = 5;
        public static int value2 = 3;
        private static Singleton2 singleton2 = new Singleton2();
        private String sign;

        int count = 20;
        {
            System.out.println("count = " + count);
        }

        private Singleton2() {
            value1++;
            value2++;
        }

        public static Singleton2 getInstance2() {
            return singleton2;
        }
    }

这个用例相比第一个,知识点更深了一层。如果你用结论1是没法分析出正确答案的,但这并不代表结论1就是错误的。

运行结果:

    Singleton value1:5
    Singleton value2:3

    Singleton2 value1:6
    Singleton2 value2:4

Singleton中的value1,value2并没有受到构造方法中自加操作的影响。然而Singleton2中的代码也相同,为什么执行出来的效果就不一样呢?
要想知道原因,必须先搞清楚Java类加载中具体做了些什么。

我们来看Singleton2.getInstance()的执行分析:

(1) 类的加载。运行Singleton2.getInstance(),JVM在首次并没有发现Singleton类的相关信息。所以通过classloader将Singleton.class文件加载到内存中。

(2) 类的验证。

(3) 类的准备。将Singleton2中的静态资源转化到方法区。value1,value2,singleton在方法区被声明分别初始为0,0,null。

(4) 类的解析。将常量池内的符号引用替换为直接引用的过程。

(5) 类的初始化。执行静态属性的赋值操作。按照顺序先是value1 = 5,value2 = 3,接下来是private static Singleton2 singleton2 = new Singleton2();

这是个创建对象操作,根据 结论1 在执行Singleton2的构造方法之前,先去执行static资源和非static资源。但由于value1,value2已经被初始化过,所以接下来执行的是非static的资源,最后是Singleton2的构造方法:value1++;value2++。

所以Singleton2结果是6和4。

以上除了搞清楚执行顺序外,还有一个重点->结论2:静态资源在类的初始化中只会执行一次

有了以上的这个结论,再来看Singleton.getInstance()的执行分析:

(1) 类的加载。将Singleton类加载到内存中。

(2) 类的验证。

(3) 类的准备。将Singleton的静态资源转化到方法区。

(4)  类的解析。将常量池内的符号引用替换为直接引用的过程。

(5) 类的初始化。执行静态属性的赋值操作。按照顺序先是private static Singleton singleton = new Singleton(),根据 结论1结论2,value1和value2不会在此层执行赋值操作。所以singleton对象中的value1,value2只是在0的基础上进行了++操作。此时singleton对象中的value1=1,value2=1。
然后, public static int value1 = 5; public static int value2 = 3; 这两行代码才是真的执行了赋值操作。所以最后的结果:5和3。

如果执行的是public static int value1; public static int value2;结果又会是多少?结果: 1和1。

因为static变量的赋值在类的初始化中只会做一次。程序在执行private static Singleton singleton = new Singleton()时,已经是对Singleton类的static变量进行赋值操作了。这里new Singleton()是一个特殊的赋值。会自动过滤static变量的赋值操作。但非static的变量依然会被赋值。

结论3:在结论2的基础上,非静态资源会随对象的创建而执行初始化。每创建一个对象,执行一次初始化。

三、类加载器

JVM提供了以下3种系统的类加载器?

  • 启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
  • 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
  • 应用程序类加载器(Application ClassLoader):也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。

用户自定义的类加载器步骤?

  • 继承ClassLoader。
  • 重写findClass方法。从特定位置加载class文件,得到字节数组,然后利用defineClass把字节数组转化为Class对象。

为什么要自定义类加载器? 

  • 可以从指定位置加载class文件,比如说从数据库、云端加载class文件。
  • 加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。

Java程序对类的执行有几种方式?

JVM必须在每个类“首次 主动使用”的时候,才会初始化这些类。

  1. 创建类的实例。
  2. 读写某个类或者接口的静态变量。
  3. 调用类的静态方法。
  4. 同过反射的API(Class.forName())获取类。
  5. 初始化一个类的子类。
  6.  JVM启动的时候,被标明启动类的类(包含Main方法的类)。

只有当程序使用的静态变量或者静态方法确实在该类中定义时,该可以认为是对该类或者接口的主动使用。

JVM规范允许类加载器在预料某个类将要被使用的时候,就预先加载它。如果该class文件缺失或者存在错误,则在程序“首次 主动使用”的时候,才报告这个错误。(Linkage Error错误)。如果这个类一直没有被程序“主动使用”,就不会报错。

类加载机制与接口?

  • 当Java虚拟机初始化一个类时,不会初始化该类实现的接口。
  • 在初始化一个接口时,不会初始化这个接口父接口。
  • 只有当程序首次使用该接口的静态变量时,才导致该接口的初始化。

ClassLoader:调用Classloader的loadClass方法去加载一个类,不是主动使用,因此不会进行类的初始化。

类的卸载:

  • JVM自带的三种类加载器(根、扩展、系统)加载的类始终不会卸载。因为JVM始终引用这些类加载器,这些类加载器使用引用他们所加载的类,因此这些Class类对象始终是可到达的。
  • 由用户自定义类加载器加载的类,是可以被卸载的。

四、参考

https://www.jianshu.com/p/202f6abb229c

https://www.cnblogs.com/jianglinliu/p/11406064.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

codedot

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值