Java类加载器解析

类加载过程

  1. 加载: 通过磁盘IO读取 .class 文件,将文件作为字节流读入,当使用到该类时才会加载,例如运行 main程序,new一个对象等等,加载阶段会生产一个 java.lang.Class 对象,作为方法区访问类数据的入口;
  2. 验证:验证字节码文件的正确性;
  3. 准备:给类的静态属性分配内存,并赋初始值;
  4. 解析: 将符号引用转化为直接应用。
    符号引用: 指的是用于描述目标的符号,可以是任何字面量,只要能定位到目标即可。例如 test()是一个方法,那么test就是可以定位到tets()方法的字面量;
    直接引用:直接指向目标的指针、相对偏移量或间接定位到目标的句柄。比如方法,静态变量的直接引用就是方法区的指针,方法区里的方法表含有类的所有方法的信息,通过解析符号引用可以定位到目标方法在类中方法的位置,从而使方法可以被调用。
  5. 初始化:执行静态方法块,未静态变量赋值。
  6. 使用
  7. 销毁
    在这里插入图片描述

类加载器

在Java的世界里,有下面几个类加载器

  • 引导类加载器(BootstrapClassLoader): 负责加载JVM运行所必须的,位于JRE的lib目录下的核心类库,如 rt.jar,charsets.jar;
  • 扩展类加载器(ExtClassLoader):负责加载JVM运行所必须的,位于JRE下,lib/ext目录下的扩展类库;
  • 应用程序类加载器(AppClassLoader):负责加载classpath目录下或者第三方jar包的类,主要内容是自己编写的类;
  • 自定义类加载器:负责加载自定义目录下的类。

用一段程序验证:

public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(com.sun.nio.zipfs.ZipPath.class.getClassLoader());
        System.out.println(ClassLoaderTest.class.getClassLoader());
    }
}

输出结果:
null
sun.misc.Launcher$ExtClassLoader@6e0be858
sun.misc.Launcher$AppClassLoader@14dad5dc

双亲委派

双薪委派可用一张图描述:
在这里插入图片描述
总结起来就是:
当 JVM 尝试加载一个类时,会尝试先向上委托它的父类加载器加载,它的父类加载器会再委托它的父类加载器加载,如果父类加载器能够加载,就让父类加载器加载;如果不能再让它本身路径的加载器加载,如果最底层的加载器都加载不了,就会抛出 ClassNotFoundException。
举个例子,当 ClassLoaderTest 要加载 String.class时,向上委托到 BootstrapClassLoader 时已经发现加载过了,就让BootstrapClassLoader 加载。但是加载 ClassLoaderTest 时在父类加载器都没有找到,就让 AppClassLoader 加载。

双亲委派源码:

public abstract class ClassLoader {
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 检查当前类加载器是否已经加载过该类
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false); //父加载器不为空,那么先委托父加载器加载
                    } else {
                        c = findBootstrapClassOrNull(name); //父加载器为空,那么委托BootstrapClassLoader加载
                    }
                } 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); //如果父加载加载不到,也就是用户编写的类都会走到这里,调用URLClassLoader.findClass()

                    // 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;
        }
    }
}
为什么要设计双亲委派?

1. 安全: 防止核心类库被人篡改,如果用户在自己的目录下也定义了一个 java.lang.String,JVM 是不会加载这个类的;

做两个简单的验证:

package java.lang;
public class String {
    public static void main(String[] args) {}
}

输出:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

=============================================================================================================

package java.lang;
public class UserDefin{
    public static void main(String[] args) {}
}

输出:
java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" 

2. 避免类的重复加载。

全盘委托机制

指一个类加载器尝试加载一个类时,如果没有显式的指明另一个类加载器,那么这个类以及它的依赖都由这个类加载加载。

自定义类加载器

首先需要将一个 .class 文件拷贝到项目外的任意一个目录。
在这里插入图片描述

public class MyClassLoader extends ClassLoader {

    String path;

    public MyClassLoader(String filePath) {
        this.path = filePath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getData();
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] getData() {
        File file = new File(path);
        if (file.exists()) {
            FileInputStream in = null;
            ByteArrayOutputStream out = null;
            try {
                in = new FileInputStream(file);
                out = new ByteArrayOutputStream();

                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = in.read(buffer)) != -1) {
                    out.write(buffer, 0, size);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    in.close();
                } catch (IOException e) {

                    e.printStackTrace();
                }
            }
            return out.toByteArray();
        } else {
            return null;
        }
    }

    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("E:/temp/Cat.class");
        // 将 Cat 交给类加载器加载
        Class catClass = myClassLoader.loadClass("Cat");
        Method method = catClass.getDeclaredMethod("toString",null);
        Object obj = catClass.newInstance();
        System.out.println(method.invoke(obj,null));
        System.out.println(catClass.getClassLoader().getClass().getName());
    }
}

运行结果:
Hi, this is greeting from Cat.class
com.classloader.MyClassLoader

打破双亲委派

按照前面对 ClassLoader.loadClass() 的分析,打破双亲委派很简单,定义自定义类加载器,重写 loadClass(),不调用父类的 loadClass() 即可。

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                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;
        }
    }
}

这样一来,所有的 Java 类都由自定义类加载器加载了。但是并不能运行,会出现错误:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)

可以看出,即使使用自定义类加载器,也没办法篡改Java的核心类库。

打破双亲委派的好处

在某些场合下,打破双亲委派是很有用处的。

拿 tomcat 举例。Tomcat 如果使用默认的双亲委派,会存在下面几个问题:

  1. Tomcat可能需要同时部署多个应用程序,每个应用程序依赖的类库虽然相同,但是版本很可能不同,如果要求同一个类库在每个容器之间都是相同的,那么就会出现问题,必须保持容器之间的相互隔离;
  2. Tomcat也有自己依赖的类库,不能将自己的类库与应用程序的类库混淆;
  3. 需要支持 jsp 的修改,满足启动应用程序后,修改 jsp 不需要重启。jsp 本质上也是一个 .class 文件,在编译后,无论怎么改变,类加载器还是会直接加载方法区内的信息。

所有Tomcat需要打破双亲委派,但也要保证在 Tomcat 内部署的版本相同的类库可以共享,不然如果需要运行100个应用程序,就会有多个相同的类库信息加载到 JVM 中
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java类加载器的作用是将Java字节码文件(.class文件)加载到内存中,并将其转换为可执行的Java类。它负责在运行时动态地加载Java类,以便可以创建对象并执行类中的方法。 具体来说,类加载器的主要作用包括: 1. 加载:类加载器负责从文件系统、网络或其他来源加载字节码文件。它会根据类的全限定名查找并读取对应的字节码文件。 2. 验证:类加载器会验证字节码文件的合法性,确保它符合Java虚拟机规范。这包括检查字节码的结构、语法和依赖关系等方面。 3. 准备:在类加载过程中,类加载器会为静态变量分配内存,并设置默认值。这些变量在后续的初始化阶段被赋予正确的初始值。 4. 解析类加载器会将字节码中的符号引用转换为直接引用,以便在运行时能够直接定位到相关的类或方法。 5. 初始化:类加载器会执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。这是类加载过程中的最后一步。 6. 缓存:为了提高性能,类加载器会缓存已加载的类。如果同一个类被多次加载,可以直接从缓存中获取,避免重复加载和初始化。 通过自定义类加载器,还可以实现一些特殊的加载需求,例如从加密文件中加载类、动态生成类等。类加载器Java虚拟机的重要组成部分,它保证了Java程序的灵活性和动态性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值