JVM类加载机制

1.类加载的生命周期

  类的加载过程包括:加载、验证、准备、解析、初始化

(解析某些情况下可以在初始化阶段后开始;

几个阶段按顺序开始,并非进行或完成,这些阶段通常是互相交叉混合进行。)

  类的加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类的加载:将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个类的Java.lang.Class对象,用来封装类在方法区类的对象。
加载类的时候,JVM必须完成三件事:

(1)通过类的全名,获取类的二进制数据流

(2)解析类的二进制数据流为方法区内的数据接口(java类模型)

(3)创建java.lang.Class类的实例,表示该类型。作为方法区这个类各种操作的入口

连接:

  验证确保被加载类的正确性(为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全)

  准备类变量分配内存并设置初始值(默认值)的阶段。(类变量被分配到方法区中,而实例变量存放在堆中)

  解析把类中的符号引用(代号)转换为直接引用(直接指向目标)

初始化:为类的静态变量赋予初始值

使用类访问方法区内的数据结构接口,对象是Heap区的数据(开发者可以在程序中调用类的静态方法、静态成员变量了)

卸载结束生命周期

 

类加载器加载规则

  JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

  对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

 JVM 判定两个 Java 类是否相同的具体规则 :JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。

 

2.类加载器的层次

 

  Bootstrap ClassLoader 启动 类加载器:负责加载系统类(从rt.jar中进行加载),这个类加载器主要加载JVM自身工作需要的类。

  Extension ClassLoader 扩展:它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(javax.*开头的类),开发者可以直接使用扩展类加载器。(sun.misc.Launcher$ExtClassLoader

  Application ClassLoader 应用:它负责加载用户类路径上指定的类库,开发者可以直接使用这个类加载器。加载应用类,由CLASSPATH环境变量或者-classpath命令行选项设置的类路径中的目录里或者是JAR/ZIP文件里查找这些类。(sun.misc.Launcher$AppClassLoader

  ExtClassLoaderAppClassLoader都是URLClassLoader的子类。

  自定义类加载器:

    在执行非置信代码之前,自动验证数字签名。

    动态地创建符合用户特定需要的定制化构建类。

    从特定的场所取得java class,例如数据库中和网络中。

 

3.Class.forName()ClassLoader.loadClass()区别

  Class.forName()将类的.class文件加载到jvm中,会对类进行解释,执行类中的static块。

  它的作用就是按参数中指定的字符串形式的类名去搜索并加载相应的类,如果该类字节码已经被加载过,则返回代表该字节码的Class实例对象,否则,按类加载器的委托机制去搜索和加载该类,如果所有的类加载器都无法加载到该类,则抛出ClassNotFoundException

  Class.forName(name, initialize, loader):带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。(第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器)

A a = (A)Class.forName("pacage.A").newInstance();     

A a = new A();

  ClassLoader.loadClass():只做一件事,将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会执行static块。

 

4.JVM有哪些类加载机制

  全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另有一个类加载器来载入。

  父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

  缓存机制:将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类后对应的二进制数据,并将其转换程Class对象,存入缓存区。(当你修改某个Java文件之后,不重启JVM的话,修改后的Java文件对应的Class文件并不在虚拟机中,所以是没法使用的。因此,想要使用修改后的Class文件,就要重启JVM。)

  双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

 

双亲委派机制过程?

  当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

  当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

  如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

  若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

双亲委派模型的好处

  双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

  如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

「jdk为什么要用双亲委派机制」
  避免类的重复加载:当一个类加载器接收到加载类的请求时,它会先向上委托给父类加载器,如果父类加载器已经加载了该类,就无需再重复加载,从而避免了类的重复加载。
  防止核心API被篡改:Java核心API被Java虚拟机提供的类加载器加载,通过双亲委派机制,可以确保核心API的安全性,防止被恶意篡改。  
  控制类的加载顺序:由于双亲委派机制是从上至下的加载方式,所以可以确保类的加载顺序是有序的,避免了类之间的依赖关系出现问题。
java核心api有:1. 包装类;2. Number类;3. Boolean类;4. Byte类;5. Character类;6. Double类;7. Math类。

如何打破双亲委派机制:  

  重写ClassLoader的loadClass()方法。

为什么是重写 loadClass() 方法打破双亲委派模型呢?双亲委派模型的执行流程已经解释了:

类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。

我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。

双亲委派的逻辑代码实现在Launcher类中loadClass方法中。源码如下

Class<?> loadClass(String name, boolean resolve){
//从自己加载的类里边找
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
//递归父加载器加载
c = parent.loadClass(name, false);
} else {
//ext加载器的父是null,因为它的父加载器是引导类加载器,他是c++实现的,在java中部显示
//这是最后一层,引导类加载器加载
c = findBootstrapClassOrNull(name);
}
if (c == null) {
//尝试加载,由URLClassLoader类实现
c = findClass(name);
}
}
return c;
}

