Java 文档 - java.lang.ClassLoader (Part 2)

201 阅读31分钟

前言

你是否真的理解java的类加载机制?点进文章的盆友不如先来做一道非常常见的面试题,如果你能做出来,可能你早已掌握并理解了java的类加载机制,若结果出乎你的意料,那就很有必要来了解了解java的类加载机制了。代码如下

package com.jvm.classloader;

class Father2{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father静态代码块");
    }
}

class Son2 extends Father2{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son静态代码块");
    }
}

public class InitativeUseTest2 {
    public static void main(String[] args) {

       System.out.println(Son2.strSon);
    }
}

运行结果:
        Father静态代码块
        Son静态代码块
        HelloJVM_Son

嗯哼?其实上面程序并不是关键,可能真的难不倒各位,不妨做下面一道面试题可好?如果下面这道面试题都做对了,那没错了,这篇文章你就不用看了,真的。

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe静态代码块");
    }
}

class Father extends YeYe{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father静态代码块");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son静态代码块");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather); 
    }
}

各位先用“毕生所学”来猜想一下运行的结果是啥…

注意了…
注意了…
注意了…

运行结果:
    YeYe静态代码块
    Father静态代码块
    HelloJVM_Father

是对是错已经有个数了吧,我就不拆穿各位的小心思了…

以上的面试题其实就是典型的java类的加载问题,如果你对Java加载机制不理解,那么你可能就错了上面两道题目的。这篇文章将通过对Java类加载机制的讲解,让各位熟练理解java类的加载机制。

其实博主还是想在给出一道题,毕竟各位都已经有了前面两道题的基础了,那么请看代码:

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe静态代码块");
    }
}

class Father extends YeYe{
    public final static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father静态代码块");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son静态代码块");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather);
    }
}

注意了
注意了
注意了

运行结果:HelloJVM_Father

冲动的小白童鞋看到了运行结果,果断的注销了博客账户…

1. 什么是类的加载(类初始化)

JVM重要的一个领域:类加载

当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

而类加载必然涉及类加载器,下面我们先来了解一下类的加载。

类的加载(类初始化):

1、在java代码中,

类型

加载连接、与初始化过程都是在

程序运行期间

完成的(类从磁盘加载到内存中经历的三个阶段)【牢牢记在心里】

2、提供了更大的灵活性,增加了更多的可能性

虽然上面的第一句话非常简短,但是蕴含的知识量却是巨大的!包含两个重要的概念:

1、类型

定义的类、接口或者枚举称为类型而不涉及对象,在类加载的过程中,是一个创建对象之前的一些信息

2、程序运行期间

程序运行期间完成典型例子就是动态代理,其实很多语言都是在编译期就完成了加载,也正因为这个特性给Java程序提供了更大的灵活性,增加了更多的可能性

1、1.类加载注意事项

1、类加载器并不需要等到某个类被 “

首次主动使用

” 时再加载它关于首次主动使用这个重要概念下文将讲解
2、JVM规范允许类加载器在预料某个类将要被使用时就预先加载它
3、如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

首先给各位打个预防针:可能没有了解过JVM的童鞋可能看的很蒙,感觉全是理论的感觉,不勉强一字一句的“死看”,只要达到一种概念印象就好!等到有一定理解认识之后再回头看一遍就好很多了,毕竟学习是一种循进渐进的过程,记住没有捷径!

2. 类的生命周期

从上图可知,类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括 7 个阶段,而验证、准备、解析 3 个阶段统称为连接。

加载、验证、准备、初始化和卸载这 5 个阶段的顺序是固定确定的,类的加载过程必须按照这种顺序开始(注意是“开始”,而不是“进行”),而解析阶段则不一定:它在某些情况下可以在初始化后再开始,这是为了支持 Java 语言的运行时绑定【也就是java的动态绑定/晚期定】。

2.1 加载

在上面已经提到过,加载阶段是类加载的第一个阶段!类的加载过程就是从加载阶段开始~

加载阶段指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象(JVM规范并未说明Class对象位于哪里,

HotSpot虚拟机将其放在方法区中

),用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

Class对象位置【HotSpot虚拟机】:

