天天看点

类加载

类加载

 一.归纳

类加载

java中所有类加载的过程都是按照加载、验证、准备、初始化、卸载这几个步骤开始的 , 而解析则不一定, 当遇到动态绑定或者晚期绑定的情况下 , 可以在初始化之后再开始  .

虚拟机把描述类的信息从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型 . 这就是类加载机制 .

在java中,类型的加载、连接和初始化过程都是在程序运行期间完成的 . 在加载的过程中虽然会稍微增加一定的性能开销 , 但是会为java程序提供更高的灵活性 . java中天生可以动态扩展的语言特性就是依赖运行期间动态加载和动态链接的特性实现的 .

当遇到以下5中情况时必须对类进行初始化(主动引用) .

1.  当new , getstatic , putstatic , invokestatic这4种情况下时 , 如类没有进行初始化 , 则先触发初始化.

最常见的场景就是 : 使用new关键字实例化对象的时候、读取或者设置一个类的静态字段(final修饰的或已在编译器就放入常量池的字段除外),以及调用一个类的静态方法

2.  使用java.lang.reflect包的方法进行反射调用的时候 .  如类没有进行初始化 , 则先触发初始化.

3. 初始化一个类的时候,若父类还未初始化 , 则先去初始化父类 . (接口不同 : 接口初始化时 , 并不要求父类接口全部完成初始化,只有在真正使用到父接口的时候才开始初始化父类)

4. 虚拟机启动时, 即项目启动时 , 用户需指定一个执行主类(包含main方法的类).虚拟机会先初始化这个类

5. 使用动态语言支持时 ,

注 : 以上情况被称为主动引用 , 其他所有的方式都为被动引用 . 且不会触发初始化 .

被动引用 :

1. 当使用子类直接调用父类的公有静态字段时 , 只有直接定义该字段的类[父类]才会初始化 , 子类不会初始化 .

2. 当定义引用类的数组时 , 不会触发该类的初始化 .

3. 类中的常量(静态final的)在编译阶段会存入静态常量池中 , 本质上并没有直接定义该常量的类 , 此时也不会触发初始化 .

要清晰一点 : 加载是类加载过程中的一部分.该阶段 , 虚拟机主要完成 :

a. 通过一个类的全限定类名(包名+类名)来获取定义此类的二进制字节流 .

b. 将这个字节流所代表的静态存储结果转化为方法区的运行时数据结构

c. 在内存中生成一个代表这个类的java对象 . 作为访问入口 .

虚拟机并不要求二进制流一定从.class文件中获取,且虚拟机并没有指明要求从哪获取 . 除从class文件中获取外 , 虚拟机也允许以下几种情况 :

从如zip,jar,war包中获取 .

通过代理的方式,如(java.lang.reflect.proxy 中使用proxygenrator.generateproxyclass为特定接口生成二进制字节流)

由jsp文件生成对应的class类

从数据库中读取 , 中间件服务器中 , 可将特定程序安装到数据库中以达到代码在集群间的分发 .

注意 : 开发过程中 , 可通过重写classloader的loadclass方法自定义自己的类加载器去控制获取二进制字节流的方式 .

数组本身也是一种引用类型 , 本身不通过类加载器创建 , 数组是通过虚拟机直接创建的 .但数组与类加载器仍有密切关系 , 因为数组的元素类型最终是依赖类加载器创建 .

一个数组类创建遵循以下原则 :

如果数组的元素类型是引用类型 , 则采用上述的加载过程加载组件类型,且数组将在加载该组件类型的类加载器的类名称空间上被标识.

若数组的元素类型不是引用类型而是基本类型,虚拟机会把数组标记为与引导加载器关联

数组类的可见性与其元素类型的可见性一致且元素不是引用类型 , 则数组类的可见性将默认为public

加载完成后 , 虚拟机外部的二进制字节流将按照虚拟机所需格式存储在方法区中 , ,然后在内存中实例化一个java对象

加载阶段与连接阶段的部分内容(如验证)是交叉进行的 .

验证是连接阶段的第一步 , 为确保class文件的字节流中的信息符合当前虚拟机的要求 . 且不会危害虚拟机自身安全需对信息进行验证 .

验证阶段主要完成以下4个阶段的动作 :

这一步主要验证字节流是否符合class文件格式规范及是否能被当前版本的虚拟机所处理 . 主要包括 :

