别翻了,这篇文章绝对让你深入理解java类的加载以及ClassLoader源码分析【JVM篇二】

@
前言
你是否真的理解java的类加载机制?点进文章的盆友不如先来作一道很是常见的面试题,若是你能作出来,可能你早已掌握并理解了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

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

以上的面试题其实就是典型的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

冲动的小白童鞋看到了运行结果,果断的注销了博客帐户....网络

一、什么是类的加载(类初始化)

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

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

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

类的加载(类初始化):

一、在java代码中,类型加载链接、与初始化过程都是在程序运行期间完成的(类从磁盘加载到内存中经历的三个阶段)【紧紧记在内心】

 

二、提供了更大的灵活性,增长了更多的可能性

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

一、类型

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

二、程序运行期间

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

一、1.类加载注意事项

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

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

二、类的生命周期

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

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

二、1.加载

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

加载阶段指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,而后在堆区建立一个 java.lang.Class对象(JVM规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在方法区中),用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口。

Class对象是存放在堆区的,不是方法区,这点不少人容易犯错。类的元数据才是存在方法区的。【元数据并非类的Class对象。Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的】

JDK7建立Class实例存在堆中;由于JDK7中JavaObjectsInPerm参数值固定为false。
JDK8移除了永久代,转而使用元空间来实现方法区,建立的Class实例依旧在java heap(堆)中

在这里插入图片描述

编写一个新的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.验证

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

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

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

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

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

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

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

二、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 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,因此就没有必要在准备阶段对它赋予用户想要的值。

若是还不是很清晰理解final和static关键字的话建议参阅下面博主整理好的文章,但愿对你有所帮助!

java中的Static、final、Static final各类用法

二、4.解析

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

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

==其实这个阶段对于咱们来讲也是几乎透明的,了解一下就好==。

二、5.初始化【重点】

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

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

一、 建立类的实例,也就是new的方式

 


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

 


三、 调用类的静态方法

 


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

 


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

 


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

 

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

二、6.使用

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

二、7.卸载

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

二、8.结束生命周期

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

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

二、 程序正常执行结束

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

四、 因为操做系统出现错误而致使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

再回头看这个题,这也太简单了吧,因为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
        输出的打印语句

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

五、理解首次主动使用

我在上面提到过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以及再也不是首次主动使用了!明白了没?若是有疑问欢迎留言,绝对第一时间回复!

六、类加载器

喔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()获取线程上下文类加载器

在Java的平常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,咱们还能够自定义类加载器,由于JVM自带的类加载器(ClassLoader)只是懂得从本地文件系统加载标准的java class文件,所以若是编写了本身的ClassLoader,即可以作到以下几点:

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

二、动态地建立符合用户特定须要的定制化构建类。

三、从特定的场所取得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。至于$符号就是内部类的含义。

七、关于命名空间

我以为讲类加载器,仍是颇有必要知道命名空间这个概念!实际上类加载器的一个必不可少的前提就是命名空间!

命名空间概念:

每一个类加载器都有本身的命名空间,命名空间由该加载器及全部父加载器所加载的类组成。

 


特别注意:

在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。

在不一样的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
由子加载器加载的类能看见父加载器的类,由父亲加载器加载的类不能看见子加载器加载的类

咱们已经知道每一个类只能被加载一次,其实这样说是不够准确的,怎样才算是准确的呢?那就涉及到命名空间的概念了!只有在相同的命名空间中,每一个类才只能被加载一次,反过来讲就是一个类在不一样的命名空间中是能够被加载屡次的,而被加载屡次的Class对象是互相独立的!

7.一、如何理解?

固然,直接把命名空间的概念直接抛给你们,若是没有接触过,100%是看不懂其中的含义的,我敢打包票,假一赔100万。。。那么博主就举出写例子让各位深入体会一下!固然这些例子涉及自定义加载器的一些知识,建议先对自定义加载器有必定了解在看!

例子必知前提:
一、 本身在idea或者eclipse中建立的工程项目中只要编译以后都会有对应的class文件成在classPath目录中
二、 而这些目录是由ApplicationClassLoader应用加载器加载
三、 我以后会将class文件放到系统桌面地址上,而这些系统地址由自定义加载器指定,因此由自定义加载器加载

7.二、准备