在JDK1.7是在方法区中或者说永久代
在JDK1.8放在方法区或者说元空间中
JDK1.8移除了永久代,转而使用元空间来实现方法区

注意

方法区其实只是个虚拟的概念,方法区具体的实现是永久代或者元空间,1.7是永久代,1.8是元空间】

永久代和元空间最大的区别:JKD7的永久代放在堆中并且独立于堆,JKD8的元空间完全剥离虚拟机,存在于直接内存

编写一个新的java类时,JVM就会帮我们编译成class对象,存放在同名的.class文件中。在运行时,当需要生成这个类的对象,JVM就会检查此类是否已经装载内存中。若是没有装载,则把.class文件装入到内存中。若是装载,则根据class文件生成实例对象。

怎么理解Class对象与new出来的对象之间的关系呢?

new出来的对象以car为例。可以把carClass类看成具体的一个人,而new car则是人物映像,具体的一个人(Class)是唯一的,人物映像(new car)是多个的。镜子中的每个人物映像都是根据具体的人映造出来的,也就是说

每个new出来的对象都是以Class类为模板参照出来的!为啥可以参照捏?因为Class对象提供了访问方法区内的数据结构的接口哇,上面提及过了喔!

总结:
加载阶段简单来说就是:

.class文件(二进制数据)——>读取到内存——>数据放进方法区——>堆中创建对应Class对象——>并提供访问方法区的接口

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个 java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

加载.calss文件的方式:
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,二进制数据通常有如下几种来源:

(1)从本地系统中直接加载
(2)通过网络下载.class文件
(3)从zip,jar等归档文件中加载.class文件
(4)从专用数据库中提取.class文件
(5)将java源文件动态编译为.class文件

2.2 验证

验证:确保被加载的类的正确性。
关于验证大可不必深入但是了解类加载机制必须要知道有这么个过程以及知道验证就是为了验证确保Class文件的字节流中包含的信息符合当前虚拟机的要求即可。
所以下面关于验证的内容作为了解即可!

验证是连接阶段的第一阶段,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

2. 3 准备【重点】

当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

这里需要注意两个关键点,即内存分配的对象以及初始化的类型

内存分配的对象

:要明白首先要知道Java 中的变量有类变量以及类成员变量两种类型,类变量指的是被 static 修饰的变量,而其他所有类型的变量都属于类成员变量。在准备阶段,JVM 只会为类变量分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始(初始化阶段下面会讲到)。

举个例子:例如下面的代码在准备阶段,只会为 LeiBianLiang属性分配内存,而不会为 ChenYuanBL属性分配内存。

public static int LeiBianLiang = 666;
public String ChenYuanBL = "jvm";

初始化的类型:在准备阶段,JVM 会为类变量分配内存,并为其初始化(JVM 只会为类变量分配内存,而不会为类成员变量分配内存,类成员变量自然这个时候也不能被初始化)。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的默认值,而不是用户代码里初始化的值。

例如下面的代码在准备阶段之后,LeiBianLiang 的值将是 0,而不是 666。

public static int LeiBianLiang = 666;

注意了!!!
注意了!!!
注意了!!!

但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,ChangLiang的值将是 666,而不再会是 0。

public static final int ChangLiang = 666;

之所以 static final 会直接被复制,而 static 变量会被赋予java语言类型的默认值。其实我们稍微思考一下就能想明白了。

两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 ChangLiang的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。

2.4 解析

当通过准备阶段之后,进入解析阶段。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

2.5 初始化【重点】

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。

Java程序对类的使用方式可分为两种:主动使用被动使用。一般来说只有当对类的首次主动使用的时候才会导致类的初始化,所以主动使用又叫做类加载过程中“初始化”开始的时机。那啥是主动使用呢?类的主动使用包括以下六种【超级重点】:

1、 创建类的实例,也就是new的方式

2、 访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰不不不其实更准确的说是在编译器把结果放入常量池的静态字段除外)

3、 调用类的静态方法

4、 反射(如 Class.forName(“com.gx.yichun”))

5、 初始化某个类的子类,则其父类也会被初始化

