文章目录
- 1. 类加载机制
- 1.1. 类加载器与类的唯一性
- 1.2. 类加载器种类
- 1.3. 双亲委派模型
- 1.4. 类加载过程
- 1.4.1 加载
- 1.4.2 连接
- 1.4.2.1 验证
- 1.4.2.2 准备
- 1.4.2.3 解析
- 1.4.3 初始化
- 类初始化时机 ( jdk1.7)
- 1.4.4 自定义类加载器
- 2. 对象实例化
1. 类加载机制
1.1. 类加载器与类的唯一性
类加载器主要用于加载类,但除此之外还可用于确定类在Java虚拟机中的唯一性。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
通俗地说,要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相同”。这里指的“相同”,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法以及instanceof关键字等判断出来的结果
1.2. 类加载器种类
- 启动类加载器,
Bootstrap ClassLoader
,加载JACA_HOME\lib
,或者被-Xbootclasspath
参数限定的类 - 扩展类加载器,
Extension ClassLoader
,加载\lib\ext
,或者被java.ext.dirs
系统变量指定的类 - 应用程序类加载器,
Application ClassLoader
,加载环境变量ClassPath
中的类库 - 自定义类加载器,
通过继承ClassLoader实现
,一般是加载我们的自定义类
类加载器的源码实现可参考 Java 类加载器源码解析
1.3. 双亲委派模型
类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的,除了启动类加载器,每个类都有其父类加载器(父子关系由组合(不是继承)来实现)。
所谓双亲委派是指每次收到类加载请求时,先将请求委派给父类加载器完成(所有加载请求最终会委派到顶层的 Bootstrap ClassLoader
加载器中),如果父类加载器无法完成这个加载(该加载器的搜索范围中没有找到对应的类),子类才尝试自己加载。
双亲委派的优点是:
- 避免同一个类被多次加载
- 每个加载器只能加载自己范围内的类,防止Java核心类被恶意篡改的同名类覆盖
1.4. 类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)下图是一个类从加载到使用及卸载的全部生命周期
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始, 而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
1.4.1 加载
在加载阶段(参考java.lang.ClassLoader#loadClass()
方法),虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
- hotspot 在JDK1.7 及之前选择将Class对象存储在方法区中,JDK 1.8 类相关文件被移到了元空间中,Java虚拟机规范并没有明确要求一定要存储在方法区或堆区中
1.4.2 连接
1.4.2.1 验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段非常重要,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
1.4.2.2 准备
为类中的所有静态变量(static修饰) 和常量(static final修饰)分配内存空间,并为其设置一个初始值。静态变量的初始值为其数据类型的零值(例如 [int :0], [String:null]),常量初始值为其在代码中已经赋予的值。由于还没有产生对象,实例变量不在此操作范围内。
1.4.2.3 解析
将常量池中所有的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量),以便直接调用该方法
1.4.3 初始化
在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段则是根据代码中的逻辑去初始化类变量和其他资源,举个例子:
public static int value1 = 4;
public static int value2 = 5;
static{
value2 = 66;
}
在准备阶段 value1 = 0,value2 = 0, 在初始化阶段 value1 =4 ,value2 = 5
- 所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是
<clinit>
方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用 - 编译器收集的顺序是由语句在源文件中出现的顺序所决定的, 静态语句块中只能访问到定义在静态语句块之前的变量
- 如果超类没有被初始化,那么优先对超类初始化,但在< clinit >方法内不会显式调用超类的< clinit >方法,由JVM负责保证一个类的< clinit >方法执行之前,它的超类< clinit >方法已经被执行
- JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程阻塞等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程 (所以可以利用静态内部类实现线程安全的单例模式)
- 如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类< clinit >方法
类初始化时机 ( jdk1.7)
- 为一个类创建一个新的对象实例时(比如new、反射、序列化)
- 调用一个类的静态方法时(即在字节码中执行invokestatic指令)
- 调用一个类或接口的静态字段或者对这些静态字段执行赋值操作时(即在字节码中执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式。需注意: 对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
- 调用Java中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)
- 初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外)
- JVM启动包含main方法的启动类时
1.4.4 自定义类加载器
要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖它的 findClass(String name)方法即可,即指明如何获取类的字节码流。
- 要遵循双亲委派规范,重写
findClass
方法(自定义类加载逻辑),因为在该方法被调用前需要调用父类加载器的loadClass
方法,父类加载器未加载到相关类才会执行自定义类加载器的findClass
方法逻辑- 要破坏双亲委派的话,重写
loadClass
方法(双亲委派的具体逻辑实现)
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); // 1.首先判断类是否已经被加载
if (c == null) { // 2. 未被加载则进行双亲委派加载
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) { //3.父类加载器 loadClass 未加载到类才调用 findClass
// 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;
}
}
2. 对象实例化
Java程序在执行过程中,类,对象以及它们成员加载、初始化的顺序如下:
- 首先加载要创建对象的类及其直接与间接父类。
- 在类被加载的同时会将静态成员进行加载,主要包括静态成员变量的初始化,静态语句块的执行,在加载时按代码的先后顺序进行。
- 需要的类加载完成后,开始创建对象,首先会加载非静态的成员,主要包括非静态成员变量的初始化,非静态语句块的执行,在加载时按代码的先后顺序进行。
- 最后执行构造器,构造器执行完毕,对象生成。
所以 java 对象实例化时的顺序为:
1. 父类的静态成员变量初始化和静态代码块执行
2. 子类的静态成员变量初始化和静态代码块执行
3. 父类成员变量初始化和方法块执行
4. 父类的构造函数执行
5. 子类成员变量初始化和方法块执行
6. 子类的构造函数执行