Jvm类加载机制

概述

class文件是一段规则有序的二进制流组成,类加载进行的就是将二进制流加载到内存中的阶段.

由于java的动态解析机制,解析阶段并不是一定在类加载的过程中完成.

类加载过程

加载

主要步骤有:

  1. 通过全限定名称获取这个类的二进制流.

  2. 将字节流里面的静态存储结构转化为方法区中运行时的存储结构.

  3. 在内存中生成一个java.lang.class对象,作为访问方法区的接口.

注:类加载器不会加载数组,这个是在jvm里面实现的,但是数组里面的类是类加载器来加载的.

验证

保证二进制流不会危害虚拟机,并且遵循了jvm规范.

验证主要是保证字节流中的信息不回危害虚拟机本身,并且符合虚拟机的规范要求.主要有4个方面的验证:

1. 文件格式验证:

对开头的二进制魔数进行验证,对主版本号的验证,等等.

目的是保证二进制能够正确解析,存到方法区之内.

2. 元数据验证:是否符合java语言规范,比如继承了一个被final修饰的类.

3. 字节码验证:

通过分析数据流和控制流,确定语义是否合法并且符合逻辑.

4. 符号引用验证:

可以看做是对类自身外的信息进行的匹配性校验,比如:是否可以通过权限定名找到相应的类,或者符号引用中的方法字段的可访问性,是否为public或者private等.

准备

此阶段是为类变量分配内存并设置初始值的阶段.这里分配的内存都在方法区进行.并且只会实例化static修饰的变量.类实例变量会在进行类初始化的时候,分配在堆内存中.需要注意的是,private static int a=1;在这句代码中设置的初始值为0,并不是1,

1.在类进行初始化后,会为1.

2. private static final int a=1,这时也是1.

下面是基本数据类型的零值(准备阶段初始值):

解析

将符号引用转化为内存引用.

将常量池内的符号引用变换为直接引用.

1. 符号引用:

和内存无关,一组用来描述所引用目标的一组符号.如:全限定名称.

2. 直接引用:

和内存有关,直接引用的数据,在内存中肯定存在.可以是指针,偏移量或者句柄.

主要解析的符号引用为:

(1) 类或者接口

(2) 字段

(3) 类方法

(4) 接口方法

(5) 方法类型

(6) 方法句柄

(7) 调用点限定符

初始化

根据java类中的代码进行初始化.也可以说是执行类构造器()方法并设置默认值的过程.

类构造器()的具体流程是:

(1) 由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生.

(2) 和类构造函数不同,不会去调用父类构造器,因为虚拟机会保证父类构造器在子类之前,肯定会执行完.因此第一个执行的类构造器是java.lang.Object.

(3) 如果一个类中没有静态语句块和变量赋值操作,则不会生成类构造器.

(4) 接口中的如果没有用到父类的变量,则不会生成父类构造器.

(5) 虚拟机会保证类构造器在多线程环境下的加锁与同步.

类加载器

一个类的唯一性是由类本身元素和类加载器共同确定的.

双亲委派模型

对于java开发人员来说,有3种系统提供的类加载器:

(1) 启动类加载器(bootstrap classLoder):加载/lib下的类,不能被java程序直接引用

(2) 扩展类加载器(extension_classLoder): 由sun.misc.Launcher$ExtClassLoder实现,加载/lib/ext下的类,java程序可以直接使用

(3) 应用程序类加载器(Application classLoder): 由sun.misc.Launcher$app-ClassLoder实现,加载用户类路径下的类.可以直接使用,并且默认类加载器为这个

双亲委派工作流程是:

当一个类加载器接收到加载类的请求时,不会立马去加载这个类,而是将这个请求委派给父类,一直到启动类加载器.当父类无法搜索到这个类时,最终类加载器才会去加载.

双亲委派模型的破坏

1) 在这个模型出现之前,可以继承java.lang.ClassLoder,重写loadClass(),后续建议只有在loadClass()加载失败后,然后再去加载自己的类.

2) 线程上下文类加载器的出现.线程还没创建时,他是父类加载器,如果全局没有设置,则会默认为应用加载器.从而实现了父类加载器请求子类去加载类.所有的SPI操作都是这个原理,比如常见的jdbc,jndi

3) 热部署话等动态的追求,OSGN的java模块化标准.使得每一个模块都有独自的类加载器.双亲委派模型成为一个网状结构.

Last updated

Was this helpful?