Java ClassLoader笔记-阿里云开发者社区

开发者社区> anmypeng1> 正文

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

}

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
4054 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
4398 0
Class 5 搭建个人Leanote云笔记本
Class 5 搭建个人Leanote云笔记本
234 0
java-Thread笔记
java-Thread笔记
66 0
+关注
9
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载