6、 Java虚拟机启动时被标明为启动类的类( JavaTest ),还有就是Main方法的类会

首先

被初始化

最后注意一点对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),这句话在继承、多态中最为明显!为了方便理解下文会陆续通过例子讲解

2.6 使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个使用阶段也只是了解一下就可以了。

2.7 卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。这个卸载阶段也只是了解一下就可以了。

2.8 结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

1、 执行了 System.exit()方法

2、 程序正常执行结束

3、 程序在执行过程中遇到了异常或错误而异常终止

4、 由于操作系统出现错误而导致Java虚拟机进程终止

3. 接口的加载过程

接口加载过程与类加载过程稍有不同。

当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。

4. 解开开篇的面试题

package com.jvm.classloader;

class Father2{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father静态代码块");
    }
}

class Son2 extends Father2{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son静态代码块");
    }
}

public class InitativeUseTest2 {
    public static void main(String[] args) {

       System.out.println(Son2.strSon);
    }
}

运行结果:
        Father静态代码块
        Son静态代码块
        HelloJVM_Son

再回头看这个题,这也太简单了吧,由于Son2.strSon是调用了Son类自己的静态方法属于主动使用,所以会初始化Son类,又由于继承关系,类继承原则是初始化一个子类,会先去初始化其父类,所以会先去初始化父类!

再看开篇的第二个题

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe静态代码块");
    }
}

class Father extends YeYe{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father静态代码块");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son静态代码块");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather); 
    }
}

运行结果:
    YeYe静态代码块
    Father静态代码块
    HelloJVM_Father

这个题就稍微要注意一下,不过要是你看懂这篇文章,这个题也很简单。这个题要注意什么呢?要注意子类Son类没有被初始化,也就是Son的静态代码块没有执行!发现了咩?那我们来分析分析…

首先看到Son.strFather,你会发现是子类Son访问父类Father的静态变量strFather,这个时候就千万要记住我在归纳主动使用概念时特别提到过的一个注意点了:**对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),这句话在继承、多态中最为明显!**嗯哼,对吧,Son.strFather中的静态字段是属于父类Father的对吧,也就是说直接定义这个字段的类是父类Father,所以在执行 System.out.println(Son.strFather); 这句代码的时候会去初始化Father类而不是子类Son

再看开篇的第三个题

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe静态代码块");
    }
}

class Father extends YeYe{
    public final static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father静态代码块");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son静态代码块");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather);
    }
}

运行结果:
HelloJVM_Father

这个题唯一的特点就在于final static !是的Son.strFather所对应的变量便是final static修饰的,依旧是在本篇文章中归纳的类的主动使用范畴第二点当中:访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰不不不其实更准确的说是在编译器把结果放入常量池的静态字段除外)

所以,这个题并不会初始化任何类,当然除了Main方法所在的类!于是仅仅执行了System.out.println(Son.strFather);所以仅仅打印了Son.strFather的字段结果HelloJVM_Father,嗯哼,是不是又突然明白了?

实际上上面的题目并不能完全说明本篇文章中归纳的类的主动使用范畴第二点!这话怎么说呢?怎么理解呢?再来一个程序各位就更加明了了

package com.jvm.classloader;

import sun.applet.Main;

import java.util.Random;
import java.util.UUID;

class Test{
    static {
        System.out.println("static 静态代码块");
    }

//    public static final String str= UUID.randomUUID().toString();
    public static final double str=Math.random();  //编译期不确定
}


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

请试想一下结果,会不会执行静态代码块里的内容呢?

重点来了
重点来了
重点来了
重点来了

运行结果

static 静态代码块
0.7338688977344875

上面这个程序完全说明本篇文章中归纳的类的主动使用范畴第二点当中的这句话:凡是被final修饰不不不其实更准确的说是在编译器把结果放入常量池的静态字段除外!

分析:

其实final不是重点,重点是编译器把结果放入常量池!当一个常量的值并非编译期可以确定的,那么这个值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,所以这个类会被初始化

到这里,能理解完上面三个题已经很不错了,但是要想更加好好的学习java,博主不得不给各位再来一顿烧脑盛宴,野心不大,只是单纯的想巅覆各位对java代码的认知,当然还望大佬轻拍哈哈哈,直接上代码:

package com.jvm.classloader;

public class ClassAndObjectLnitialize {

        public static void main(String[] args) {
            System.out.println("输出的打印语句");
        }

      public ClassAndObjectLnitialize(){

            System.out.println("构造方法");
            System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
        }

        {
            System.out.println("普通代码块");
        }

        int ZhiShang = 250;
        static int QingShang = 666;
        
        static
        {
            System.out.println("静态代码块");
        }     

}

建议这个题不要花太多时间思考,否则看了结果你会发现自己想太多了,导致最后可能你看到结果想砸电脑哈哈哈

隔离运行结果专业跑龙套…

隔离运行结果专业跑龙套…

隔离运行结果专业跑龙套…

隔离运行结果专业跑龙套…

隔离运行结果专业跑龙套…

运行结果
		静态代码块
		输出的打印语句

怎么样,是不是没有你想的那么复杂呢?

下面我们来简单分析一下,首先根据上面说到的触发初始化的(主动使用)的第六点:

Java虚拟机启动时被标明为启动类的类( JavaTest ),还有就是Main方法的类会首先被初始化

嗯哼?小白童鞋就有疑问了:不是说好有Main方法的类会被初始化的么?那怎么好多东西都没有执行捏?

那么类的初始化顺序到底是怎么样的呢?在我们代码中,我们只知道有一个构造方法,

但实际上Java代码编译成字节码之后,最开始是没有构造方法的概念的,只有类初始化方法对象初始化方法**。**

这个时候我们就不得不深入理解了!那么这两个方法是怎么来的呢?

类初始化方法:编译器会按照其出现顺序,收集:类变量(static变量)的赋值语句静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。

所以,上面的这个例子,类初始化方法就会执行下面这段代码了:

 static int QingShang = 666;  //类变量(static变量)的赋值语句

  static   //静态代码块
   {
       System.out.println("静态代码块");
   }

而不会执行普通赋值语句以及普通代码块了

对象初始化方法:编译器会按照其出现顺序,收集:成员变量的赋值语句普通代码块,最后收集构造函数的代码,最终组成对象初始化方法,值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

以上面这个例子,其对象初始化方法就是下面这段代码了:

    {                        
       System.out.println("普通代码块");    //普通代码块
    }
 
    int ZhiShang = 250;   //成员变量的赋值语句
    
    System.out.println("构造方法");  //最后收集构造函数的代码
    System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);

明白了类初始化方法 和 对象初始化方法 之后,我们再来看这个上面例子!是的!正如上面提到的:如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。上面的这个例子确实没有执行对象初始化方法。忘了吗?我们根本就没有对类ClassAndObjectLnitialize 进行实例化!只是单纯的写了一个输出语句。

如果我们给其实例化,验证一下,代码如下:

package com.jvm.classloader;

public class ClassAndObjectLnitialize {

        public static void main(String[] args) {
            new ClassAndObjectLnitialize();
            System.out.println("输出的打印语句");
        }

      public ClassAndObjectLnitialize(){

            System.out.println("构造方法");
             System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
        }

        {
            System.out.println("普通代码块");
        }

        int ZhiShang = 250;
        static int QingShang = 666;
        
        static
        {
            System.out.println("静态代码块");
        }      
}

运行结果:
		静态代码块
		普通代码块
		构造方法
		我是熊孩子我的智商=250,情商=666
		输出的打印语句		

到这里博主必须要声明一点了!我为什么要用这些面试题作为这篇文章的一部分?因为关于学习有一定的方法,你可以设想一下,如果博主不涉及并分析这几个面试题,你还有耐心看到这里吗?小白杠精童鞋说有。。。好的,就算有,大篇大篇的理论各位扣心自问,能掌握所有知识吗?小白杠精童鞋说说能。。。额,就算能,那你能保证光记理论一个月不遗忘吗?小白杠精童鞋说可以。。。我特么一老北京布鞋过去头给你打歪(我这暴脾气我天)。所以呢学习要带着兴趣、“目的”、“野心”!希望我这段话能对你有所帮助,哪怕是一点点…