事先编译好,而后将项目工程中的两个字节码class文件【File1和File2】拷贝到系统桌面路径上,编译main方法就会出如今项目工程(ClassPath)下,注意如下例子状况中系统桌面路径的class文件一直都存在!

Main方法状况:

  • 一、建立一个自定义加载器classloader2,并声明桌面class文件路径,接着加载File1

  • 二、打印File1的加载器
  • 三、newInstanceFile1的实例

File1类的方法状况:

  • 一、File1的构造方法中存在一行代码:new File2new实例代码

File2类的方法状况:

  • 一、打印File2的加载器

7.三、测试代码情景一

删除File1File2项目工程中的class文件,工程项目的两个class文件都删除(只存在系统桌面路径下的class文件)

结果:File1File2的加载器都是自定义加载器

7.四、测试代码情景二

只删除File1项目工程中的class文件

结果:File1的加载器是自定义加载器,而执行到File2实例的加载器是App应用加载器

7.五、测试代码情景三

只删除File2项目工程中的class文件

结果:File1的加载器都是APP应用加载器,而执行到File2实例的时候报NoClassDefFoundError异常

得出结论:加载一个类(File1)的时候,这个类里面调用了其余的类(File2)或者其余类方法的初始化代码,那么这里面的类也会试着从这个类的加载器开始向上委托加载,若是全都加载不了加载不了就报NoClassDefFoundError异常

固然这样理解命名空间和类加载机制仍是远远不够的!

File2类中发生改变状况以下:

  • 一、File1的构造方法中存在一行new File2的实例这没变
  • 二、在File2的构造方法中,打印(访问)File1的class文件

7.六、测试代码情景四

只删除项目工程中File1的class文件

结果:File1的加载器都是自定义加载器,而执行到File2实例的加载器是App应用加载器,当运行到File2构造方法中的打印(访问)File1的class文件的时候报NoClassDefFoundError异常

得出结论:父亲加载器加载的类(File2)不能看见子加载器加载的类(File1

File1方法发生改变状况以下:
一、Main方法中newInstanceFile1的实例,File1的构造方法中存在一行new File2的实例这都没变
二、在File1的构造方法中,打印File2的class文件

7.七、测试代码情景五

只删除File1项目工程中的class文件

结果:File1的加载器都是自定义加载器,而执行到File2实例的加载器是App应用加载器,当运行到File1构造方法中的打印File2的class文件的时候没问题

得出结论:由子加载器加载的类(File1)能看见父加载器的类(File2)

固然还要注意知道的一点的是:若是两个加载器之间没有直接或间接的父子关系,那么它们各自加载类相互不可见。

固然整对上面的状况仍是至关比较抽象,毕竟没上代码,若是有任何疑问,欢迎留言,宜春绝对第一时间回复!

八、JVM类加载机制

JVM的类加载机制主要有以下3种。

全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其余Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入

父类委托:先让父类加载器试图加载该类,只有在父类加载器没法加载该类时才尝试从本身的类路径中加载该类,通俗讲就是儿子们都他么是懒猪,本身无论能不能作,就算能加载也先不干,先给本身的父亲作,一个一个往上抛,直到抛到启动类加载器也就是最顶级父类,只有父亲作不了的时候再没办法由下一个子类作,直到能某一个子类能作才作,以后的子类就直接返回,实力坑爹!

缓存机制:缓存机制将会保证全部加载过的Class都会被缓存,当程序中须要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为何修改了Class后,必须重启JVM,程序的修改才会生效

九、双亲委派模型

双亲委派模型的工做流程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,所以,全部的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即没法完成该加载,子加载器才会尝试本身去加载该类。也就是实力坑爹!

双亲委派机制:

一、当AppClassLoader加载一个class时,它首先不会本身去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

 


二、当 ExtClassLoader加载一个class时,它首先也不会本身去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

 


三、若是 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用
ExtClassLoader来尝试加载;

 


四、若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,若是
AppClassLoader也加载失败,则会报出异常 ClassNotFoundException。
在这里插入图片描述
从代码层面了解几个Java中定义的类加载器及其双亲委派模式的实现,它们类图关系以下:
在这里插入图片描述
从图能够看出顶层的类加载器是 抽象类ClassLoader类,其后 全部的类加载器都继承自ClassLoader(不包括启动类加载器),为了更好理解双亲委派模型,ClassLoader源码中的loadClass(String)方法该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2以后再也不建议用户重写但用户能够直接调用该方法, loadClass()方法是ClassLoader类本身实现的,该方法中的逻辑就是双亲委派模式的实现,loadClass(String name, boolean resolve)是一个重载方法,resolve参数表明是否生成class对象的同时进行解析相关操做。源码分析以下::

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用从新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //若是找不到,则委托给父类加载器去加载
                      c = parent.loadClass(name, false);
                  } else {
                  //若是没有父类,则委托给启动加载器去加载
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 若是都没有找到,则经过自定义实现的findClass去查找并加载
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否须要在加载时进行解析
              resolveClass(c);
          }
          return c;
      }
  }

