类是在运行期间第一次使用
时动态加载
的,而不是一次性
加载所有类。因为如果一次性加载,那么会占用很多的内存。
包含了加载、验证、准备、解析和初始化这 5 个阶段。
加载是类加载的一个阶段,加载过程完成以下三件事:
类
的完全限定名称
获取定义该类的二进制字节流
。静态存储结构
转换为方法区的运行时存储结构
。其中二进制字节流可以从以下方式中获取:
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
类变量 Class Variable
是被static
修饰的变量,准备阶段为类变量 Class Variable
分配内存并设置初始值,使用的是方法区
的内存。实例变量 Object Variable
不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆
中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前
,并且类加载只进行一次
,实例化可以进行多次
。
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
如果类变量
是常量,那么它将初始化为表达式所定义
的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。
public static final int value = 123;
将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
初始化阶段才真正开始执行
类中定义的 Java 程序代码
。初始化阶段是虚拟机执行类构造器
方法的过程。在 准备
阶段,类变量已经赋过一次系统要求的初始值
,而在初始化
阶段,根据程序员通过程序制定的主观计划去初始化 类变量
和其它资源
。
是由编译器
自动收集
类中所有类变量
的赋值动作
和静态语句块
中的语句合并产生的,编译器收
集的顺序由 语句在源文件中出现的顺序
决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类 变量只能赋值,不能访问。
例如以下代码:
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System. out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
由于父类的
方法先执行,也就意味着父类
中定义的静态语句块
的执行要优先于子类
。例如以下代码:
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
接口Interface
中不可以使用静态语句块
,但仍然有类变量Class Variable
初始化的赋值操作,因此接口Interface
与类Class
一样都会生成
方法。但 接口Interface
与类Class
不同的是,执行接口的
方法不需要
先执行父接口 Super Interface
的
方法。只有当父接口
中定义的变量
使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的
方法。
虚拟机会保证一个类的
方法在多线程环境下被正确
的加锁
和同步
,如果多个线程同时初始化一个类,只会 有一个线程
执行这个类的
方法,其它线程都会阻塞等待
,直到活动线程执行
方法完毕。如果在一 个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
两个类相等,需要类本身相等,并且使用同一个类加载器
进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的Class 对象
的 equals() 方法
、isAssignableFrom() 方法
、isInstance() 方法
的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
启动类加载器(Bootstrap ClassLoader
)此类加载器负责将存放在
目录中的,或者被 - Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的 类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用, 用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
扩展类加载器(Extension ClassLoader
)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)
实现的。它负责将
或者被java.ext.dir
系统变量所指定路径中
的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)
实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器
。它负责加载用户类路径(ClassPath)
上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情 况下这个就是程序中默认的类加载器
。
应用程序是由三种类加载器
互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器
。
下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)
。该模型要求除了顶层的启动类加载器
外,其它的类加载器都要有自己的父类加载器
。这里的父子关系一般通过组合关系(Composition)
来实现,而不是继承关系(Inheritance)
。
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
例如java.lang.Object
存放在 rt.jar 中,如果编写另外一个 java.lang.Object
并放到 ClassPath
中,程序可以编译通 过。由于双亲委派模型的存在,所以在 rt.jar
中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器
,而 ClassPath 中的 Object 使用的是应用程序类加载器
。rt.jar 中的 Object 优 先级更高,那么程序中所有的 Object 都是这个 Object。
以下是抽象类 java.lang.ClassLoader
的代码片段,其中的 loadClass()
方法运行过程如下:
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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) {
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.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}