类加载器系统回顾与内容延伸
在之前已经对JVM中涉及到的类加载器的知识点进行了非常详细的学习,但是距离上一次又过去很长一段时间了,人的记忆肯定会有些模糊,所以在学习JVM下一个全新知识点时有必要对之前所学的类加载器进行一个系统性的回顾,启到一个承上起下的作用,不是有古人云嘛:“温故而知新,可以为师矣”,并且通过系统性的回顾会延升出一些未来要学的东东,前方比较枯燥,但是又是为了更加扎实的去学习JVM接下来的新知识做准备的,一定得耐着性子:
类加载:
- 在Java代码中,类型的加载、连接与初始化都是在程序运行期间完成的。
- 提供了更大的灵活性,增加了更多的可能性。
对于上面提到的两点在之前的学习中只是一笔带过了,这里稍加解释一下,对于C、C++语言而说有个dll动态链接库,其实它是在程序编译期间就已经把类型之间的关系就已经确定好了,但是在Java当中并非是这样的,在编译期间其类型的关系并没有完全的确定好,只有类型【在内存中的运行模型】被类加载器所加载之后,并且完成了连接与初始化其模型才真正的确立起来,基于这样一个特别Java就给我们提供了灵活性,因为替范并没有规定在类的加载、连接、初始化之后要做什么事情,所以只要满足规范之后的事情咱们就可以自由发挥了,也就是增加了更多的可能性了,比如说:可以从磁盘上加载、从网绺加载、也能从数据库加载。
Java虚拟机与程序的生命周期:
在如下几种情况下,Java虚拟机将结束生命周期:
①、执行了System.exit()方法。
②、程序正常执行结束。
③、程序在执行过程中遇到了异常或错误而异常终止。
④、由于操作系统出现错误而导致Java虚拟机进程终止。
类的加载、连接与初始化【特别重要!!】:
- 加载:查找并加载类的二进制数据。【可以从网络、磁盘、数据库等多方位进行加载】
- 连接:
①、验证:确保被加载的类的正确性。
那怎么确保被加载类的正确性呢?其实是由字节码的规范所约束,那这个规范是什么呢?期待未来的学习~~
②、准备:为类的静态变量分配内存,并将其初始化为默认值。
注意:默认值并非是咱们在代码中给静态变量显示给定的值,比如静态bool类型的变量默认值为false、int或long类型的默认值为0等等。
③、解析:把类中的符号引用转换为直接引用。
那什么是"符号引用",什么是"直接引用"呢?期待未来的学习~~ - 初始化:为类的静态变量赋予正确的初始值。
这个正确的初始值则为咱们在代码中给静态变量赋的值。
下面用图来展示其整个相关的过程:
其中初始化的动作可以在两个地方发生:一是直接给静态变量声明时进行赋值,二是在一个静态代码块当中,其实这两种情况对于编译成字节码而言是相等的,只是形式不太一样,这个在未来学习字节码再来探究。
- Java程序对类的使用方式可以分为两种:主动使用、被动使用。
- 所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。
- 主动使用(七种):
①、创建类的实例。
②、访问某个类或接口的静态变量,或者对该静态变量赋值。
③、调用类的静态方法。
④、反射(如Class.forName("com.test.Test"))。
⑤、初始化一个类的子类,表示对父类的主动使用。
⑥、Java虚拟机启动时被标明为启动类的类(Java Test,通过Java命令来执行Test,那么Test则为启动类)。
⑦、JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化。 - 除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
类的使用与卸载:
- 使用
- 卸载
类的加载:
- 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构。
- 加载.class文件的方式:
①、从本地系统中直接加载。
②、通过网络下载.class文件。
③、从zip、jar等归档文件中加载.class文件。
④、从专有数据库中提取.class文件。
⑤、将Java源文件动态编译为.class文件。
下面来看一下类加载使用的流程图:
另外再来看一下更加详细的时序图:
其中在解析过程中是我们目前完全没法理解的:
其中常量池是需要从字节码文件角度去进行查看的,所以接下来的学习中会详细的分析字节码文件,另外类的初始化和实例化是完全不同的概念,这个需要注意:
- 类的加载的最终产品是位于内存中的Class对象。
- Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
这就是典型的反射技术~~ - 有两种类型的类加载器:
①、Java虚拟机自带的加载器:
a、根类加载器(Bootstrap)
b、扩展类加载器(Extension)
c、系统(应用)类加载器(System)
②、用户自定义的类加载器:
a、java.lang.ClassLoader的子类
b、用户可以定制类的加载方式
也就是说可以自定义从哪里加载,加载过程中进行一些什么样的处理,比如说这种场景:为了不让用户直接能看到字节码生成字节码文件时会进行加密处理, 此时如果直接由系统自带的加载器去加载肯定是加载不了的,此时就可以用自义类加载器的方式进行解密处理。 - 类加载器并不需要等到某个类被“首次主动使用”时再加载它。【为什么?下面两点则是对它的解释】
- JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)。
- 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
类的验证:
- 类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
- 类的验证的内容:
①、类文件的结构检查。
②、语义检查。【比如说一个类中不可能是abstract,又是final的,类似于这种语义检查】
③、字节码验证。
④、二进制兼容性的验证。【比如说老版本编译出来的字节码文件是可以运行在新版本的JVM之上,相反则不行】
类的准备:
在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0。
类的初始化:
在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:(1)在静态变量的声明处进行初始化;(2)在静态代码块中进行初始化。例如在以下代码中,静态变量a和b都被显式初始化,而静态变量c没有被显式初始化,它将保持默认值0。
静态变量的声明语句,以及静态代码块都被看做类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。例如当以下Sample类被初始化后,它的静态变量a的取值为4。
类的初始化步骤:
- 假如这个类还没有被加载和连接,那就先进行加载和连接。
- 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类。
- 假如类中存在初始化语句,那就依次执行这些初始化语句。
类的初始化时机【再次温故其类的主动使用的七种方式】:
- 创建类的实例。
- 访问某个类或接口的静态变量,或者对该静态变量赋值。
- 调用类的静态方法。
- 反射(如Class.forName("com.test.Test"))。
- 初始化一个类的子类,表示对父类的主动使用。
- Java虚拟机启动时被标明为启动类的类(Java Test,通过Java命令来执行Test,那么Test则为启动类)。
- JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化。
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化【关于这点可以参考:https://www.cnblogs.com/webor2006/p/8922287.html】。
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
- 在初始化一个类时,并不会先初始化它所实现的接口。
- 在初始化一个接口时,并不会先初始化它的父接口。
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
类加载器:
类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。
除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的加载器都应该继承ClassLoader类。
用图来表示一下其关系:
获得ClassLoader的途径:
Jar hell问题以及解决办法:
- 当一个类或者一个资源文件存在多个jar中,就会存在jar hell问题。
- 可以通过以下代码来诊断问题:
类加载器的父亲委托机制:
- 在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父加载器。
比如说如下情况:loader1要去加载Sample这个类,并非直接由它来加载,而是先委托给它的父类加载,如果父类加载不了则又往下委托,最终则由loader1加载了,并不会委托给loader1的子类loader2去加载。
其详细过程可以看如下图:
- Bootstrap ClassLoader / 启动类加载器
$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类。 - Extension ClassLoader / 扩展类加载器
负责加载Java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。 - App ClassLoader / 系统类加载器
负责加载classpath中指定的jar包及目录中class。
若有一个类加载器能够成功加载Test类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器。
假设loader1实际加载了Sample类,则loader1为Sample类的定义类加载器,loader2与loader1为Sample类的初始类加载器。
比如:
loader1是loader2的父加载器,层次结构是loader1在loader2上面,但是其实它们是同一个类型的。
上面这段话在面试时可能会被问题,需要注意!!!
命名空间:
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
- 在同一个全名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
创建用户自定义的类加载器:
要创建用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法既可,该方法根据参数指定的类的名字,返回对应的Class对象的引用 。
不同类加载器的命名空间关系:
类的卸载:
- 当MySample类被加载、连接和初始化后,它的生命周期就开始了。当代表MySample类的Class对象不再被引用,既不可触及时,Class对象就会结束生命周期,MySample类在方法区的数据也会被卸载,从而结束MySample类的生命周期。
- 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
- 由用户自定义的类加载器所加载的类是可以被卸载的。
至此!!就已经将之前学习的所有知识进行了一下温故,打好坚实的基础以便未来的学习学得更加的轻松!!