1 概述
在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制
在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这为Java应用程序提供高度的灵活性
Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的
2 类加载的时机
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
有且只有5种情况必须立即对类进行“初始化”【主动引用】
- 1、遇到
new(使用new关键字实例化对象的时候)
、getstatic(读取一个类的静态字段)
、putstatic(设置一个类的静态字段)
或invokestatic(调用一个类的静态方法的时候)
这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。 - 2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
- 3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化【对于接口无此约束】
- 4、当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()方法
的那个类),虚拟机会先初始化这个主类。 - 5、当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
除此之外,所有引用类的方式都不会触发初始化,称为被动引用
被动引用例子1
/**
* 被动使用类字段演示一:
* 通过子类引用父类的静态字段,不会导致子类初始化
**/
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
运行结果
结果分析:
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。但是在Sun HotSpot虚拟机中,通过加入-XX:+TraceClassLoading
后发现,SubClass
也是被load进来的,只是没有被初始化。
被动引用例子2
/**
* 被动使用类字段演示二:
* 通过数组定义来引用类,不会触发此类的初始化
**/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
运行之后发现没有输出“SuperClass init!”,说明并没有触发类org.fenixsoft.classloading.SuperClass的初始化阶段。但是这段代码里面触发了另外一个名为
[Lorg.fenixsoft.classloading.SuperClass
的类的初始化
阶段,对于用户代码来说,这并不是一个合法的类名称,它是一个由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。这个类代表了一个元素类型为org.fenixsoft.classloading.SuperClass的一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为public的length属性和clone()方法)都实现在这个类里
被动引用例子3
/**
* 被动使用类字段演示三:
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
**/
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
上述代码运行之后,也没有输出“ConstClass init!”,这是因为在编译阶段通过
常量传播优化
,已经将此常量的值“hello world”存储到了NotInitialization类的常量池中,以后NotInitialization对常量ConstClass.HELLOWORLD的引用实际都被转化为NotInitialization类对自身常量池的引用了。
也就是说,实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。
3 类加载的过程
3.1 加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
3.2 验证
验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证
3.3 准备
- 准备阶段是正式为
类变量
分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配 - 这时候进行内存分配的仅包括
类变量(被static修饰的变量)
,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。 - 这里所说的初始值“通常情况”下是
数据类型的零值
3.4 解析
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
3.5 初始化
- 在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行
类构造器<clinit>()
方法的过程 -
<clinit>()方法
是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生 - <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕
4 类加载器
4.1 类与类加载器
- 类和类加载器唯一确定一个类
package study7;
import java.io.IOException;
import java.io.InputStream;
/**
* Created by haicheng.lhc on 31/03/2017.
*
* @author haicheng.lhc
* @date 2017/03/31
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("study7.ClassLoaderTest").newInstance();
System.out.println(obj.getClass().getClassLoader());
System.out.println(study7.ClassLoaderTest.class.getClassLoader());
System.out.println(obj instanceof study7.ClassLoaderTest);
}
}
运行结果
4.2 双亲委派模型
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现[1],是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类
java.lang.ClassLoader
。
从Java开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java程序都会使用到以下3种系统提供的类加载器
-
启动类加载器(Bootstrap ClassLoader)
:前面已经介绍过,这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。 -
扩展类加载器(Extension ClassLoader)
:这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器 -
应用程序类加载器(Application ClassLoader)
:这个类加载器由sun.misc.Launcher $AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器
。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。