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

}

目录
相关文章
|
2月前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
本系列教程笔记详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。若需快速学习Kotlin,建议查看“简洁”系列教程。本期重点介绍了Kotlin与Java的共存方式,包括属性、单例对象、默认参数方法、包方法、扩展方法以及内部类和成员的互操作性。通过这些内容,帮助你在项目中更好地结合使用这两种语言。
48 1
|
2月前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
34 2
|
9天前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
9天前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
16天前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
25 2
|
9天前
|
Java 数据库连接 编译器
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
26 0
|
1月前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
1月前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
1月前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
|
21天前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
10 0