概述:虚拟机把描述类的数据从Class字节码文件文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可已被虚拟机直接使用的Java类型,这就是类加载机制。
阶段:加载、验证、准备、解析、初始化、使用、卸载。
类加载的时机
加载可以交给虚拟机的具体实现来自有把握;
初始化的情况
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,使用new实例化对象的时候、读取或设置一个类的静态字段、调用一个类的静态方法的时候。
- 使用reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化;
- 当初始化一个类的时候,发现其父类还没有初始化,则需要先触发其父类初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会初始化这个主类。
不被初始化的情况
- 子类引用父类的静态变量,子类不会被初始化;
- 通过数组定义来引用类,被引用的类不会初始化;
- 调用类的常量;
接口和类初始化的区别
当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成初始化,只有在真正用到父接口时才会初始化。
类加载过程
加载
方法区(1.7之前叫永久代,从1.8开始叫元空间)
初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class
实例(类对象);
- 通过一个类的全限定名来获取定义此类的二进制流;
- 将这个二进制流加载到元空间(方法区)中,将静态存储结构转换为方法区的运行时数据结构;
- 并且生成一个java.lang.Class对象,作为方法区这些数据的访问入口;
- 如果这个类还有父类没加载,则先加载父类;如果在加载过程中,父类被删除,则发生NoClassDefFoundError错误,即没有类定义被找到。
ClassNotFoundException 类没有被找到异常,发生在类加载阶段,加载类本身不存在;而NoClassDefFoundError是第一个类找到了,跟它关联的类没找到。
注意: 这里的字节码二进制流只并非是从class文件中获取,还有可能从其他地方获取。如网络中、数据库中、运行时计算生成等;加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需格式存储在方法区中,方法区中的数据存储格式由虚拟机自行定义。
Link阶段
包含验证、准备、解析三个步骤,验证是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理等,准备阶段是为静态变量分配内存、并设定默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存布局。
验证
- 验证类是否符合JVM规范、安全性检查;保证字节码的正确性,不会危害虚拟机自身安全;
- 文件格式验证:验证字节流是否符合class文件格式的规范;经过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面三个验证都是基于方法区的存储结构进行的。
- 版本号是否与虚拟机版本号兼容;
- 常量池中是否有不被支持的常量类型;
- 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,父类是否继承了不允许被继承的类等。以保证其描述的信息符合Java语言规范要求;
- 字节码验证:主要是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
- 符号引用验证:对类自身以外的信息进行匹配性校验;
- 文件格式验证:验证字节流是否符合class文件格式的规范;经过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面三个验证都是基于方法区的存储结构进行的。
准备
为 类变量(被static修饰的变量) 分配内存并设置类变量初始值阶段,这些内存都将在方法区中进行分配;这里的初始化值阶段是指数据类型的零值。
比如public static int value=123
在准备阶段过后的初始值是0而是不是123.123的值是在初始化阶段才会做的。但是如果一个静态成员是不可变得如public static final int value=123
则会直接赋值123.
解析
将常量池内的符号引用替换为直接引用的过程
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧视地定位到目标即可。
- 直接引用:
- 直接引用是可以使直接指向目标的指针;比如指向类型、类变量、类方法的直接引用可能是指向方法区的指针;
- 相对偏移量;指向实例变量、实例方法的直接引用都是偏移量;
- 一个能间接定位到目标的句柄;
同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那么引用的目标必定已经在内存中存在。
对于符号引用的理解
在编译时,java类并不知道所引用的类的实际实际地址,因此只能用符号引用来代替,比如:类似于CONSTANT_Class_info的常量来表示类地址。在解析阶段就是将符号引用替换为指向实际内存地址的直接引用的过程。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。
解析的时机
虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。要求在执行anewarray、new、putfield等13个用于操作符号引用字节码指令之前,先对它们所使用的符号引用进行解析。
- 类或接口的解析 《java虚拟机p183》
- 字段解析
- 类方法解析
- 接口方法解析
初始化
在准备阶段,变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
在类初始化之前,会有一个特殊的构造方法(被称为类构造器方法<clinit>()方法),线程安全的,只会执行一次,用来收集所有静态代码块,静态变量的赋值语句操作,按顺序收集。然后以线程安全的方式来执行这个特殊构造;
初始化触发的时机:
- 首次访问这个类的静态变量或静态方法时;
- 子类初始化,如果父类还没有还没有,会触发;
- main方法所在的类,首先被初始化;
不会触发初始化
- 访问类的static final 变量不会触发初始化,甚至不会触发类的链接;
- 类对象.class不会触发初始化;
- 创建该类的数组不会触发初始化;
类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程,在这个过程中,JVM会初始化继承树上还没有被初始化过得所有父类,并且会执行这个链路上所有未执行过得静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。
new和newInstance的区别:
new是强类型校验,可以调用任何构造方法,在使用new操作时,这个类可以没有被加载过。而Class类下的newInstance时弱类型,只能调用无参构造方法,如果没有默认构造方法,就会抛出InstantiationException
异常,java通过类加载器把类的实现与类的定义进行解耦,所以是实现面向接口编程、依赖倒置的必然选择。
类加载器
概念:
“通过一个类的全限定名来获取描述此类的二进制字节流“ 把这个动作放到虚拟机外部去实现,以便这个程序自己决定如何去获取所有的类,实现这个动作的代码被称为类加载器。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性。换句话说比较两个类是否相等,只有这两个类是由同一个类加载器加载的前提下才有意义。如果不是由同一个类加载器加载的,即使来源于同一个class文件,那么这两个类都不相等。
类加载器分类
-
类加载器类似于原始部落结构,存在权利等级制度,最该一层是家族中威望最高的Bootstrap,它是JVM启东时创建的,通常由与操作系统相关的本地代码实现,是最根基的类加载器,负载装载最核心的Java类,如Object、System等;
-
第二层为PlatForm ClassLoader即平台类加载器,用以加载一些扩展的系统类,如XML、加密、压缩相关功能;
-
第三层是Application ClassLoader 应用类加载器,主要加载用户定义的CLASSPATH路径下的类。
双亲委派模型
从java虚拟机角度,只存在两种不同的类加载器:
启动类加载器:使用C++实现,是虚拟机自身的一部分;
所有其他的类加载器:由java语言实现,地理与虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader;
类的双亲委派加载机制
从java开发人员角度来看:
- 启动类加载器:负责加载jdk jre/lib中核心的类,例如:rt.jar;无法被Java程序直接使用;
- 扩展类加载器:负责加载jre/lib/ext 扩展目录中的类;开发者了一直接使用扩展类加载器;
- 应用程序类加载器(系统类加载器):加载classpath下的所有class类;如果应用程序中没有自定义过自己的类加载器,一般情况下就是程序中默认的类加载器;
双亲委派模型要求除了顶层的启动类加载器之外,其余类加载器都应当有自己的福类加载器。这里的类加载器之间的父子关系一般不会以继承的关系来实现,而都使用组合关系来复用父加载器代码
组合
通过对现有对象进行拼装即组合产生新的具有更复杂的功能;强调的是整体与局部的关系;
继承和组合的概念解析
继承的理念是is-a(是一个)的关系,比如定义一个Car类,它继承自交通工具类,可以说它是一个交通工具;
组合的理念是has-a(有一个)的关系,比如Car类它有四个轮子、四个车门、四个车窗组成。组成它的又分别是不同的对象。可以说它有没有轮子这个类,有没有车门这个类。
继承和组合的区别
继承 | 组合 | |
---|---|---|
优点 | 支持扩展,通过继承父类实现,但会使系统结构较复杂;易于修改被复用的代码 | 代码黑盒复用,被包括的对象内部细节对外不可见,封装型号性好;整体与局部类之间松耦合,相互独立;支持扩展;每个类只专注于一项任务;支持动态扩展,可在运行时根据具体对象选择不同类型的组合对象 |
缺点 | 代码白盒复用,父类实现细节暴露给子类,破坏了封装性;当父类的实现代码修改时,可能使得子类也不得不修改,增加维护难度;子类缺乏独立性,依赖于父类,耦合度较高;不支持动态扩展,在编译期就决定了父类 | 创建整体类时,需要创建所有局部类对象,导致系统对象很多 |
注意:从上到下 父亲关系
类的加载顺序:
双亲委派模型的工作过程:
其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
双亲委派模式的优势
- 采用双亲委派模式的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就不会再去加载。
- 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
- 可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常
java.lang.SecurityException: Prohibited package name: java.lang