5. 理解首次主动使用

我在上面提到过Java程序对类的使用方式可分为两种:主动使用与被动使用。一般来说只有当对类的

首次主动使用的时候才会导致类的初始化,其中首次关键字很重要,因此特地用一小结将其讲解!

怎么理解呢?老规矩看个题:

package com.jvm.classloader;

class Father6{
    public static int a = 1;
    static {
        System.out.println("父类粑粑静态代码块");
    }
}
class Son6{
    public static int b = 2;
    static {
        System.out.println("子类熊孩子静态代码块");
    }
}

public class OverallTest {
    static {
        System.out.println("Main方法静态代码块");
    }

    public static void main(String[] args) {
        Father6 father6;
        System.out.println("======");

         father6=new Father6();
        System.out.println("======");

        System.out.println(Father6.a);
        System.out.println("======");

        System.out.println(Son6.b);

    }
}

请试想一下运行结果

运行结果:
		Main方法静态代码块
		======
		父类粑粑静态代码块
		======
		1
		======
		子类熊孩子静态代码块
		2

分析:
首先根据主动使用概括的第六点:Main方法的类会首先被初始化。 所以最先执行Main方法静态代码块,而 Father6 father6;只是声明了一个引用不会执行什么,当运行到father6=new Father6();的时候,看到关键字new并且将引用father6指向了Father6对象,说明主动使用了,所以父类Father6将被初始化,因此打印了:父类粑粑静态代码块 ,之后执行 System.out.println(Father6.a);属于访问静态变量所以也是主动使用,这个时候注意了,因为在上面执行father6=new Father6();的时候父类已经主动使用并且初始化过一次了,这次不再是首次主动使用了,所以Father6不会在被初始化,自然它的静态代码块就不再执行了,所以直接打印静态变量值1,而后面的System.out.println(Son6.b);同样,也是只初始化自己,不会去初始化父类,只因为父类Father6以及不再是首次主动使用了!明白了没?如果有疑问欢迎留言,绝对第一时间回复!

6. 类加载器

喔o,终于到类加载器内容了!我们之前讲的类加载都是给类加载器做的一个伏笔,在这之前讲的所有类被加载都是由类加载器来完成的,可见类加载器是多么重要。由于上面的面试题并不涉及类加载器的相关知识,所以到这里再涉及涉及类加载器的知识!

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载入JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。

关于唯一标识符:

在Java中,一个类用其全限定类名(包括包名和类名)作为标识;

但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),如下:

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

启动类加载器BootstrapClassLoader,启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

总结一句话:启动类加载器加载java运行过程中的核心类库JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List…

扩展类加载器: ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。

应用程序类加载器: ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

总结一句话:应用程序类加载器加载CLASSPATH变量指定路径下的类 即指你自已在项目工程中编写的类

线程上下文类加载器:除了以上列举的三种类加载器,其实还有一种比较特殊的类型就是线程上下文类加载器。类似Thread.currentThread().getContextClassLoader()获取线程上下文类加载器,线程上下文加载器其实很重要,它违背(破坏)双亲委派模型,很好地打破了双亲委派模型的局限性,尽管我们在开发中很少用到,但是框架组件开发绝对要频繁使用到线程上下文类加载器,如Tomcat等等…

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,因为JVM自带的类加载器(ClassLoader)只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

1、在执行非置信代码之前,自动验证数字签名。

2、动态地创建符合用户特定需要的定制化构建类。

3、从特定的场所取得java class,例如数据库中和网络中。

需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机默认采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面将会详细讲到!

下面我们看一个程序:

package com.jvm.classloaderQi;

public class ClassloaderTest {
    public static void main(String[] args) {
        //获取ClassloaderTest类的加载器
        ClassLoader classLoader= ClassloaderTest.class.getClassLoader(); 
        
        System.out.println(classLoader);
        System.out.println(classLoader.getParent()); //获取ClassloaderTest类的父类加载器
        System.out.println(classLoader.getParent().getParent());
    }
}

运行结果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null

