双亲委派机制
- Java虚拟机对class文件的加载采用的是按需加载,也就是说需要使用该类时才会将他的class文件加载到内存生成class对象,而且加载到某个类的class文件时,Java虚拟机采用的是双亲委派机制,他把任务交给父类来进行处理,这是一种任务委派模式
工作原理
- 如果一个类加载收到了类加载请求,它并不会自己先去加载而是把这个请求委托给父类的加载器去执行
- 如果父类的加载器还存在存在其父类加载器则进一步向上委托,依次向上递归,请求最终将达到的启动类加载器
- 如果父类加载器可以完成类加载任务,就成功返回,若父类加载器无法完成此加载任务,子类加载器才会尝试自己去加载,这就是双亲委派机制
查看源码
- 在java.lang.ClassLoader.java下查看加载类的方法(loadClass)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检测当前类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
// c == null 代表当前类没有被加载过
long t0 = System.nanoTime();
try {
// 如果有父类加载器就让父类加载器进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父类加载器为空,表示已经到启动类加载器(BootStrapClassLoader)了
// 启动类加载器(BootStrapClassLoader)是由C/C++实现的,通过get是无法获取到的
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果从非空的父类加载器中找不到类,则抛出ClassNotFoundException异常
}
if (c == null) {
// 如果仍然没有被加载,则尝试自己去加载该类
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- 从上述的代码中可以看出,当一个类加载时,不考虑用户自定义的类加载器,类首先会在系统类加载器(AppClassLoader)中检查是否已经被加载,如果不用加载。如果没有,那么会拿到父加载器,然后调用父加载器扩展类加载器(ExtClassLoaderloadClass)的loadClass。父类中同理也会先检查自己是否已经加载过,如果没有再往上到启动类加载器(BootstrapClassLoader)。在到达启动类加载器(BootstrapClassLoader)之前,都是在检查是否加载过,并不会选择自己去加载。直到启动类加载器BootstrapClassLoader,没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
双亲委派机制的优势
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改
- 自定义类:java.lang.String
- 例子
自定义String类,但在加载自定义String类的时候会率先使用应到类的加载器加载,而引导类加载器在加载的过程中会先加载JDK自带的文件(rt.jar包中的java\lang\String.class),此时不在加载自定义的String类.报错信息说没有main方法,就是因为加载的是rt.jar包中的java.lang.String类中是没有main方法的.所以就会抛出异常。这样可以保证对java核心源代码的保护,这就是沙箱安全机制
package java.lang;
public class String {
// 初始化的第三阶段初始化
static{
System.out.println("自定义string");
}
// 错误: 在类 java.lang.String 中找不到 main 方法,
public static void main(String[] args) {
System.out.println("hello,String");
}
}
/**
* 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
* public static void main(String[] args)
* 否则 JavaFX 应用程序类必须扩展javafx.application.Application
* @param args
*/
Java判断是否是同一个类
- 在JVM中表示两个class对象是否是同一个类存在两个必要条件
- 类的完整类名必须一致,包括包名
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
- 换句话说,在JVM中及时这两个类对象(class对象)来源于同一个Class文件,被同一个虚拟机所加载,但只要是加载他们的ClassLoader实例对象不同,那么这两个类对象也是不相等的
PC寄存器(程序计数器)
-
PC寄存器位于运行时数据区,PC寄存器为线程私有的
-
Jvm中的程序计数寄存器(Program Count Register),是对物理PC寄存器的一种抽象模拟
-
作用:PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令
-
他是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域
-
在JVM规范中,他的每个线程都有他自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
-
任何一个时间线程都只有一个方法在执行,也就是所谓的**当前方法,**程序计数器会存储当前线程正在执行的Java方法的Jvm指令地址,或者如果是在执行native方法,则是未指定值(undefined)
-
他是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
-
字节码解释器工作时,就是通过改变这个技术器的值来选取下一条需要执行的字节码指令
-
它是唯一一个在Java虚拟机规范中没有规定任何OutotMemoryError情况的区域
-
使用PC寄存器存储字节码指令地址的作用(记录当前线程的执行地址的作用)
- 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始执行
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条该执行什么样的字节码指令
-
PC寄存器(程序寄存器)为什么设为线程私有
- 为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法是为每一个线程都分配一个PC寄存器,这样一来各个线程便可以独立计算,从而不会出现互相干扰的情况
- 由于CPU时间片轮的限制,中多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令