既然存在这个双亲委派模型,那么就必定有着存在的意义,其意义主要是:Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系,经过这种层级关能够避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设经过网络传递一个名为java.lang.Integer的类,经过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会从新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样即可以防止核心API库被随意篡改。

双亲委派模型意义总结来说就是:
一、系统类防止内存中出现多份一样的字节码
二、保证Java程序安全稳定运行

十、ClassLoader源码分析

ClassLoader类是一个抽象类,全部的类加载器都继承自ClassLoader(不包括启动类加载器),所以它显得格外重要,分析ClassLoader抽象类也是很是重要的!

简单小结一下ClassLoader抽象类中一些概念:

二进制概念(Binary name):格式以下
在这里插入图片描述
把二进制名字转换成文件名字,而后在文件系统中磁盘上读取其二进制文件(class文件),每个class对象都包含了定义了这个类的classload对象,class类都是由类加载器加载的只有数组类型是有JVM根据须要动态生成。

特别注意数组类型

一、 数组类的类对象不是由类加载器建立的,而是根据Java运行时的须要自动建立的。
二、 数组类的类加载器getClassLoader()与它的元素类型的类加载器相同;若是元素类型是基本类型,则数组类没有类加载器也就是null,而这个null不一样于根类加载器返回的null,它是单纯的null。

到这里,下面就主要分析ClassLoader抽象类中几个比较重要的方法。

十、1.loadClass

该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2以后再也不建议用户重写但用户能够直接调用该方法,loadClass()方法是ClassLoader类本身实现的,该方法中的逻辑就是双亲委派模式的实现,其源码以下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数表明是否生成class对象的同时进行解析相关操做:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用从新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //若是找不到,则委托给父类加载器去加载
                      c = parent.loadClass(name, false);
                  } else {
                  //若是没有父类,则委托给启动加载器去加载
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 若是都没有找到,则经过自定义实现的findClass去查找并加载
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否须要在加载时进行解析
              resolveClass(c);
          }
          return c;
      }
  }

正如loadClass方法所展现的,当类加载请求到来时,先从缓存中查找该类对象,若是存在直接返回,若是不存在则交给该类加载去的父加载器去加载,假若没有父加载则交给顶级启动类加载器去加载,最后假若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也能够知道若是不想从新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载本身指定的类,那么咱们能够直接使用this.getClass().getClassLoder.loadClass("className"),这样就能够直接调用ClassLoader的loadClass方法获取到class对象。

十、2.findClass

在JDK1.2以前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,可是在JDK1.2以后已再也不建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用本身的findClass()方法来完成类加载,这样就能够保证自定义的类加载器也符合双亲委托模式。须要注意的是ClassLoader类中并无实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法一般是和defineClass方法一块儿使用的(稍后会分析),ClassLoader类中findClass()方法源码以下:

//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

十、3.defineClass(byte[] b, int off, int len)

defineClass()方法是用来将byte字节流解析成JVM可以识别的Class对象(ClassLoader中已实现该方法逻辑),经过这个方法不只可以经过class文件实例化class对象,也能够经过其余方式实例化class对象,如经过网络接收一个类的字节码,而后转换为byte字节流建立对应的Class对象,defineClass()方法一般与findClass()方法一块儿使用,通常状况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,而后调用defineClass()方法生成类的Class对象,简单例子以下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
      // 获取类的字节数组
      byte[] classData = getClassData(name);  
      if (classData == null) {
          throw new ClassNotFoundException();
      } else {
          //使用defineClass生成class对象
          return defineClass(name, classData, 0, classData.length);
      }
  }