从上面的结果可以看出,并没有获取到ExtClassLoader的父加载器(Loader),原因是Bootstrap Loader(启动类加载器)是用C++语言实现的(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),找不到一个确定的返回父Loader的方式,于是就返回null。至于$符号就是内部类的含义。

7. java虚拟机入口应用:sun.misc.Launcher

到这里,我为什么要讲Launcher呢?如果你没有接触过这个Launcher类可能你会特别陌生,但是它却特别重要!为啥说它重要呢,不知道你有没有想过为啥类加载器首先会去到应用程序类加载器 ApplicationClassLoader,是的!我在介绍应用程序类加载器 ApplicationClassLoader和扩展类加载器 ExtensionClassLoader的时候就已经提到过这两个类加载器是由sun.misc.Launcher实现的!为了更好的理解,我们可以查看Launcher源码:

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
        Thread.currentThread().setContextClassLoader(loader);
    }

    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {}

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {}

当然这份源码是从OpenJDK上找到的,并不是所有的JDK源码都开源,如果不知道啥是OpenJDK的童鞋可以自行谷歌一下,OpenJDK中有很多JDK不开源的开源代码,当然也可以直接在idea中找到Launcher的源码,只不过由源码是由Intelliu IDEA反编译器提供的,里面一些变量都是以var+数字组成,其代码差别并不大,好了不扯了,虽然博主还能在哔哔几句。

通过以上源码我们可以得到相关的信息:

1、Launcher初始化了ExtClassLoaderAppClassLoader,首先是创建了Extcl扩展类加载器

2、之后的App应用类【系统类】加载器作为Launcher中的一个成员变量,至于为啥不把Extcl扩展类加载器也做为成员变量的原因,大家可以仔细想一想,是为啥呢?其实很简单,因为没必要,因为直接把App系统加载器.parent()方法即可得到Extcl扩展类加载器!

3、Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty(“sun.boot.class.path”)得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。我们可以用输出语句代码测试一下sun.boot.class.path是什么内容,其实就是JRE目录下的jar包或者是class文件。

System.out.println(System.getProperty("sun.boot.class.path"));

得到的结果是:

C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;
C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;
C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;
C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;
C:\Program Files\Java\jre1.8.0_91\classes

8. ExtClassLoader、AppClassLoader源码

如果想更进一层次的理解或者足够的好奇心,可以看ExtClassLoader、AppClassLoader源码

8.1 ExtClassLoader源码

/*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {

        static {
            ClassLoader.registerAsParallelCapable();
        }

        /**
         * create an ExtClassLoader. The ExtClassLoader is created
         * within a context that limits which files it can read
         */
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) {
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }

        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            if (s != null) {
                StringTokenizer st =
                    new StringTokenizer(s, File.pathSeparator);
                int count = st.countTokens();
                dirs = new File[count];
                for (int i = 0; i < count; i++) {
                    dirs[i] = new File(st.nextToken());
                }
            } else {
                dirs = new File[0];
            }
            return dirs;
        }

    }

当然我们可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径,有兴趣的童鞋可以进行测试。

8.2 AppClassLoader源码

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {


        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);

     
            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }       
    }

从源码中可以看到AppClassLoader加载的就是java.class.path下的路径

8.3 小结

从上面源码也可以看出,父加载器并不是指其父类,ExtClassLoaderAppClassLoader同样继承自URLClassLoader,这个时候小白童鞋就不耐烦了:那为啥调用AppClassLoader.getParent()方法会得到ExtClassLoader的实例呢?

实际上URLClassLoader的源码中也并没有getParent()方法。这个方法在ClassLoader中,源码如下:

public abstract class ClassLoader {

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
    // @GuardedBy("ClassLoader.class")
private static ClassLoader scl;

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    ...
}
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
    if (parent == null)
        return null;
    return parent;
}
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    return scl;
}

private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            //通过Launcher获取ClassLoader
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}
}

从上面的源码可以看到getParent()实际上返回的就是一个ClassLoader对象parentparent的赋值是在ClassLoader对象的构造方法中,它有两个情况:

1、由外部类创建ClassLoader时直接指定一个ClassLoaderparent

2、由getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。简单的说,就是一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader