首页 > 编程语言 >Java类加载过程

Java类加载过程

时间:2023-01-14 14:23:39浏览次数:57  
标签:java name class Java 过程 Class 加载

概念

Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中,用于加载系统、网络或者其他来源的类文件。

Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。

类加载器分类

引导类加载器(BootstrapClassLoader)

引导类加载器(BootstrapClassLoader)属于JVM的一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心Java库,即在/jre/lib/rt.jar目录当中的类。

该类加载扩展类加载器和应用类加载器,并作为它们的父类加载器

处于安全考虑,引导类加载器只加载包名为:java、javax、sun开头的类

扩展类加载器(ExtensionsClassLoader)

扩展类加载器(ExtensionsClassLoader),用来加载/jre/lib/ext目录下的类,或者是在java.ext.dirs中指明的目录

sun.misc.Launcher$ExtClassLoader方法实现,父类加载器为引导类加载器

应用类加载器(AppClassLoader)

应用类加载器(AppClassLoader),用来加载java.class.path指定目录或者classpath路径下的类

sun.misc.Launcher$AppClassLoader方法实现,父类加载器为引导类加载器

自定义类加载器(UserDefineClassLoader)

自定义类加载器(UserDefineClassLoader),通过继承java.lang.ClassLoader类重写findClass()方法来实现自定义类加载器

类加载机制-双亲委派

JVM对class文件采用是按需加载方式,当需要使用该类的时候,JVM才会将class文件加载到内存中产生class对象

在加载类的时候,采用双亲委派机制

工作原理

  1. 当一个类加载器接收到了类加载的请求,会把这个请求委托给父类加载器去执行
  2. 如果父类还存在父类加载器,则继续向上委托,一直到引导加载器:BootstrapClassLoader
  3. 如果父类加载器可以完成加载任务,则返回成功结果,否则就由子类去加载,如果子类加载失败就会抛出ClassNotFoundException异常

优点

自底向上检查类是否加载、自顶向下加载类

避免了同一个类被多次加载,避免有重复的字节码出现,保证了Java程序安全稳定运行

ClassLoader类核心方法

findClass:查找指定的Java类

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

findLoadedClass:查找JVM是否已加载该类

protected final Class<?> findLoadedClass(String name) {
    if (!checkName(name))
        return null;
    return findLoadedClass0(name);
}

resolveClass:链接指定Java类

protected final void resolveClass(Class<?> c) {
    resolveClass0(c);
}

private native void resolveClass0(Class<?> c);

loadClass:加载指定的Java类

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;
        }
    }

loadClass方法中,大概的流程如下:

  1. 使用findLoadedClass方法来检查该类是否已被加载
  2. 若未被加载,接着使用parent.loadClass父类加载器进行加载,直到使用引导类加载器BootstrapClassLoader进行加载
  3. 之后调用findClass方法进行查找该类并加载
  4. 成功装载后,就会调用resolveClass方法去链接该类

defineClass:定义一个Java类

