概述
虚拟机把描述类的数据从Class文件加载到内存,对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,此为虚拟机的类加载机制。
一、类加载的时机
- 类的整个声明周期:加载、连接(验证、准备、解析)、初始化、使用、卸载。
- 有且只有五种情况下类需要初始化(加载时间并没有明确限制)
- 遇到 new、getstatic、putstatic、invokestatic这四条字节码指令时;
- 使用反射包的方法对类进行反射调用时;
- 初始化一个类时,如果父类没有初始化,则会先初始化父类;
- 虚拟机启动时,虚拟机会初始化指定的启动类(包含main()方法的那个类);
- 动态语言支持(暂时省略)
- 被动引用的例子,上面五种为主动引用(能促发类的初始化)。
- 通过类名访问父类的静态变量 SubClass.value;(只有直接定义静态字段的类才会被初始化),是否导致类的加载可以通过(-XX:TraceClassLoading )参数查看;
- 通过数组定义来引用类;如 Person[] ps = new Person[10];这里不会促发Person类的初始化,但是会有一个名为"[Lcom.hfi.pepper.Person"的类的初始化(由虚拟机生成:newarray)
- 对常量字段的引用(final、static修饰);如在B类中访问A类的常量value(A.value),这其实在编译器已经被常量传播优化了--转化为对类B自身常量池的访问,所以直接从常量池拿值,不需到类A拿值。
- 接口初始化时,并不要求其父接口都初始化。
二、类加载的过程
2.1 加载
- 通过一个类的权限定名获取定义此类的二进制字节流(JAR包,网络,动态代理),如:
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
- 将二进制流代表的静态结构转化为方法区的运行时数据结构
- 内存中生成代表这个类的Class对象(
存在于方法区1.7
)。 - 数组类型的加载是将其 组件类型 按上述方式加载。
2.2验证
- 文件格式验证(魔数、版本号、常量类型等),只有这个阶段是基于二进制流验证的。
- 元数据验证 保证不存在不符合JAVA语言规范的元数据信息。
- 字节码验证 通过数据流和控制流分析 确保程序语义合法、逻辑合法。
- 符号引用验证 (解析阶段发生)
- Xverify:none 关闭类验证,以缩短类加载时间。
2.3准备
为类变量分配空间,并设置初始值。通常初始值为零,除了常量:被final和static修饰的(编译时 通过ConstantValue属性设值)。
2.4解析阶段
符号引用替换为直接引用的过程
- 解析发生的时间--在使用操作符号引用的字节码指令之前,先对符号引用解析。
- 符号引用应该只解析一次,解析之后的直接引用应该缓冲在运行时常量池中。
1、类或者接口的解析
2、字段解析
3、类方法解析
4、接口方法解析
2.5初始化
- 准备阶段,变量已经赋过一次系统要求的初始值(除常量外都为零值
- 初始化阶段,可以理解为执行类构造器 <clinit>()方法的过程。
- <clinit>()方法的生成,是编译器将源文件中对 类变量的赋值和静态代码块中的语句进行收集(按其出现的顺序)。
- 静态语句块可以赋值定义在其后的类变量(注意理解),但是不能访问定义在其后的类变量(why?)
- <clinit>()不需要显示调用父类的类构造器,so第一个执行的类构造器属于Object类。
- <clinit>()是非必须的(如果没有静态代码块,和类变量的赋值动作)
- 接口中虽没有静态代码块,但是可以有变量的赋值,所以可以有类构造器
- 执行接口的<clinit> 不需要先执行父接口的构造器。只有当父接口中变量使用时,父接口才会初始化。
- 接口的实现类在初始化时,也不必执行接口的初始化。
- 虚拟机会保证一个类的<clinit>()方法的正确同步,这个原理可以被用来实现单例。
三、类加载器
3.1 类与类加载器
类的唯一性由加载他的 类加载器和 这个类本身一同确定
3.2 双亲委派模型
- 启动类加载器(Bootstrap ClassLoader),使用C++实现,加载<JAVA_HOME>\lib目录中,或者被-Xbootclasspath参数指定的路劲中的文件(按名字识别,如rt.jar)
- 启动类加载器无法被Java程序直接引用,如果需要把加载请求委托给启动类加载器,则直接使用null代替。
- 扩展类加载器,负责加载<JAVA_HOME>\lib\ext目录中的文件,ExtClassLoader可以被开发者直接使用。
- 应用程序加载器(AppClassLoader),由ClassLoader中的getSystemClassLoader方法返回,
负责加载用户类路劲(classpath)上的所有类库
。 - 工作流程: 类加载器收到类加载请求时,首先把请求委托给父类(这里用的组合的方式),只有父类无法完成类加载时,才自己去加载类。
- 双亲委派模型这种层级关系可以保证如Object类只被引导类加载。
- 双亲委派模型代码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
网友评论