我们在使用一个Java类的时候,比如使用累的静态属性或者new一个对象,JVM虚拟机会将我们目标类的.class
文件加载到JVM特定的内存区域,然后经过一系列的加载流程,最后我们的程序才能够使用,这整个流程称之为类加载机制
,也就是说我们要使用一个类,必须先加载才能使用。当然,一个类的声明周期不单单只是加载,还有许多阶段,例如初始化、卸载等操作。
JVM类加载是通过一系列的类加载器进行实现,类加载器具有加载一个.class的文件能力,不同的类加载器有着不同的效果。
如图所示,我们要使用自己写的com.minor.test类,则JVM需要经历很多阶段,每个阶段很重要。
类加载的5步中,加载、校验、准备、初始化这几个阶段是严格顺序的,但是解析阶段不一定,因为Java支持运行时动态解析绑定(静态绑定、动态绑定)。
懒加载
是JVM加载类的一个策略,当程序需要用到类的时候才回去检查和加载。
JVM会保证一个类的加载时线程安全的。
1. 根据要加载的类的全限定名获取到该类的二进制文件,通过磁盘I/O转化为字节流。
1. 将字节流信息结构转化为方法区的运行时数据结构。
1. 在JVM堆内存中创建一个该类的`java.lang.Class`对象,作为方法区类元数据的访问入口。
校验阶段是类加载连接阶段的第一步
,目的是检查class文件是否符合JVM规范要求
。其中,校验阶段一共分为4个校验点
:
验证clsss文件字节流是否符合JVM规范,并支持当前JVM版本,例如:
- 文件是否是cafe babe开头
- 主版本号和副版本号是否支持当前JVM版本
- 常量是否有不符合格式的定义,等等...
字节码文件描述信息的语义是否符合Java语言语法规范,例如:
- 这个类是否继承了final修饰的类
- 是否是抽象类,是的话检查是否实现了父类或接口的方法
- 重写、重载等是否有语法错误,等等...
通过数据流分析、控制流分析,判断程序语义是否合法,保证不会危害JVM安全,例如:
- 跳转指令符合跳转规则
- 类型转换是否符合Java规范,等等...
检查符合引用的合法性,例如符号引用是否能指向目标、符号引用是否可访问等问题
准备阶段会为类中定义的静态变量分配内存并设置初始值,基本类型就赋值默认值,引用类型就是null。
但是,如果是final修饰的静态变量,且是基本类型和String且赋值时字面量形式,在准备阶段会赋值真实值,如果不是字面量形式,那么会在初始化赋值。
解析阶段是将JVM常量池类信息的符号引用
替换为直接引用
过程,该阶段会将一些静态方法替换为指向数据所真实存在的内存地址,称之为静态链接 or 静态绑定
,动态绑定
则是在程序运行时动态完成。
初始化阶段主要是对class中定义的静态块和静态变量赋予真实值。JVM规范中定义了6种情况必须对类进行初始化操作,包括:
1. 使用new关键字实例化对象时,读取 or 设置一个类的静态变量时,调用静态方法时。
1. 使用java.lang.reflect包进行反射时。
1. 发现父类没有初始化时,会初始化父类。
1. 程序启动类main方法所在类,严格触发初始化。
1. JDK7后,使用java.lang.invoke.MethodHandle句柄操作类时。
1. 当接口中存在default修饰符时,且实现类发生了初始化时。
类加载过程的执行者就是一个称为类加载器
的东西处理的(5个步骤),JDK提供了三层类加载器:
任何类加载的行为都要经过它,他负责加载java核心类库:rt.jar、resources.jar、charsets.jar等,也可以通过JVM指令进行指定加载路径:-Xbootclasspath
,我们在Java程序中打印rt.jar包下面的类的加载器是显示null
,是因为这个引导类加载器是C++语言构造。
主要用户加载lib/ext包下的jar包和.class文件,它继承自URLClassLoader
。
用来加载工程classpath下的jar包和.class文件。
负责加载用户自定义路劲的jar包和.class文件。
如果在java.lang包下,再自己写一个String类,其实自己写的String类并不会生效,除非放在我们自己的classpath下才能生效。这是因为java的类加载机制提供了一个双亲委派
的模型来保证核心类库的安全性,不被随意修改,因为同一个类被两个不同的类加载器加载就会是不同的类了。
双亲委派机制的简要流程是一个递归
的过程,如下:
1.AppClassLoader检查该类是否已经被加载findLoadClass()
,如果又返回,则不用加载,直接返回。
2.如果没有被加载过,则判断是否有父加载器父加载器不是父类,他们没有继承关系
,如有有则委托父 加载器加载loadClass()
。
3.如果父加载器加载不了,则调用当前类加载器进行加载。
1.保证安全,防止JDK核心类库不会被篡改。
2.避免重复加载,保证类的唯一性。
自定义一个类加载器,只需要继承java.lang.ClassLader
类,并重写findClass()
。
可以通过覆盖loadClass()
进行双亲委派的自定义。但是打破双亲委派是在ExtClassLoader之下的类加载器,JDK核心包仍然是不能打破的,会抛出package java权限错误。
例如Tomcat就是打破双亲委派机制典型的代表:
Tomcat最基本的类加载器,加载各个webapp应用公用库的加载。
对webapp应用不可见。
对tomcat容器和catalina不可见,对所有webapp应用可见。
各个应用的类加载器,各个应用的类相互隔离,每个WebappClassLoader加载自己目录的class文件和jar,并不会委托给父加载器,打破了双亲委派模
。
如果Tomcat使用的是Java默认的双亲委派模型,那么会有如下问题:
1.无法加载两个全限定名一样的类,实现不了多版本的class加载问题。
2.web容器和应用程序的类库容易混淆,不安全。