类加载器 - 类的加载、链接与初始化

类的加载、链接与初始化

概述

在Java代码中,类型的加载、链接与初始化过程都是在程序运行期间完成的java

  • 类型:能够理解为一个class
  • 加载:查找并加载类的二进制数据,最多见的状况是将已经编译完成的类的class文件从磁盘加载到内存中
  • 链接:肯定类型与类型之间的关系,对于字节码的相关处理
    • 验证:确保被加载的类的正确性
    • 准备:为类的静态变量分配内存,并将其初始化为默认值。可是在到达初始化以前,类变量都没有初始化为真正的初始值
    • 解析:在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用转换为直接引用的过程
  • 初始化:为类的静态变量赋予正确的初始值
  • 使用:好比建立对象,调用类的方法等
  • 卸载:类从内存中销毁

理解:public static int number = 666;程序员

上面这段代码,在类加载的链接阶段,为对象number分配内存,并初始化为0;而后再初始化阶段在赋予正确的初始值:666数据库

类的使用方式

Java程序对类的使用方式可分为两种数组

  • 主动使用
    • 建立类的实例
    • 访问某个类或接口的静态变量,或者对静态变量赋值
    • 调用类的静态方法
    • 反射
    • 初始化类的子类
    • Java虚拟机启动时被标明为启动类的类(包含main方法)
    • JDK1.7开始提供的动态语言支持(java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化)
  • 被动使用
    • 除了主动使用的七种状况以外,其余使用Java类的方法都被看做是对类的被动使用,都不会致使类的初始化

全部的Java虚拟机实现必须在每一个类或接口被Java程序“首次主动使用”时才初始化他们网络

代码理解

示例一:类的加载链接和初始化过程

代码一数据结构

public class Test01 {
    public static void main(String[] args) {
        System.out.println(Child01.str);
    }
}

class Father01 {
    public static String str = "作一个好人!";
    static {
        System.out.println("Father01 static block");
    }
}

class Child01 extends Father01 {
    static {
        System.out.println("Child01 static block");
    }
}

运行结果作一个好人!dom

Father01 static block
作一个好人!

代码二jvm

public class Test01 {
    public static void main(String[] args) {
        System.out.println(Child01.str2);
    }
}

class Father01 {
    public static String str = "作一个好人!";

    static {
        System.out.println("Father01 static block");
    }
}

class Child01 extends Father01 {
    public static String str2 = "作一个好人!";
    static {
        System.out.println("Child01 static block");
    }
}

运行结果函数

Father01 static block
Child01 static block
作一个好人!

分析:spa

  • 代码一中,咱们经过子类调用父类中的str,这个str是在父类被定义的,对Father01主动使用,没有主动使用Child01,故Child01的静态代码块没有执行,父类的静态代码块被执行了。 -> 对于静态字段来讲,只有直接定义了该字段的类才会被初始化。
  • 代码二中,对Child01主动使用;根据主动使用的7种状况,调动类的子类时,其全部的父类都会被先初始化,因此Father01会被初始化。 -> 当一个类初始化时,要求其父类所有都已经初始化完毕了。

以上验证的是类的初始化状况,那么如何验证类的加载状况呢,能够经过在启动的时候配置虚拟机参数:-XX:+TraceClassLoading查看

运行代码一,查看输出结果,能够看见控制台打印了very多的日志,第一个加载的是java.lang.Object类(无论加载哪一个类,他的父类必定是Object类),后面是加载的一系列jdk的类,他们都位于rt包下。往下查看,能够看见Loaded classloader.Child01,说明即便没有初始化Child01,可是程序依然加载了Child01类。

