1 类加载机制概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
懒加载:java虚拟机加载策略是懒加载的.(用的时候再加载)
相对应的是即时加载:
2 类加载时机
2.1 类的生命周期
- 1.加载(Loading)
- 2.连接(Linking)
-
- 2.1 验证(Verification)
-
- 2.2 准备(Preparation)
-
- 2.3 解析(Resolution)
- 3.初始化(Initialization)
- 4.使用(Using)
- 5.卸载(Unloading)
加载过程指:加载,验证,准备,解析,初始化.
2.2 初始化时机
- 1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。 - 2.使用Java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 5.JDK7之后,动态语法时有时机创建.
package classload.loadtime;
public class Parent {
static {
System.out.println("parent init");
}
}
package classload.loadtime;
public class Child extends Parent{
static {
System.out.println("child init");
}
public static void main(String[] args) {
}
}
执行结果:
parent init
child init
2.3 不初始化的时机
- 通过子类引用父类的静态字段(child.parentstatic),父类会初始化但子类不会被初始化,
- 通过数组定义来引用类,引用类不会被初始化
- 调用类的常量
//证明 通过子类引用父类的静态字段,父类会初始化但子类不会被初始化
package classload.loadtime;
public class Parent2 {
static {
System.out.println("parent2 init");
}
public static int num = 10;
}
package classload.loadtime;
public class Child2 extends Parent2{
static {
System.out.println("child2 init");
}
}
package classload.loadtime;
public class Test2 {
public static void main(String[] args) {
System.out.println(""+Child2.num);
}
}
执行结果:
parent2 init
10
//通过数组定义来引用类
package classload.loadtime;
public class Parent3 {
static {
System.out.println("parent3 init");
}
}
package classload.loadtime;
public class Child3 extends Parent3{
static {
System.out.println("child3 init");
}
}
package classload.loadtime;
public class Test3 {
public static void main(String[] args) {
Child3 [] child3s = new Child3[10];
}
}
执行结果
Process finished with exit code 0
//调用类的常量
package classload.loadtime;
public class Parent4 {
static {
System.out.println("parent4 init");
}
}
package classload.loadtime;
public class Child4 extends Parent4{
static {
System.out.println("child4 init");
}
public static final int NUM = 10;
}
package classload.loadtime;
public class Test4 {
public static void main(String[] args) {
System.out.println("Child4.NUM->"+Child4.NUM);
}
}
Child4.NUM->10
Process finished with exit code 0
3 类加载的过程-加载
3.1 加载步骤
-
1.通过一个类的全限定名来获取定义此类的二进制流
-
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
如spring的配置文件(XML中的Bean). -
3.在内存中生成一个代表这个类的Class 对象,作为这个类的各种数据的访问入口(Class对象放在方法区中)
-
注意:加载和连接是有平行同时执行的,在加载的过程中就开始验证了
3.2 加载源
- 文件
-
- Class文件
-
- Jar 文件,WAR,
- 网络 Applet
- 计算生成一个二进制流
-
- $Proxy(反射包中java.lang.reflect)
- 由其他文件生成
-
- Jsp(转成servlet)
- 数据库
4 类加载的过程-验证
-
验证是连接的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
-
文件格式验证(魔数,主次版本号,常量池中常量是否都支持,指向常量的索引值指向是否正确…等等)
-
元数据验证(是否有父类,是否继承了不允许被继承的类[final类],不是抽象类是否实现了父类/接口中所有的抽象方法)
-
字节码验证(语义是否合法,符合逻辑,对方法体校验分析,确保类的方法在运行时不会威海虚拟机安全)
-
符号引用验证(常量池的符号引用,类/方法/字段/访问性[private,protected,public,default]是否可被当前类访问)
-
注意:虚拟机可以配置参数跳过验证这一步.
5 类加载的过程-准备
5.1 概述
- 准备阶段正式为类变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配。
- 这里的初始值并非我们指定的值,而是其默认值.
但是如果被final修饰,那么在这个过程中,常量值会被一同指定。 - 变量初始值:
变量类型 | 初始值 |
---|---|
Int | 0 |
Boolean | false |
Float | 0.0 |
Char | ‘0’ |
抽象数据类型 |
class hello {
// 0,此时初始化的值为0
public static int a = 10;
}
6 类加载的过程-解析
6.1 概述
- 解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
- 类或者接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
解析阶段是将常量池中的符号引用替换为直接引用的过程。在进行解析之前需要对符号引用进行解析,不同虚拟机实现可以根据需要判断到底是在类被加载器加载的时候对常量池的符号引用进行解析(也就是初始化之前),还是等到一个符号引用被使用之前进行解析(也就是在初始化之后)。
到现在我们已经明白解析阶段的时机,那么还有一个问题是:如果一个符号引用进行多次解析请求,虚拟机中除了invokedynamic(JDK7动态语言Lambda表达式)指令外,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录引用,并把常量标识为一解析状态),这样就避免了一个符号引用的多次解析。
解析动作主要针对的是类或者接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用。这里主要说明前四种的解析过程。
6.2 解析详解
6.2.1 类或者接口解析
要把一个类或者接口的符号引用解析为直接引用,需要以下三个步骤:
1. 如果该符号引用不是一个数组类型,那么虚拟机将会把该符号代表的全限定名称传递给类加载器去加载这个类。这个过程由于涉及验证过程所以可能会触发其他相关类的加载
2. 如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似”[java/lang/Integer”的形式,将会按照上面的规则进行加载数组元素类型,如果描述符如前面假设的形式,需要加载的元素类型就是java.lang.Integer ,接着由虚拟机将会生成一个代表此数组对象的直接引用
3. 如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.lang.IllegalAccess异常
6.2.2 字段解析
对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确解析得到其类的正确的直接引用才能继续对字段的解析。对字段的解析主要包括以下几个步骤:
1. 如果该字段符号引用就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,解析结束
2. 否则,如果在该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么久直接返回这个字段的直接引用,解析结束
3. 否则,如果该符号所在的类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都相匹配的字段,那么直接返回这个字段的直接引用,解析结束
4. 否则,解析失败,抛出java.lang.NoSuchFieldError异常
如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常
字段可以在类,父类和接口中查找.
6.2.3 类方法解析
进行类方法的解析仍然需要先解析此类方法的类,在正确解析之后需要进行如下的步骤:
1. 类方法和接口方法的符号引用是分开的,所以如果在类方法表中发现class_index(类中方法的符号引用)的索引是一个接口,那么会抛出java.lang.IncompatibleClassChangeError的异常
2. 如果class_index的索引确实是一个类,那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法,如果有的话就返回这个方法的直接引用,查找结束
3. 否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的字段,如果有,则直接返回这个字段的直接引用,查找结束
4. 否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象类,查找结束,返回java.lang.AbstractMethodError异常
5. 否则,查找失败,抛出java.lang.NoSuchMethodError异常
如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError异常
类方法只能在类和父类中查找.
类方法和接口方法的符号引用是分开的
6.2.4 接口方法解析
同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:
1. 如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么也会抛出java.lang.IncompatibleClassChangeError的异常
2. 否则,在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用。
3. 否则,在该接口以及其父接口中查找,直到Object类,如果找到则直接返回这个方法的直接引用
4. 否则,查找失败
接口的所有方法都是public,所以不存在访问权限问题
7 类加载的过程-初始化
类初始化阶段是类加载过程的最后一步,前面类加载的过程中除了在加载阶段用户应用程序可以通过自定义类加载器参与以外,其余动作完全由虚拟机主导与控制。到了初始化阶段,才是真正执行类中定义的Java程序代码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据开发者通过程序控制制定的主观计划去初始化类变量和其他资源。
初始化阶段是执行类构造器()方法的过程。
- 解读1
public class Demo {
static {
i = 0;
System.out.println(i);
}
static int i = 1;
}
//编译报错: Error:(6, 28) java: 非法前向引用
//可以赋值,不能引用,所以报错在System.out.println(i);而不是i=0;
上面这段代码变量的赋值语句(i = 0;)可以通过编译,而下面的输出(system.out)却编译不通过。这是为什么呢?
- <clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的语句块中可以赋值,但是不能访问。
- 解读2
public 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
-
子类的<clinit>()在执行之前,虚拟机保证父类的先执行完毕,因此在赋值前父类static已经执行,因此结果为2
-
接口中也有变量要赋值,也会生成<clinit>(),但不需要先执行父类的<clinit>()方法。只有父接口中定义的变量使用时才会初始化。
-
如果多个线程同时初始化一个类,只有一个线程会执行这个类的<clinit>()方法,多线程有线程安全问题,如果没有防御机制,则会有线程阻塞,实际上说明是线程安全的.虚拟机自己也会有锁机制,不会出现线程阻塞.(如下DemoThread)
-
如果多个线程同时初始化一个类,只有一个线程会执行这个类的()方法,其他线程等待()执行完毕。如果方法执行时间过长,那么就会造成多个线程阻塞(如下DemoThread2)。
- DemoThread
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DemoThread {
static class Hello {
static {
System.out.println(Thread.currentThread().getName() + " init ....");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(20);
for(int i=0;i<20;i++){
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " start ..");
Hello dt = new Hello();
System.out.println(Thread.currentThread().getName() + " end ...");
}
});
}
//多个线程一起执行,只有一个线程来执行<cinit>()方法,多线程有线程安全问题,如果没有防御机制,则会有线程阻塞,实际上说明<cinit>是线程安全的.虚拟机自己也会有锁机制,不会出现线程阻塞.(如DemoThread)
//如果多个线程同时初始化一个类,只有一个线程会执行这个类的<clinit>()方法,其他线程等待<cinit>()执行完毕。如果方法执行时间过长,那么就会造成多个线程阻塞(DemoThread2)。
}
/**输出结果:
pool-1-thread-1 start ..
pool-1-thread-2 start ..
pool-1-thread-1 init ....
pool-1-thread-4 start ..
pool-1-thread-6 start ..
pool-1-thread-8 start ..
pool-1-thread-10 start ..
pool-1-thread-12 start ..
pool-1-thread-13 start ..
pool-1-thread-17 start ..
pool-1-thread-14 start ..
pool-1-thread-18 start ..
pool-1-thread-16 start ..
pool-1-thread-3 start ..
pool-1-thread-7 start ..
pool-1-thread-5 start ..
pool-1-thread-9 start ..
pool-1-thread-20 start ..
pool-1-thread-11 start ..
pool-1-thread-15 start ..
pool-1-thread-19 start ..
pool-1-thread-1 end ...
pool-1-thread-19 end ...
pool-1-thread-15 end ...
pool-1-thread-11 end ...
pool-1-thread-20 end ...
pool-1-thread-9 end ...
pool-1-thread-5 end ...
pool-1-thread-3 end ...
pool-1-thread-7 end ...
pool-1-thread-16 end ...
pool-1-thread-18 end ...
pool-1-thread-14 end ...
pool-1-thread-17 end ...
pool-1-thread-13 end ...
pool-1-thread-12 end ...
pool-1-thread-10 end ...
pool-1-thread-8 end ...
pool-1-thread-6 end ...
pool-1-thread-4 end ...
pool-1-thread-2 end ...
*
*/
}
- DemoThread2
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DemoThread2 {
static class Hello {
static {
if(true)
System.out.println(Thread.currentThread().getName() + " init ....");
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(20);
for(int i=0;i<30;i++){
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " start ..");
Hello dt = new Hello();
System.out.println(Thread.currentThread().getName() + " end ...");
}
});
}
//多个线程一起执行,只有一个线程来执行<cinit>()方法,多线程有线程安全问题,如果没有防御机制,则会有线程阻塞,实际上说明<cinit>是线程安全的.虚拟机自己也会有锁机制,不会出现线程阻塞.(如DemoThread)
//如果多个线程同时初始化一个类,只有一个线程会执行这个类的<clinit>()方法,其他线程等待<cinit>()执行完毕。如果方法执行时间过长,那么就会造成多个线程阻塞(DemoThread2)。
}
/**输出结果:
pool-1-thread-2 start ..
pool-1-thread-2 init ....
pool-1-thread-1 start ..
pool-1-thread-19 start ..
pool-1-thread-5 start ..
pool-1-thread-6 start ..
pool-1-thread-9 start ..
pool-1-thread-13 start ..
pool-1-thread-3 start ..
pool-1-thread-7 start ..
pool-1-thread-11 start ..
pool-1-thread-15 start ..
pool-1-thread-17 start ..
pool-1-thread-10 start ..
pool-1-thread-14 start ..
pool-1-thread-18 start ..
pool-1-thread-4 start ..
pool-1-thread-8 start ..
pool-1-thread-12 start ..
pool-1-thread-16 start ..
pool-1-thread-20 start ..
*
*/
}
8 类加载器
8.1 概述
- 虚拟机的设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为类加载器。
- 只有被同一个类加载器加载的类才可能会相等。相同的字节码被不同的类加载器加载的类不相等。
8.2 类加载器分类
- 1. 启动类加载器(Bootstrap ClassLoader)
-
- 由C++实现,是虚拟机的一部分,用于加载javahome下的lib目录下的类,或者Xbootclasspath参数所指定的路径中的.
- 2. 其它类加载器
- 2.1 扩展类加载器(Extension ClassLoader)
-
- 由sun.misc.Launcher$ExtClassLoader实现,加载Javahome下/lib/ext目录中的类;或者被java.ext.dirs系统变量中所指定的路径中所有类库,
- 2.2 应用程序类加载器(Applicationg ClassLoader)
-
- 由sun.misc.Launcher$AppClassLoader实现,是ClassLoader中的getSystemClassLoader()方法的返回值,所以也叫系统加载器. 加载用户类路径(ClassPath)上的所指定的类库;
- *2.3 自定义类加载器(User ClassLoader)
8.3 自定义类加载器
定义一个类,继承ClassLoader
重写loadClass方法
实例化Class对象
//自定义类加载器和原版加载器得到的类不相等
import java.io.InputStream;
public class ClassLoadDemo {
public static void main(String[] args) throws Exception{
//自定义类加载器
ClassLoader mycl = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".")+1) + ".class";
InputStream ins = getClass().getResourceAsStream(fileName);
if(ins == null){
return super.loadClass(name);
}
try{
byte [] buff = new byte[ins.available()];
ins.read(buff);
return defineClass(name,buff,0,buff.length); //定义一个class
}catch (Exception e){
throw new ClassNotFoundException();
}
}
};
//调用class的实例
Object c = mycl.loadClass("course.jvmstu.classload.selfload.ClassLoadDemo").newInstance();
System.out.println(c.getClass());
//虚拟机类
System.out.println(c instanceof ClassLoadDemo);
}
}
8.4 自定义类加载器的优势
- 类加载器是Java语言的一项创新,也是Java语言流行的重要原因之一,它最初的设计是为了满足Java Applet的需求而开发出来的。
- 高度的灵活性
- 通过自定义类加载器可以实现热部署
- 代码加密
9 虚拟机字节码执行引擎
9.1 双亲委派模型
9.1.1 概述
从JDK1.2开始,java虚拟机规范推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:
- (1).如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
- (2).每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器。
- (3).如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。
- 双亲委派 模式的类加载机制的优点是java类 它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。
9.1.2 ClassLoader源码
//查看java.lang.ClassLoader的源码
//属于 应用程序类加载器(Applicationg ClassLoader)
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;
}
}