protected final Class<?> defineClass(byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(null, b, off, len, null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

将字节码解析成JVM识别的Class对象

自定义类加载器定义过程

自定义类加载器可以让我们可以调用本地磁盘文件或者通过网络远程加载类

为了符合双亲委派规范,所以我们一般不去重写loadClass方法,而是去修改findClass方法,所以自定义类加载器需要有以下三个步骤:

  1. 继承ClassLoader类
  2. 重写findClass方法
  3. 在findClass方法中调用defineClass方法来定义一个类

示例

先来定义一个自定义类Test.java

package com.MyClassLoader;

public class Test {
    public static void main(String[] args) {
        System.out.println("This experiment test is successful");
    }
}

先将该自定义类编译为Class字节码文件

然后我们编写一个异或加密类Encryption.java,执行该类对Test.class字节码进行异或加密

package com.MyClassLoader;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Encryption {
    public static void main(String[] args) {
        encode(new File("./src/main/java/com/MyClassLoader/Test.class"), new File("./src/main/java/com/MyClassLoader/Test.class"));
    }

    public static void encode(File src, File dest) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(src);
            fos = new FileOutputStream(dest);

            // 取反加密
            int temp = -1;
            while ((temp = fis.read()) != -1) {
                fos.write(temp ^ 0xff);
            }
        } catch (IOException e) {

        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("This experiment test is successful");
    }
}

然后通过自定义类加载器Decryption.java来加载这个被加密的类

package com.MyClassLoader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Dectyption extends ClassLoader {
    public byte[] getClassData(String className) {
        String path = "./src/main/java/" + className.replace('.', '/') + ".class";
        InputStream is = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            is = new FileInputStream(path);
            int temp;
            while ((temp = is.read()) != -1) {
                baos.write(temp ^ 0xff);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (baos != null) {
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    protected Class<?> findClass(String name) {
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            byte[] classData = getClassData(name);
            System.out.println(name);
            c = defineClass(name, classData, 0, classData.length);
        }
        return c;
    }
}

然后我们去使用自定义类去加载Test.class

package com.MyClassLoader;

public class TestLoader {
    public static void main(String[] args) throws ClassNotFoundException{
        Dectyption dectyption = new Dectyption();
        Class<?> clazz = dectyption.loadClass("com.MyClassLoader.Test");
        System.out.println(clazz);
    }
}

应用场景

代码保护

通过刚才的自定义类加载器示例就能发现,我们可以对自定义的类进行加密,然后再通过自定义解密类加载器去加载该类,实现了对代码的保护,并且还可以构造恶意类绕过服务器对文件内容的检测。

资源隔离

通过类加载器,我们可以去加载指定的类,故可以实现不同项目或同一项目上不同版本的jar包隔离,避免集群错误或者产生冲突。

热部署

热部署是在不重启Java虚拟机的前提下,能自动侦测到class文件的变化,更新正在运行的Class对象行为。

Java类在被ClassLoader加载后,会产生相应的Class对象,默认的JVM只会在启动时加载类,如果后期有一个类更新,只会替换class文件,不会更新正在运行的Class对象

所以改变了ClassLoader的加载行为,使得JVM能够监听class文件的更新并重写加载class文件

热部署步骤:

  1. 销毁自定义ClassLoader(被该加载器加载的class也会自动卸载)
  2. 更新class文件
  3. 使用新的ClassLoader去加载class文件

JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):

  • 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
  • 加载该类的ClassLoader已经被GC。
  • 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法

参考文章

JAVA安全基础(一)--类加载器(ClassLoader) - 先知社区

jvm类加载器,类加载机制详解,看这一篇就够了

吊打面试官-类加载器

标签:java,name,class,Java,过程,Class,加载
From: https://www.cnblogs.com/seizer/p/17051693.html

相关文章

  • Centos7下安装Dogtail GUI自动化测试工具并打开sniff工具过程中遇到的问题解决方法
    (目录)因为测试需要,需在Centos下进行liunxGUI软件自动化测试,所以用到了python的Dogtail库,继而使用Dogtail的sniff控件获取工具,但是遇到了很多问题记录如下。1环境Cent......
  • 记一次yapi部署过程
    一、为什么用yapiyapi基于文档注释生成,没有代码的入侵。同一个工程的接口文档可以导出多个项目中,分权限查看。可以本地化部署,统一的接口文档,支持其他的文档接入。有id......
  • (Java)设计模式:结构型
    前言这篇博文续接的是UML建模、设计原则、创建型设计模式、行为型设计模式,有兴趣的可以看一下3.3、结构型这些设计模式关注类和对象的组合。将类和对象组合在一起,从......
  • JAVA是引用传递还是值传递
    我们先不上结论,接下来一点点推导:publicstaticvoidmain(String[]args){SpringApplication.run(Main.class,args);log.info("项目启动成功");......
  • 随机过程的思维导图和笔记
    简单梳理了一下随机过程前四章的内容,这四章联系还是很紧密的。以第一章为基础,延伸出第二章的多种poisson过程,包括复合、稀释、叠加以及条件分布的poisson过程。第三章应用......
  • JAVASE强化基础Day1
    总结:java跨平台性:首先编写java文件,再通过编码变成class文件,最后通过JVM(JAVA虚拟机)跨平台可以运行编码:java代码编码一般再eclipse和idea上都式TUF-8,如果发现代码的中文......
  • Java学习笔记10
    1.抽象类1.1概述​ 没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。抽象方法:没有方法体的方法。抽象类:包含抽象方法的类。1.2abstract......
  • 【Java】蚂蚁迷宫问题
    packagecom;publicclassMiGong{publicstaticvoidmain(String[]args){//思路//1、先创建迷宫,用二维数组表示intmap[][]=newint[8][7];......
  • Java数组动态扩容和动态缩减
    数组动态扩容:packagecom;importjava.lang.String;importjava.util.Scanner;publicclassLinghu{publicstaticvoidmain(String[]args){intarr[]={1,2,3......
  • Java基础数据类型
     今天学习了基本的数据类型,主要分为基本数据类型和引用数据类型,基本数据类型之中主要有整数类型、浮点类型、布尔类型和字符类型。byte类型主要占用1个字节,short类型占用......