[Opened /usr/local/jdk1.8/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/local/jdk1.8/jre/lib/rt.jar]
...
[Loaded java.lang.Void from /usr/local/jdk1.8/jre/lib/rt.jar]
[Loaded classloader.Father01 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
[Loaded classloader.Child01 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
Father01 static block
作一个好人!
[Loaded java.lang.Shutdown from /usr/local/jdk1.8/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/local/jdk1.8/jre/lib/rt.jar]

拓展:JVM参数介绍

由于前一章节使用了JVM参数,因此对其作一下简单的介绍

  • 全部的JVM参数都是以-XX:开头的
  • 若是形式是:-XX:+<option>,表示开启option选项
  • 若是形式是:-XX:-<option>,表示关闭option选项
  • 若是形式是:-XX:<option>=<value>,表示将option选项的值设置为value

示例二:常量的本质含义

public class Test02 {
    public static void main(String[] args) {
        System.out.println(Father02.str);
    }
}

class Father02{
    public static final String str = "作一个好人!";

    static {
        System.out.println("Father02 static block");
    }
}

执行结果

作一个好人!

分析

能够看见,此段代码并无初始化Father02类。这是由于final表示的是一个常量,在编译阶段常量就会被存入到调用这个常量的方法所在的类的常量池当中,本质上,调用类并无直接引用到定义常量的类,所以并不会触发定义常量的类的初始化。在本代码中,常量str会被存入到Test02的常量池中,以后Test02与Father02没有任何关系,甚至能够删除Father02的class文件。

咱们反编译一下Test02类

Compiled from "Test02.java"
public class classloader.Test02 {
  public classloader.Test02();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String 作一个好人!
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

第一块是Test02类的构造方法,第二块是咱们要看的main方法。能够看见3: ldc #4 // String 作一个好人!,此时这个值已是肯定无疑的作一个好人!了,而不是Father02.str,证明了上面说的在编译阶段常量就会被存入到调用这个常量的方法所在的类的常量池当中

拓展:助记符

因前一章节涉及到了助记符,因此介绍下本章节涉及到的助记符及扩展

  • ldc:表示将int、float或String类型的常量值常量池中推送至栈顶
  • bipush:表示将单字节(-128 -至 127)的常量推送至栈顶
  • sipush:表示将一个短整形(-32768 至 32767)的常量推送至栈顶
  • iconst_1:表示将int类型的1推送至栈顶(这类助记符只有iconst_m1 - iconst_5七个)

示例三:编译期常量与运行期常量的区别

public class Test03 {
    public static void main(String[] args) {
        System.out.println(Father03.str);
    }
}

class Father03 {
    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("Father03 static block");
    }
}

运行结果

Father03 static block
a60c5db4-2673-4ffc-a9f0-2dbe53fae583

分析

本代码与示例二的区别在于str的值是在运行时确认的,而不是编译时就肯定好的,属于运行期常量,而不是编译期常量。当一个常量的值并不是编译期间肯定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会致使主动使用这个常量所在的类,致使这个类被初始化。

示例四:数组建立本质

代码一

public class Test04 {
    public static void main(String[] args) {
         Father04 father04_1 = new Father04();
        System.out.println("-----------");
        Father04 father04_2 = new Father04();
    }
}

class Father04 {
    static {
        System.out.println("Father04 static block");
    }
}

运行结果

Father04 static block
-----------

分析

  • 建立类的实例时,会初始化类
  • 全部的Java虚拟机实现必须在每一个类或接口被Java程序“首次主动使用”时才初始化他们

代码二

public class Test04 {
    public static void main(String[] args) {
        Father04[] father04s = new Father04[1];
        System.out.println(father04s.getClass());
    }
}

运行结果

class [Lclassloader.Father04;

分析

  • 建立数组对象再也不主动使用的7种状况内,属于被动使用,故不会初始化Father04
  • 打印father04s的类型为[Lclassloader.Father04,这是虚拟机在运行期生成的。 -> 对于数组示例来讲,其类型是有JVM在运行期动态生成的,表示为[Lclassloader.Father04这种形式,动态生成的类型,其父类就是Object。
  • 对于数组来讲,JavaDoc常常将构成数组的元素为Component,实际上就是将数组下降一个维度后的类型

反编译一下:

public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: anewarray     #2                  // class classloader/Father04
       4: astore_1
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: aload_1
       9: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      12: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      15: return
  • anewarray:表示建立一个引用类型(如类、接口、数组)的数组,并将其引用值值压入栈顶
  • newarray:表示建立一个指定的原始类型(如int、float、char等)的数组,并将其引用值压入栈顶

示例五:接口的加载与初始化

代码一

public class Test05 {
    public static void main(String[] args) {
        System.out.println(Child05.j);
    }
}

interface Father05 {
    int i = 5;
}

interface Child05 extends Father05 {
    int j = 6;
}

运行结果

6

分析

  • 接口中定义的常量自己就是public、static、final的
  • 结果显而易见,这时咱们删除掉Father05.class文件和Child05.class文件,程序依然能够正常运行
    • 接口中的常量自己就是final常量,会被加载到Test05的常量池中
    • 此时,Father05和Child05都不会被加载

代码二

public class Test05 {
    public static void main(String[] args) {
        System.out.println(Child05.j);
    }
}

interface Father05 {
    int i = 5;
}

interface Child05 extends Father05 {
    int j = new Random().nextInt(8);
}

运行结果

6

将Father05.class文件删除,运行结果

Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Father05
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at classloader.Test05.main(Test05.java:15)
Caused by: java.lang.ClassNotFoundException: classloader.Father05
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 13 more

分析

  • 只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会加载初始化

代码三

public class Test05 {
    public static void main(String[] args) {
        System.out.println(Child05.j);
    }
}

interface Father05 {
    Thread thread = new Thread() {
        {
            System.out.println("Father05 code block");
        }
    };
}

class Child05 implements Father05 {
    public static int j = 8;
}

运行结果

8

分析

  • 在初始化一个类时,并不会先初始化他所实现的接口

代码四

public class Test05 {
    public static void main(String[] args) {
        System.out.println(Father05.thread);
    }
}

interface GrandFather {
    Thread thread = new Thread() {
        {
            System.out.println("GrandFather code block");
        }
    };
}

interface Father05 extends GrandFather{
    Thread thread = new Thread() {
        {
            System.out.println("Father05 code block");
        }
    };
}

运行结果

Father05 code block
Thread[Thread-0,5,main]

分析

  • 在初始化一个接口时,并不会先初始化他的父接口

示例六:类加载器准备阶段和初始化阶段

代码一

public class Test06 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();

        System.out.println("i:" + Singleton.i);
        System.out.println("j:" + Singleton.j);
    }
}

class Singleton {
    public static int i;

    public static int j = 0;

    private static Singleton singleton = new Singleton();

    private Singleton() {
        i ++;
        j ++;
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

运行结果

i:1
j:1

分析

首先Singleton.getInstance();进入SingletongetInstance方法,getInstance会返回Singleton的实例,Singleton的实例是new Singleton();出来的,所以调用了自定义的私有构造方法。在调用构造方法以前,给静态变量赋值,i默认赋值为0,j显式的赋值为0,通过构造函数以后,值都为1。

代码二

public class Test06 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();

        System.out.println("i:" + Singleton.i);
        System.out.println("j:" + Singleton.j);
    }
}

class Singleton {
    public static int i;

    private static Singleton singleton = new Singleton();

    private Singleton() {
        i ++;
        j ++;
    }

    public static int j = 0;

    public static Singleton getInstance() {
        return singleton;
    }
}

运行结果

i:1
j:0

分析

程序主动使用了Singleton类,准备阶段对类的静态变量分配内存,赋予默认值,下面给出类在链接及初始化阶段常量的值的变化

  • i : 0
  • singleton:null
  • j : 0
  • getInstance:初始化
    • i:0
    • singleton:调用构造函数
      • i:1
      • j:1
    • j:0【覆盖了以前的1】

故返回的值i为1,j为0

深刻解析类的加载、链接与初始化

类的加载

  • 将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,而后再内存中建立一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区的数据结构
  • 加载.class文件的方式
    • 从本地系统直接加载
    • 经过网络下载.class文件
    • 从zip等归档文件中加载.class文件
    • 从专有数据库中提取.class文件
    • 将Java源代码动态编译为.class文件
  • 类的加载的最终产品是位于内存中的Class对象
  • Calss对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口
  • 有两种类型的类加载器
    • Java虚拟机自带的类加载器
      • 根类加载器(Bootstrap):该加载器没有父加载器。他负责加载虚拟机的核心类库,如java.lang.*等。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操做系统,呀没有继承java.lang.CalssLoader类
      • 扩展类加载器(Extension):父加载器为根加载器。从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,若是用户建立的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类
      • 系统(应用)类加载器(System):父加载器为扩展加载器。从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,是用户自定义类加载器的默认父加载器。系统类加载器是纯Java类,是java.lang.ClassLoader类的子类
    • 用户自定义的类加载器
      • java.lang.ClassLoader的子类
      • 用户能够定制类的加载方式
  • 类加载器并不须要等到某个类被“首次使用”时再加载他
  • JVM规范容许类加载器在预料某个类将要被使用时就预先加载他,若是在预先加载的过程当中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用时才报告错误(LinkageError错误)。若是这个类一直没有被程序主动使用,那么类加载器就不会报告错误

类的链接

类被加载后,就进入链接阶段。链接就是将已经读入到内存中的类的二进制数据合并到虚拟机的运行时环境中去

类的验证

类的验证的内容

  • 类文件的结构检查
  • 语义检查
  • 字节码验证
  • 二进制兼容性验证

类的准备

在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于下面的Sample类,在准备阶段,将为int类型的静态变量i分配4个字节的内存空间,而且赋默认值0;为long类型的静态变量j分配8个字节的内存空间,并赋予默认值0

public class Sample {
    private static int i = 8;
    private static long j = 8L;
    ......
}

类的初始化

在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:

  • 在静态变量的声明处初始化
  • 在静态代码块中初始化

静态变量的声明语句,预计静态代码块都被看做类的初始化语句,Java虚拟机会按照初始化语句在类文件中的前后顺序来依次执行他们

类的初始化步骤

  • 假如这个类尚未被加载和链接诶,须要先进行加载和链接
  • 假如类存在直接父类,而且这个父类尚未被初始化,须要先初始化直接父类
  • 假如类中存在初始化语句,须要依次执行这些初始化语句

类的初始化时机

  • 当Java虚拟机初始化一个类时,要求他的全部父类都已经被初始化,可是这条规则并不适用于接口

    • 在初始化一个类时,并不会先初始化他所实现的接口
    • 在初始化一个接口时,并不会先初始化他的父接口

    所以,一个父接口并不会由于他的子接口或实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会致使该接口的初始化。代码参照代码理解-接口的初始化

  • 只有当程序访问的静态变量或者静态方法确实在当前类或者当前接口中定义时,才认为是对类或接口的主动使用
  • 调用ClassLoader类的loadClass方法加载一个类,并非对类的主动使用,不会致使类的初始化

拓展:类实例化

类的生命周期除了前文提到的加载、链接、初始化以外,还有类示例化,垃圾回收和对象终结

  • 为新的对象分配内存
  • 为实例变量赋予默认值
  • 为实例变量赋予正确的初始值
  • Java编译器为它编译的每个类都至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法被称为<init>。针对源代码中每个类的构造方法,Java编译器都产生一个<init>方法
相关文章
相关标签/搜索