程序的class文件是程序编译的产物,
虚拟机把描述类的数据从Class文件加载到内存中,并进行数据校验,转换解析和初始化,最终形成可以直接被虚拟机使用的Java类型------这叫虚拟机的类加载机制。
一些语言在编译的时候需要进行连接工作,但是Java中,类型的加载,连接和初始化都是在程序运行期间做的。
类的生命周期
类从被加载到虚拟机开始到卸载出内存为止,整个生命周期:
加载,(验证,准备,解析),初始化,使用,卸载。其中验证,准备,解析叫做连接。
这个七个阶段除了解析之外其他的阶段都是按照这个顺序执行的而解析在某些情况下是可以在初始化之后开始。
类加载的过程:
加载,验证,准备,解析,初始化
解析的执行时间可能在初始化之前也可能在初始化之后
PART ONE:类的加载
1.加载★
弄清楚概念:加载是类加载过程的一个阶段
干什么:
- 通过一个类的全限定名来
获取定义此类的而进制字节流
- 将这个字节流代表的静态存储结构转化为方法去的
运行时数据结构
,将class文件中的数据加载到方法区的运行时常量池 - 在内存中
生成一个代表这个类的java.lang.Class 对象
。作为方法区这个类各种数据访问的入口
关于加载的时机,JVM对此并没有强制性的限制,同时对于如何获取二进制字节流也没有明确的规定
细节:
- 对于一个非数组类的加载阶段(准确的是获取二进制字节流的阶段),
可以使用系统提供的引导类加载器完成,也可以使用自定义的类加载器完成。
- 数组类本身不是通过类加载器创建的,是有JVM 直接创建的。但是数组类仍然和类加载器联系紧密
1. 数组类的组建类型是引用类型,还是要使用类加载器去加载组件类型
2. 如果数组的组件类型是一般数据类型(int[]),JVM 会把该数组标记为与引导类加载器相关联
3. 数组类和组件类的可见性是一致的
2.验证
为了确保Class文件的字节流包含的信息符合当前虚拟机的要求(Class文件不一定由Java源码编译而来,所以要进行检查)
- JVM规范校验。JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe babe开头,主次版本号是否在当前虚拟机处理范围之内等。
- 代码逻辑校验。JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类。
当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。
3.准备★
正式为类变量分配内存(分配在方法去中/运行时常量池)并设置类变量初始值
类变量:被
staitic
修饰(没有被final 修饰)
实例变量:在对象实例化的时候随对象一起分配到Java堆中
public static int a = 123;
public static final int b =1245;
/**
*准备阶段之后a=0;b=1245
*/
4.解析
在解析阶段,JVM针对类或者接口,字段,方法,类方法,接口方法,方法类型,方法句柄和调用下宁府7类引用进行解析。将常量池中的符号引用替换成其在内存中的直接内存
解析阶段是虚拟机将常量池内的符号引用(引用的目标不一定已经加载到内存)替换为直接引用(引用的目标一定已经在内存中)的过程
1.符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字>面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。
2.直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
字段,类方法,接口方法都是要先解析所属的类,如果类中有相符合的内容则直接返回直接引用,如果不存在则按照实现或者继承从下至上搜索,找到之后返回直接引用。
找到符号引用在内存中的直接引用,class 文件已经加载到内存中,要找到这些符号引用在内存中的位置
5.初始化★★
干什么:
类加载的几个阶段,除了加载阶段可以有自定义的类加载器参与,其他阶段都由JVM主导控制。到了初始化阶段,才真正的执行另类中定义的Java程序代码, 会根据语句执行顺序对类进行初始化
在准备阶段,JVM已经为变量(static)赋过一次系统希望的初始值,而在初始化阶段,则是根据程序员制定的程序去初始化变量和其他资源
public static int a=123;
public int b ;
/*在准备阶段a=0;
*初始化之后 a=123;
*初始化之后 b = 0;
*/
换一个角度:类的初始化过程就是执行类构造器< clinit >() 方法的过程
类构造器< clinit >() 方法 :是由编译器自动收集类中所有类变量的所有赋值动作和静态语句块合并产生
静态语句只能访问定义在静态语句块之前的静态变量,声明在静态语句块之后的静态变量,静态语句块只能赋值,不能访问
jpublic class Test{ static{ i =0; } static int i=1;}
这样的代码书写就是错误的
分清楚< clinit >() 是类的构造器与类的构造函数(实例构造器< init >())是不一样的,
当前类在执行< clinit >(),默认父类已经执行完了
< clinit >不是必须的,没有静态代码块和对类变量的赋值语句也行
时机:
虚拟机对什么情况下进行类的初始化是有严格规定的
- 使用new ,getstatic,putstatic,invokrstatic 这几个字节码指令的时候,如果该类没有进行初始化,那么就要先进行初始化。当new 一个该类对象,设置或者获取该类的静态字段(除了final 修饰的字段),以及调用一个类的静态方法的时候
- 使用java.lang.reflect 的方法对类进行反射操作的时候
- 当初始化一个类时,如果他的父类没有初始化那么要先初始化他的父类
- 当虚拟机启动的时候,用户指定的一个要执行的类(包含main() 方法的类)虚拟机会先初始化这个类。
有且只有出现这几种情况的时候会初始化类,其他任何方式都不行
例如:
- 通过子类访问父类的静态字段,不会初始化子类,只会初始化父类。
- 通过数组定义来引用该类,不会触发该类的初始化。
people[] peoples = new people[10];
/**
*这样并不能触发people这个类的初始化,而会触发[people 这个类的初始化,这是有虚拟机自己生成的类继承于Object,
*这个类*代表了一个元素类型为people 的数组,数组使用的clone /length 方法都定义在里面。这是Java中对于数组访问更
*加安全的访问模式的实现---封装
*/
- 常量(final)在编译阶段就会存入调用类的常量池中,所以本质上并没有直接引用到定义常量的类,因此不会触发定义常量类的初始化
- 关于接口:
接口和类大致是一样的,但是对于第三条,一个类在初始化时要求他的父类都已完成初始化,但是对于接口并不这样要求;当接口初始化时,他的接口可以不用初始化,当使用接口的时候再初始化也行。
PART TWO:类加载器
在加载阶段“通过一个类的全限定名来获取描述此类的二进制字节流”。实现这个过程的模块代码叫类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果
JVM中的类加载器体制
JVM中只有两种不同的类加载器:
- 启动类加载器:是有C++编写,是虚拟机的一部分
- 其他类加载器:由Java语言实现,独立于虚拟机外部都继承Java.lang.ClassLoader
体制结构:
- 启动类加载器(BootStrap ClassLoader): 启动ClassLoader< JAVA_HOME>\lib 目录,-Xbootclasspath,rt.jar 类库到内存中
- 扩展类加载器(Extension ClassLoader):扩展ClassLoader < JAVA_HOME>\lib\ext 目录
- 应用类加载器(Application ClassLoader):应用ClassLoader
- 用户类加载器(User ClassLoader):自定义加载器ClassLoader
双亲委派模式
除了顶层的BootStrap Class Loader 其他类加载器都有自己的父类加载器,这里的父子关系不是通过继承实现的而是通过组合关系来复用父加载器中的代码。
双亲委派的工作模式:
当一个类加载器收到了加载请求他首先不会自己去尝试加载,而是 会把这个加载请求委派给父类加载器去完成,所以最后所有的请求都会传到启动类加载器中,当父类返回自己无法完成加载时(他的搜索范围内没有找到所需类),子类才会自己去尝试完成加载。
双亲委派模式的好处
Java类随类加载器有一个很好的层级关系,例如:加载java.lang.Object 因为加载请求都会传递给启动加载器,因此就保证类始终只有一个java.lang.Object ,不会因为不同的类加载器加载这个类就会出现多个java.lang.Object ,这样程序就乱套了。
/*
*双亲委派模式的实现代码:java.lang.ClassLoader 的loadClass 方法*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果父类加载器中没有加载这个类
// 再调用本类的加载器加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}