上一篇博客我介绍了JVM的运行时数据区中的各个角色,以及各个角色的作用,本文介绍java类加载的过程。双亲委派原则
什么是类加载: 将.java的文件编译成的.class之后,将.class文件中的二进制数据读入到内存中,将其放在运行时数据区的元数据区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在元数据区内的数据结构。
类加载器何时启动: 可以是饿汉式(只要有其它类引用了它就加载)加载类,也可以是懒加载(等到类初始化发生的时候才加载)。JVM规范允许预加载(预料某类将要被使用提前加载),若加载时存在错误则在该类首次被调用报告错误,如果该类一直没有被调用则一直不会报错。
.class文件加载的位置:
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件
类的声明周期和加载流程:
从上图可以看出类加载分7小步,3大步(加载–>连接–>初始化)。接下来根据三大步为主题介绍,详细7小步。
一:加载 :Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象)
- 加载: 通过类的全限定名获取二进制字节流,将其静态存储结构转换成方法区的运行时数据区结构在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
相对于类生命周期的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
二:连接 :核心步骤,将原始的类定义信息平滑地转入 JVM 运行的过程中
- 验证:确保被加载类的准确性,
- 确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段是非常重要的,非必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施。前提是你能够保证不做验证没问题
文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,保证方法中的类型转换有效等等。
符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等。
- 准备:为类的静态变量分配内存,并将其赋默认值
为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。
对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)
- 解析:将常量池中的符号引用替换为直接引用(内存地址)的过程
符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。
假设:一个类有一个静态变量,该静态变量是一个自定义的类型,那么经过解析后,该静态变量将是一个指针,指向该类在方法区的内存地址。
初始化: 为类的静态变量赋初值
定义静态变量时指定初始值。如 private static String str=“a”;
在静态代码块里为静态变量赋值。如 static{ str=“b”; }
*** 只有对类的主动使用才会导致类的初始化。
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
1.创建类的实例,也就是new的方式
2.访问某个类或接口的静态变量,或者对该静态变量赋值
3.调用类的静态方法
4.反射(如 Class.forName(“com.shengsiyuan.Test”))
5.初始化某个类的子类,则其父类也会被初始化
6.Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类
父类型的初始化逻辑优先于当前类型的逻辑
卸载
-
执行了System.exit()方法
-
程序正常执行结束
-
程序在执行过程中遇到了异常或错误而异常终止
-
由于操作系统出现错误而导致Java虚拟机进程终止
什么是双亲委派?
当某个类加载器需要加载某个.class文件的时候,他会先把这个任务委托给他的上级类加载器,当他的上级类加载器没有加载,自己才回去加载这个.class文件
类加载的类别说明
-
BootstrapClassLoader:启动类加载器,加载java核心库,涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
-
ExtClassLoader:拓展类加载器,加载扩展库,如classpath中的jre ,javax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器 -
AppClassLoader:系统类加载器,加载程序所在的目录,如user.dir所在的位置的class
-
CustomClassLoader:自定义类加载器,可以加载指定的class文件
类加载器流程图:
图上文字描述:
- HelloWorld.class加载开始请求AppClassLoader
- AppClassLoader收到加载请求,已经执行过不在执行,若没有执行过则委派父类ExtClassLoader完成加载
- ExtClassLoader收到加载请求,已经执行过不在执行,若没有执行过则委派父类BootStrap完成加载
- 如果BootStrapClassLoader加载失败,让ExtClassLoader尝试加载
- 如果ExtClassLoader加载失败,让AppClassLoader加载
- 如果AppClassLoader加载失败,让自定义的ClassLoader加载
- 以上加载器全部加载失败,抛出异常 ClassNotFoundException
为什么要设计这种机制
-
可以避免重复加载,父类已经加载了,子类就不需要再次加载更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
-
保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
-
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,但是在这种机制下这些系统的类已经被Bootstrap classLoader加载过了,所以并不会再去加载,从一定程度上防止了危险代码的植入。
怎么打破双亲委派机制
自定义类加载器,重写loadClass方法;
使用线程上下文类加载器;
自己的类路径下的对象走我自己的classLoader, 其他的类还是走双亲委派
为什么要破坏?不破坏行不行?
每个webapp有自己的目录和类库,比如一个webapp使用类库A1.0版本,一个webapp使用类库A2.0版本,父类加载器加载类库A1.0版本,如果使用双亲委派,会由commonClassLoader去加载类库A1.0版本,这样第二个webapp会有问题