Java ClassLoader笔记

简介:

.java:保存需要执行的程序逻辑 编译  .class:保存java代码转换后的虚拟机指令
要使用某个类时,虚拟机将加载.class文件,并创建对应的class对象。

将class文件加载到虚拟机的内存的过程叫类加载,过程如下:
①加载Loading:利用class文件创建class对象
②验证Verification:确保class文件的字节流中包含信息符合虚拟机要求,不会危害虚拟机本身安全,包括四种验证:文件格式验证;元数据验证;字节码验证;符号引用验证。
③准备Preparation:为类变量(static修饰的变量)分配内存并设置该类变量的初始值0
(static int i = 5;将只会把i初始化为0,5的值在初始化时赋值)。注意:智利不包含用final修饰的static,因为final在编译时就会分配了;这里不会为实例变量分配初始化,连变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
④解析Resolution:将常量池中的符号引用替换为直接引用的过程。
⑤初始化Initialization:执行静态初始化器和静态初始化成员变量。
其中②③④被称为链接(Linking)过程。

符号引用:符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
直接引用:
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
符号引用和直接引用的解释:
https://www.zhihu.com/question/30300585
https://blog.csdn.net/kkdelta/article/details/17752097
启动(BootStrap)类加载器:
加载JVM自身需要的类,是虚拟机自身的一部分,会将JAVA_HOME/lib下的核心类库或-Xbootclasspath参数指定路径下的jar包加载到内存中(出于安全考虑,bootstrap类加载器只加载包名为java、javax、sun等开头的类)。

扩展(Extension)类加载器:
Sun公司实现的sum.misc.Luncher$ExtClassLoader类,是Luncher的静态内部类,负责加载JAVA_HOME/lib/ext下或由系统变量-Djava.ext.dir指定路径中的类库,开发者可以直接使用标准扩展类加载器。

系统(System)类加载器:
也称应用程序加载器,指sun公司实现的sum.misc.Launcher$AppClassLoader。负责加载系统类路径java –classpath或 –Djava.class.path下的类库。一般情况下该类加载器是程序默认的类加载器,可以通过ClassLoader#getSystemClassLoader()方法获取到该类加载器。

双亲委派模式加载:
image

原理说明:一个类加载器收到了类加载请求,不会自己先加载,而是把这个请求委派给父类加载器去执行,如果父类还有父类则继续向上委托,最终达到顶层的启动类加载器,从启动类加载器开始进行加载,如果成功则返回,否则子加载器才会尝试自己加载。
好处:
①防止重复加载:如果父加载器已经加载了此类子加载器就没必要再加载了
②安全考虑:如果运行时要加载一个java.lang.Integer类,会传递到启动类加载器,而启动类加载器发现在核心java API中有这个类已被加载,就直接返回已经加载了的Integer.class。再例如:要加载一个java.lang.FakeInteger,启动,扩展类加载器的路径下都没有该类,不会加载,会反向委托给系统类加载器加载,但是这样是不行的,因为java.lang是核心API包,需要访问权限,强制加载会报如下异常:
java.lang.SecurityException: Prohibited package name: java.lang
image

类加载器常见类:
通过上面的报错堆栈,可以看出几个ClassLoader相关类的关系如下:
image

①ClassLoader
抽象类,定义了几个基本的类加载相关方法。
②SecureClassLoader
扩展了ClassLoader,新增了几个与使用相关的代码源和权限定义类验证。
③URLClassLoader
实现了ClassLoader没实现的几个方法,如findClass 等,有一个相关类URLClassPath负责获取Class字节码流,一般自定义类加载器继承URLClassLoader即可。这两个类的构造方法都有一个必填参数URL,该参数是一个路径,可能是文件或jar包,然后根据不同路径创建FileLoader或JarLoader或默认的Loader去加载相应路径下的class文件。
image

类加载器常用方法:
①loadClass
image

②findClass
③defineClass
将byte字节流解析成JVM能够识别的Class对象,一般defineClass()方法通常与findClass()方法一起使用,一般在自定义类加载器时会覆盖ClassLoader类的findClass方法并编写加载规则,取的要加载类的字节码后转换成流,然后调用defineClass方法成成类的Class对象。
④resolveClass(Class c)
对Class对象进行解析。

ExtClassLoader和AppClassLoader
他们都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。类结构如下:
image

类加载器关系:
①启动类加载器:由C++实现,没有父加载器
②扩展类加载器(ExtClassLoader)由Java实现,父加载器为null
③系统类加载器(AppClassLoader),父加载器为ExtClassLoader
④自定义类加载器,父加载器为AppClassLoader
image

我们实现的自定义类加载器的父加载器都是AppClassLoader。Launcher类的源码如下:
image

如上:Launcher初始化时会创建ExtClassLoader,然后把ExtClassLoader作为parent入参构造AppClassLoader,然后将AppClassLoader默认设置为线程上下文加载器。而ExtClassLoader的parent为null,如下代码:
image

类与类加载器
JVM中两个class对象是否是统一个类对象的必要条件为:
①类的完整类名必须一致,包括包名
②加载这个类的ClassLoader必须相同