是否以模数0xcafebabe开头(每一个java字节码文件(.class)都是以相同的4字节内容开始的——十六进制的<code>cafebabe</code>)

主次版本号是否在当前虚拟机处理范围内

常量池中的常量是否有不被支持的常量类型

指向常量的索引值是否有指向不存在的常量或不符合类型的常量

constant_utf-8_info中是否有不符合utf-8的编码

class文件中各个部分及文件本身是否有被删除或附加的信息

主要对字节码信息进行语义分析,保证其信息符合java规范,主要包括 :

验证是否有父类

父类是否继承了不被允许的类

是否实现了其父类或接口中要求实现的所有方法

字段/方法是否与父类相矛盾

主要是对类的方法体进行检验分析 . 主要工作有 :

保证任意时刻操作的数据类型与指令代码序列都能配合工作

保证跳转指令不会跳转到方法体以外的字节码指令上

保证方法体中的类型转换是有效的 .

该动作将在连接的最后一个阶段----解析时发生 .主要内容:

符号引用中通过字符串描述的全限类名是否能找到对应的类

指定类中是否存在符合方法的字段描述符以及简单名称所描述的字段和方法

符号引用中的类/方法/字段的访问性检查 . 即引用的是否可以被当前类访问

该阶段是正式为变量(仅包括static修饰的变量)分配内存并设置类变量初始值(为默认的零值)的阶段 , 这些变量所使用的内存都将在方法区中进行分配 .

此处仅包括类中定义的被static修饰的变量,不包括实例变量 , 实例变量将会在对象实例化的时候随着对象一起被分配在java堆内存中 .

如 : 一个类中定义有public static int a = 100; 那么在准备过后的a = 0 ; 而a = 100是在程序被编译后 , 存放在类构造器&lt;clinit&gt;方法中.即a=100的赋值是在初始化阶段进行的

若a被final修饰符修饰 , 即: public static final int a = 100; 那么该字段的属性将存为constantvlaue属性,在准备阶段虚拟机就会为a赋值为100.

该阶段是虚拟机将常量池中的符号引用替换为直接引用的过程

以一组符号来描述所引用的目标 , 符号可以是任何形式的字面量.是要在使用时无歧义的可以定位到目标即可,其余虚拟机的内存布局无关 .

是可以直接指向目标的指针,相对偏移量或者是能间接定位到目标的句柄 . 其和虚拟机的内存布局相关,如果有直接引用 , 那么该引用的目标已经在内存中存在 .

类或者接口的解析

字段解析

类方法解析

接口方法解析

类加载的最后一步 , 将以上经过加载、验证、准备、解析的字节码文件加载为java对象 .

对于java中的每一个类 , 都需要加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性. 每一个类加载器都有一个独立的类名称空间 .

即: 比较两个类是否相等 , 只有这两个类是由同一个类加载器加载的前提下才有意义 . 否则即便同一个虚拟机环境下 , 两个类也绝不相同 .

细分而言 , 类加载器分三种:

启动类加载器 . 由c++实现 , 是虚拟机的一部分

bootstrapclassloader: 用c++编写 , 嵌在jvm内核中的加载器 , 主要是负责加载java_home/lib下的类库

扩展类加载器 ,

extensionclassloader: 用java编写 , 器父类加载器是bootstrap主要是加载java_home/lib/ext目录下的类库

ava中系统属性java.ext.dirs指定的目录由extclassloader加载器加载,如果程序中没有指定该系统属性(-djava.ext.dirs=sss/lib)那么该加载器默认加载$java_home/lib/ext目录下的所有jar文件,通过程序来看下系统变量java.ext.dirs所指定的路径

应用程序类加载器

appclassloader: 加载classpath目录下的所有的jar和class文件 , 其父类加载器为ext classloader

原则 :

除了顶层的bootstrapclassloader外 , 所有的类加载器都会有自己的父类加载器 , 且加载器之间一般不会以继承的方式实现 , 而是通过组合的方式 .

工作原理:

如果一个类加载器收到加载请求 , 先把请求委派给父类完成,因此所有的加载请求都会上传到父类加载器中,只有当父类加载器无法完成时 , 才有子类加载器尝试自己去加载 .

优势:

java类随着类加载器一起具备了一种带优先级的层级关系