jvm9-类加载机制
概述
-
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制
-
相当于懒加载机制
OSGI 就是通过类的加载机制实现的
类加载时机
加载:
连接:
初始化:
在以下情况才会进行初始化
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先出发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字是梨花对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类还没呕进行过初始化,则需要先触发其父类的初始化
4、当虚拟机启动时,用户需要指定一个需执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5、jdk7开始支持动态语言,
不被初始化的例子
1、通过子类引用父类的静态字段,子类不会被初始化
2、通过数组定义来引用类
3、调用类的常量
使用:
卸载:
类的加载
加载和连接的过程是并行的
加载
- 通过一个类的全限定名来获取定义此类的二进制流
加载源:可以从
文件(class文件,jar文件),
网络(?),
计算生成一个二进制流(代理模式-proxy),
其他文件生成,比如Jsp文件,
数据库
等加载
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
hotspot会把加载的class对象存放到方法区中
- 在内存中生成一个代表这个类的Class对象,作为这个类的各种数据的访问入口
连接
验证
- 验证是连接的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
主要校验以下4大类
-
文件格式验证
-
元数据验证
-
字节码验证
-
符号引用验证:主要为了确保解析环节正确
(nosuchmetch等异常就是这里抛出的)
准备
-
准备阶段正式为类变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配
-
这里的初始值并非我们指定的值,而是其默认值,但是如果被final修饰,那么在这个过程中,常量值会被一同指定
int 0
boolean false
float 0.0
char '0'
抽象数据类型 null
class hello{
// 准备阶段会把a初始化为默认的值0
public static int a = 10;
}
解析
解析阶段是将常量池中的符号引用替换为直接引用的过程。在进行解析之前需要对符号引用进行解析,不同虚拟机实现可以根据需要判断到底是在类被加载器加载的时候对常量池的符号引用进行解析(也就是初始化之前),还是等到一个符号引用被使用之前进行解析(也就是在初始化之后)。
到现在我们已经明白解析阶段的时机,那么还有一个问题是:如果一个符号引用进行多次解析请求,虚拟机中除了invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录引用,并把常量标识为解析状态),这样就避免了一个符号引用的多次解析
解析动作主要针对的是类或者接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用。这里主要说明前4种的解析过程
类或者接口解析
要把一个类或者接口的符号引用解析为直接引用,需要以下三个步骤
1、如果该符号引用不是一个数组类型,那么虚拟机将会把该符号代表的全限定名称传递给类加载器区加载这个类。这个过程由于涉及验证过程所以可能会触发其他相关类的加载。
2、如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似“java/lang/Integer”的形式,将会按照上面的规则进行加载数组元素类型,如果描述符如前面假设的形式,需要加载的元素类型就是java.lang.Integer,接着虚拟机将会生成一个代表此数组对象的直接引用。
3、如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.IllegalAccess异常。
字段解析
对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确解析得到其类的正确的直接引用才能继续对字段的解析。对字段的解析主要包括以下几个步骤
1、如果该字段符号引用就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,解析结束
2、否则,如果在该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么就直接返回这个字段的直接引用,解析结束
3、否则,如果该符号所在的类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都相匹配的字段,那么直接返回这个字段的直接引用,解析结束
4、否则,解析失败,抛出java.lang.NoSuchFieldError异常
如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常
类方法解析
进行类方法的解析仍然需要先解析此类,在正确解析之后需要进行如下的步骤:
1、类方法和接口方法的符号引用是分开的,所以如果在类方法表中发现class_index(类中方法的符号引用)的索引是一个接口,那么会抛出java.lang.IncompatibleClassChangeError的异常
2、如果class_index的索引确实是一个类,那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法,如果有的话就返回这个方法的直接引用,查找结束
3、否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的字段,如果有,则直接返回这个字段的直接引用,查找结束
4、否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象类,查找结束,返回java.lang.AbstractMethmodError异常
5、否则,查找失败,抛出java.lang.NoSuchMethodError异常
如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError异常
接口方法解析
同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:
1、如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么也会抛出java.lang.IncompatibleClassChangeError的异常
2、否则在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用
3、否则,在该接口以及其父接口中查找,直到Object类,如果找到这直接返回这个方法的直接引用
4、否则,查找失败
接口的所有方法都是public,所以不存在访问权限问题,不需要进行权限验证
初始化(面试题比较多需要关注)
类初始化是类加载的最后一步,前面类加载的过程中除了在加载阶段用户应用程序可以通过自定义类加载器参与以外,其余动作完全由虚拟机主导与控制。到了初始化阶段,才是真正执行类中定义的java程序代码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据开发者通过程序控制制定的主观计划去初始化类变量和其他资源(初始化阶段是执行类构造器
先来看一段代码
public class Demo{
static{
i = 0;
// 编译报错:illegal forward reference
System.out.println(i);
}
static int i = 1;
}
上面这段代码变量的赋值语句可以通过编译,而下面的输出确编译不通过,这是为什呢?
再来看一段代码
public class Parent{
public static int A =1;
static {
A = 2;
}
static class Sub extents Parent{
public static int B = A;
}
public static void main(){
System.out.println(Sub.B);
}
}
-
子类的
()在执行之前,虚拟机保证父类的先执行完毕,因此在赋值前父类static已经执行,因此结果为2。 -
接口中也有变量要赋值,也会生成
()但不需要先执行父类的 ()方法。只有父接口中定义的变量使用时才会初始化。 -
如果多个线程同时初始化一个类,只有一个线程执行这个类的
()方法,其他线程等待执行完毕。如果方法执行时间过长,那么就会造成多个线程阻塞 public class DemoThread { static class Hello{ static{ System.out.println(Thread.currentThread().getName() + "init ..."); try{ Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args){ new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "start ..."); Hello he = new Hello(); System.out.println(Thread.currentThread().getName() + " end ..."); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "start ..."); Hello he = new Hello(); System.out.println(Thread.currentThread().getName() + " end ..."); } }).start(); } }
输出结果:
Thread-0start ... Thread-1start ... Thread-0init ... Thread-0 end ... Thread-1 end ...
类加载器
- 虚拟机的设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为类加载器。
- 只有被同一个类加载器加载的类才可能会相等。相同的字节码被不同的类加载器加载的类不相等
类加载器分类
从虚拟机的角度只分为启动类加载器和其它加载器
从使用者角度分为:
- 启动类加载器
由c++实现,是虚拟机的一部分,用于加载javahome下的lib目录下的类 - 扩展类加载器
加载javahome下/lib/ext目录中的类 - 应用程序类加载器(系统类加载器)
加载用户类路径上的所指定的类库 - 自定义类加载器
自定义类加载器
- 定义一个类,继承ClassLoader
- 重写loadClass方法
- 实例化Class对象
自定义类加载器的优势
- 类加载器是java语言的一项创新,也是java语言流行的重要原因之一,它最初的设计是为了满足java Applet的需求而开发出来的。
- 高度的灵活性
- 通过自定义类加载器可以实现热部署
- 代码加密
双亲委派模型
- 从JDK1.2开始,java虚拟机规范推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:
1、如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
2、每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器。
3、如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。 - 双亲委派模式的类加载机制的优点是java类它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。
双亲委派模型的实现
ClassLoad.loadClass(String name, boolean resolve)