实现自己的ClassLoader
public class MyClassLoader extends ClassLoader
{

private String classPath;
public MyClassLoader(){
    super();
}
public MyClassLoader(String classPath){
    this.classPath = classPath;
}
public static void main(String[] args)
{
    try
    {
        String path = "F:\\Learn\\Java\\workspace\\spring\\MyClassLoader\\bin";
        
        MyClassLoader mcl1 = new MyClassLoader(path); // 自定义类加载器实例1
        Class clazz1 = mcl1.findClass("com.classloader.MyClassLoader"); // 自定义类加载器加载自身类
        
        MyClassLoader mcl2 = new MyClassLoader(path); // 自定义类加载器实例2
        Class clazz2 = mcl2.findClass("com.classloader.MyClassLoader"); // 自定义类加载器加载自身类
        
        Object inst = clazz1.newInstance(); //new一个自身类的实例
        System.out.println(inst.toString());
        
        System.out.println(mcl1.getClass().hashCode());//系统默认加载器加载的class类的实例是一样的
        System.out.println(mcl2.getClass().hashCode());//系统默认加载器加载的class类的实例是一样的
        System.out.println(clazz1.hashCode());//自定义加载器两次加载的class实例是不一样的(使用findClass)
        System.out.println(clazz2.hashCode());//自定义加载器两次加载的class实例是不一样的(使用findClass)
        System.out.println(mcl1.loadClass("com.classloader.MyClassLoader").hashCode());//loadClass和上面findClass的class对象实例是一样,因为loadClass时会先findClass,检查类是否已经被加载
        System.out.println(mcl2.loadClass("com.classloader.MyClassLoader").hashCode());//loadClass和上面findClass的class对象实例是一样,因为loadClass时会先findClass,检查类是否已经被加载

        System.out.println(mcl1.getSystemClassLoader());//系统类加载器是AppClassLoader
        System.out.println(mcl1.getParent());//自定义类加载器的父加载器是AppClassLoader
        System.out.println(mcl1.getSystemClassLoader().getParent());//系统类加载器的父加载器是ExtClassLoader
        System.out.println(mcl1.getSystemClassLoader().getParent().getParent());//ExtClassLoader的父加载器是null
    } catch (Exception e)
    {
        e.printStackTrace();
    }
}

@Override
public String toString()
{
    return "MyClassLoader";
}

public Class<?> findClass(String name) throws ClassNotFoundException
{
    // 获取class文件的byte[]数组
    try
    {
        byte[] classBytes = findClassBytes(name);
        if(null == classBytes){
            throw new ClassNotFoundException();
        }
        return defineClass(name,classBytes,0,classBytes.length);
    } catch (FileNotFoundException e)
    {
        e.printStackTrace();
    }
    return null;
}

public byte[] findClassBytes(String className) throws FileNotFoundException
{
    String path = this.classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    InputStream is = null;
    if(this.classPath.startsWith("http")){
        try
        {
            /*URL u = new URL(path);
            is = u.openStream();*/
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }else{
        is = new FileInputStream(path);
    }
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] bs = new byte[1024];
    int bytesNum = 0;
    try
    {
        while((bytesNum = is.read(bs)) != -1){
            baos.write(bs,0,bytesNum);
        }
        return baos.toByteArray();
    } catch (IOException e)
    {
        e.printStackTrace();
    }
    finally{
        try
        {
            is.close();
            baos.close();
        } catch (IOException e)
        {
            e.printStackTrace();
        }
    }
    return null;
}

}

目录
相关文章
|
25天前
|
设计模式 Java
Java基础—笔记—多态、final、抽象类、接口篇
该文介绍了编程中的多态、final和抽象类、接口相关概念。多态允许子类重写父类方法,通过父类引用调用子类方法,实现解耦和提高代码灵活性,但也可能导致无法使用子类特有功能,需通过强制类型转换解决。final用于修饰不可变的类、方法或变量,防止继承、重写和多次赋值。抽象类是一种包含抽象方法的类,用于强制子类重写特定方法,实现多态,适用于模板方法设计模式,解决代码重复问题。
17 0
|
25天前
|
Java
Java基础—笔记—static篇
`static`关键字用于声明静态变量和方法,在类加载时初始化,只有一份共享内存。静态变量可通过类名或对象访问,但推荐使用类名。静态方法无`this`,不能访问实例成员,常用于工具类。静态代码块在类加载时执行一次,用于初始化静态成员。
10 0
|
25天前
|
Java API 索引
Java基础—笔记—String篇
本文介绍了Java中的`String`类、包的管理和API文档的使用。包用于分类管理Java程序,同包下类无需导包,不同包需导入。使用API时,可按类名搜索、查看包、介绍、构造器和方法。方法命名能暗示其功能,注意参数和返回值。`String`创建有两种方式:双引号创建(常量池,共享)和构造器`new`(每次新建对象)。此外,列举了`String`的常用方法,如`length()`、`charAt()`、`equals()`、`substring()`等。
15 0
|
2月前
|
算法 搜索推荐 Java
数据结构与算法(Java篇)笔记--希尔排序
数据结构与算法(Java篇)笔记--希尔排序
|
3月前
|
监控 负载均衡 Dubbo
|
2月前
|
Java 关系型数据库 MySQL
在Java的反射中,Class.forName和ClassLoader的区别
在Java的反射中,Class.forName和ClassLoader的区别
36 3
|
25天前
|
Java API
Java基础—笔记—内部类、枚举、泛型篇
本文介绍了Java编程中的内部类、枚举和泛型概念。匿名内部类用于简化类的创建,常作为方法参数,其原理是生成一个隐含的子类。枚举用于表示有限的固定数量的值,常用于系统配置或switch语句中。泛型则用来在编译时增强类型安全性,接收特定数据类型,包括泛型类、泛型接口和泛型方法。
11 0
|
2月前
|
Java
Java8 Stream流 使用笔记
Java8 Stream流 使用笔记
27 1
|
2月前
|
算法 搜索推荐 Java
数据结构与算法(Java篇)笔记--快速排序
数据结构与算法(Java篇)笔记--快速排序
|
2月前
|
机器学习/深度学习 算法 搜索推荐
数据结构与算法(Java篇)笔记--归并排序
数据结构与算法(Java篇)笔记--归并排序