须要注意的是,若是直接调用defineClass()方法生成类的Class对象,这个类的Class对象并无解析(也能够理解为连接阶段,毕竟解析是连接的最后一步),其解析操做须要等待初始化阶段进行。

十、4.resolveClass (Class<?>c)

使用该方法可使用类的Class对象建立完成也同时被解析。前面咱们说连接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

十、5.ClassLoader小结

以上上述4个方法是ClassLoader类中的比较重要的方法,也是咱们可能会常常用到的方法。接看SercureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,通常咱们不会直接跟这个类打交道,更可能是与它的子类URLClassLoader有所关联,前面说过,ClassLoader是一个抽象类,不少方法是空的没有实现,好比 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能,在编写自定义类加载器时,若是没有太过于复杂的需求,能够直接继承URLClassLoader类,这样就能够避免本身去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

检查完父类加载器以后loadClass会去默认调用findClass方法,父类(ClassLoader)中的findClass方法主要是抛出一个异常。

findClass根据二进制名字找到对应的class文件,返回值为Class对象Class<?>

defineClass这个方法主要是将一个字节数组转换成Class实例,会抛三个异常,但只是threws一个,由于其余两个是运行时异常。

loadClass方法是一个加载一个指定名字的class文件,调用findLoadedClass (String)检查类是否已经加载…若是已经加装就再也不加载而是直接返回第一次加载结果 因此一个类只会加载一次

十一、自定义类加载器

自定义核心目的是扩展java虚拟机的动态加载类的机制,JVM默认状况是使用双亲委托机制,虽然双亲委托机制很安全极高可是有些状况咱们须要本身的一种方式加载,==好比应用是经过网络来传输 Java类的字节码,为保证安全性,这些字节码通过了加密处理,这时系统类加载器就没法对其进行加载,这样则须要自定义类加载器来实现==。所以自定义类加载器也是颇有必要的。

自定义类加载器通常都是继承自 ClassLoader类,从上面对 loadClass方法来分析来看,咱们只须要重写 findClass 方法便可。自定义加载器中点:重写findClass,下面直接看自定义类加载器代码的流程:

package com.yichun.classloader;
import java.io.*;

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\dirtemp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.yichun.classloader.Demo1");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

自定义类加载器的核心在于对字节码文件的获取,若是是加密的字节码则须要在该类中对文件进行解密。上面代码程序只是简单Demo,并未对class文件进行加密,所以省略了解密的过程。这里有几点须要注意:

一、这里传递的文件名须要是类的全限定性名称,即com.yichun.test.classloading.Test格式的,由于
defineClass 方法是按这种格式进行处理的。

 


二、最好不要重写loadClass方法,由于这样容易破坏双亲委托模式。

 


三、这类Test 类自己能够被 AppClassLoader类加载,所以咱们不能把com/yichun/test/classloading/Test.class放在类路径下。不然,因为双亲委托机制的存在,会直接致使该类由AppClassLoader加载,而不会经过咱们自定义类加载器来加载。

十二、加载类的三种方式

到这里,相信你们已经对类的加载以及加载器有必定的了解了,那么你知道吗,其实加载类常见的有三种方式,以下:

一、静态加载,也就是经过new关键字来建立实例对象。

 

二、动态加载,也就是经过Class.forName()方法动态加载(反射加载类型),而后调用类的newInstance()方法实例化对象。

 

三、动态加载,经过类加载器loadClass()方法来加载类,而后调用类的newInstance()方法实例化对象

十二、1.三种方式的区别:

一、第一种和第二种方式使用的类加载器是相同的,都是当前类加载器。(this.getClass.getClassLoader)。而3由用户指定类加载器。

二、若是须要在当前类路径之外寻找类,则只能采用第3种方式。第3种方式加载的类与当前类分属不一样的命名空间

三、第一种是静态加载,而第2、三种是动态加载。

十二、2.两种异常(exception)

一、静态加载的时候若是在运行环境中找不到要初始化的类,抛出的是NoClassDefFoundError,它在JAVA的异常体系中是一个Error

二、动态态加载的时候若是在运行环境中找不到要初始化的类,抛出的是ClassNotFoundException,它在JAVA的异常体系中是一个checked异常

十二、3.理解Class.forName

Class.forName()是一种获取Class对象的方法,并且是静态方法。

