java的类加载机制
当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM或通过加载、链接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这3个步骤统称为类加载或类初始化。
类加载过程
1.加载
加载指的是将类的clas文件读到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之创建一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种方式
- 从本地文件系统加载class文件,这是前面绝大部分实例程序的类加载方式。
- 从JAR包加载class文件,这种方式也是很常见的,JDBC编程时用到的数据库驱动类就放在JAR文件中。
- 通过网络加载class文件。
- 把一个Java源文件动态编译,并执行加载。
类加载器通常无须等到“首次使用”该类才加载该类,Java虚拟机规范允许系统预先加载某些类。
2.链接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入链接阶段,链接阶段负责把类的二进制数据合并到JRE中。类链接又分为如下3个阶段。
- 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量中的索引值是否存在不存在的常量或者不符合类型的常量。
- 元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
- 字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
- 符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类无法访问等问题。
- 准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
- 解析:将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述引用的目标,符号可以使任何的字面形式的字面量,只要不会出现冲突,能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
3.初始化
初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实不然,如果类中有语句:
1 | privae static int a = 10; |
它的执行阶段是这样的:首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,先给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a = 0,然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a = 10。
类加载时机
- 创建类的实例,也就是new一个对象
- 访问某个类或者接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时表明的启动类,即文件名和类名相同的那个类
除此之外,下面几种情形需要特别指出:
对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译的时候确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。
类加载器
类加载器负责加载所有的类,其为所有的被载入内存的类生成一个java.lang.Class实例对象。一旦一个类被加载入JVM中,同一个类就不会被再次载入了。正如一个对象有一个标识一样,一个载入JVM的类也有一个唯一的标识。在java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为唯一标识。例如:如果在pg的包中有一个名为Preson的类,被类加载器ClassLoader的实例kl负责加载,则该Preson类对应的Class对象在JVM中表示(Preson.pg.kl)。这意味着两个类加载器加载的同类名是不同的、它们所加载的类也不是完全不同、互不兼容的。
JVM预定义有三种类加载器,当一个JVM启动的时候,Java开始使用如下三种类加载器:
- 根类加载器(bootstrap class loader):它们用来加载Java的核心类,使用原生代码来实现的,并不继承自java.lang.ClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
- 扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
- 系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。系统通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父类加载器。由Java语言实现,父类加载器为ExtClassLoader。
类加载器加载Class大致要经过如下8个步骤:
- 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
- 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
- 请求使用父类加载器去载入目标类,如果载入成功跳至第8步,否则执行第5步。
- 请求使用根类加载器去载入目标类,如果载入成功跳至第8步,否则跳至第7步。
- 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到执行第7步。
- 从文件中载入Class文件,成功后跳至第8步。
- 抛出ClassNotFindException异常。
- 返回对应的java.lang.Class对象。
类加载机制
JVM的类加载机制主要有如下3种。
- 全盘负责:所谓的全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。
- 双亲委派机制:所谓的双亲委派,则是先父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,一次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
- 缓存机制:缓存机制将会保存所有加载过的Class都会被缓存,当程序中需要某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Calss对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后必须重新启动JVM,程序所做的修改才会生效的原因。
特别强调一下双亲委派机制
双亲委派机制,其工作原理是,如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件我也干不了时,儿子自己才想办法去完成。
双亲委派机制的优势:采用双亲委派模式的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层次关系可以避免类的重复加载,当父类已经加载了该类时,就没有必要子类再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派机制传递到启动类加载器,而启动类加载器在核心api发现这个名字的类,发现该类已被加载,并不会重新加载网络传递过来的,这样便可以防止核心API库被随意篡改