尝试加载,#URLClassLoader.findClass源码

Class<?> findClass(final String name){
String path = name.replace('.', '/').concat(".class");
//判断是不是自己要加载的。jrt/ ext/
Resource res = ucp.getResource(path, false);
if (res != null) {
//真正加载class的方法
return defineClass(name, res);
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;


自定义加载器

我们要自定义加载器,只需要实现ClassLoader,重写findClass就可以。

public class MyClassLoader extends ClassLoader {
private String classPath;

public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//真正的加载步骤
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader =
new MyClassLoader("C:/myClassLoader");
Class<?> clazz = myClassLoader.loadClass("com.bbk.code.User");
Object o = clazz.newInstance();
Method method = clazz.getDeclaredMethod("eat");
method.invoke(o);
System.out.println("当前MyClassLoader的类加载器:"+MyClassLoader.class.getClassLoader());
}
}

User类:我们需要将User类编译好的class文件放到指定的目录(C:/myClassLoader),需要新建目录为com/bbk/code,执行main方法时候先要target下边的User.class删除。

public class User {
public void eat(){
System.out.println("我是User类eat方法,我的加载器是"+User.class.getClassLoader());
}
}
我是User类eat方法,我的加载器是com.bbk.code.MyClassLoader@5a07e868
自定义MyClassLoader继承ClassLoader类初始话时候会先初始化父类,在这时候会给自定义MyClassLoader赋值parent为AppClassLoader。具体代码体现在:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
//默认复制为AppClassLoader
this.parent = parent;
... ...
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
... ...
return scl;
}
private static synchronized void initSystemClassLoader() {
... ...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
//返回this.loader,引导类在加载Launcher类的时候会赋值为AppClassLoader
scl = l.getClassLoader();
... ...
}

打破双亲委派意思就是不在委托父加载,直接自己加载类。双亲委派的逻辑代码实现在loadClass方法中。需要继承ClassLoader重写loadClass方法即可:

    @Override
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);
//把去父类查找逻辑删除
//c = parent.loadClass(name, false);
if (c == null) {
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

重新执行main方法,出现以下错误:

java.io.FileNotFoundException: C:\myClassLoader\java\lang\Object.class (系统找不到指定的路径。)

在加载User的时候,会先加载父类Object,我们打破双亲委派之后,自定义的加载器不存在Object所以会报错。我们把Object.class放入到我们的本地文件夹中。重新执行报错:

//禁止加载java.lang 包
java.lang.SecurityException: Prohibited package name: java.lang

java.lang必须由我们引导类加载器加载,沙箱安全。我们只能让我们的引导类去加载java.lang。修改代码

    @Override
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) {
//我们自己的逻辑类名
if(!name.startsWith("com.bbk")){
//如果不是我们自己写的类,还是走原来双亲委派逻辑
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}

}
if (resolve) {
resolveClass(c);
}
return c;
}
}

只有当name是由com.bbk开头的由我们加载器加载。其他的都由父加载器加载。不会报错。



posted @ 2023-05-04 19:35  壹索007  阅读(15)  评论(0编辑  收藏  举报