Class.forName()是一个静态方法,一样能够用来加载类,Class.forName()返回与给定的字符串名称相关联类或接口的Class对象。注意这是一种获取Class对象的方法

官方给出的API文档以下

publicstatic Class<?> forName(String className)

Returns the Class object associated withthe class or interface with the given string name. Invokingthis method is equivalent to:

Class.forName(className,true, currentLoader)

where currentLoader denotes the definingclass loader of the current class.

For example, thefollowing code fragment returns the runtime Class descriptor for theclass named java.lang.Thread:

Class t =Class.forName("java.lang.Thread")

A call to forName("X") causes theclass named X to beinitialized.

Parameters:

className - the fully qualifiedname of the desired class.

Returns:

the Class object for the classwith the specified name.

能够看出,Class.forName(className)其实是调用Class.forName(className,true, this.getClass().getClassLoader())。第二个参数,是指Classloading后是否是必须被初始化。能够看出,使用Class.forName(className)加载类时则已初始化。因此Class.forName()方法能够简单的理解为:得到字符串参数中指定的类,并初始化该类。

十二、4.Class.forName与ClassLoader.loadClass区别

首先,咱们必须先明确类加载机制的三个过程主要是:加载 --> 链接 --> 初始化。

  • Class.forName():将类的.class文件加载到jvm中以外,还会对类进行解释,执行类中的static块;

  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

  • Class.forName(name, initialize, loader):带参函数也可控制是否加载static块。而且只有调用了newInstance()方法采用调用构造函数,建立类的对象 。

这个时候,咱们再来看一个程序:

package com.jvm.classloader;

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

public class ClassLoaderDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader=ClassLoaderDemo.class.getClassLoader();
        //一、使用ClassLoader.loadClass()来加载类,不会执行初始化块
        classLoader.loadClass("com.jvm.classloader.Demo");
        
        //二、使用Class.forName()来加载类,默认会执行初始化块
        Class.forName("com.jvm.classloader.Demo");
        
        //三、使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
        Class.forName("com.jvm.classloader.Demo",false,classLoader);
    }
}

记得一个一个测试!我上面的程序是一次写了三个的而且已经标明了标号一、二、3!!!各位再自个电脑上跑一遍,思路就很会清晰了!

1三、总结

类的加载、链接与初始化:

一、加载:查找并加载类的二进制数据到java虚拟机中

 

二、 链接:

验证: 确保被加载的类的正确性
准备:为类的静态变量分配内存,并将其初始化为默认值,可是到达初始化以前类变量都没有初始化为真正的初始值(若是是被 final 修饰的类变量,则直接会被初始成用户想要的值。)
解析:把类中的符号引用转换为直接引用,就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程

三、 初始化:为类的静态变量赋予正确的初始值

 


类从磁盘上加载到内存中要经历五个阶段:加载、链接、初始化、使用、卸载

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

(1)主动使用
(2)被动使用

全部的Java虚拟机实现必须在每一个类或接口被Java程序“首次主动使用”时才能初始化他们
主动使用
(1)建立类的实例
(2)访问某个类或接口的静态变量 getstatic(助记符),或者对该静态变量赋值 putstatic
(3)调用类的静态方法 invokestatic
(4)反射(Class.forName(“com.test.Test”))
(5)初始化一个类的子类
(6)Java虚拟机启动时被标明启动类的类以及包含Main方法的类
(7)JDK1.7开始提供的动态语言支持(了解)

被动使用
除了上面七种状况外,其余使用java类的方式都被看作是对类的被动使用,都不会致使类的初始化

1四、特别注意

初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当须要对一个类进行初始化时,会首先初始化类构造器(),以后初始化对象构造器()。

初始化类构造器:JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
初始化对象构造器:JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。值得特别注意的是,若是没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法通常在实例化类对象的时候执行。

若是在初始化 main 方法所在类的时候遇到了其余类的初始化,那么就先加载对应的类,加载完成以后返回。如此反复循环,最终返回 main 方法所在类。

若是本文对你有一点点帮助,那么请点个赞呗,谢谢~

最后,如有不足或者不正之处,欢迎指正批评,感激涕零!若是有疑问欢迎留言,绝对第一时间回复!

欢迎各位关注个人公众号,一块儿探讨技术,向往技术,追求技术,说好了来了就是盆友喔...

在这里插入图片描述

相关文章
相关标签/搜索