javajava基础首页投稿(暂停使用,暂停投稿)

java基础(第三篇)ClassLoader必知必会

2018-01-27  本文已影响960人  kk少年

转载请联系作者获得授权并注明出处:
原文链接:www.jianshu.com/p/92c27f117edc
原文作者:wyn_lin

本文篇幅稍长,建议收藏慢慢看。

虚拟机类加载机制经常在面试的时候是必考的问题,了解类加载机制对每个java程序员来说都是很重要的,知根知底地写代码,遇到问题的时候才能快速找出问题所在。

虚拟机类加载过程

java是一门解释型语言,虚拟机在运行时将字节码进行动态加载和链接。一个类从被加载到内存开始,到它卸载分7个阶段:

其中,加载验证准备解析初始化类加载的过程。解析阶段和初始化阶段的时间先后并没有强制要求。

加载

通过类的全限定名,从指定来源加载二进制字节码,并转换成jvm可以使用的数据结构,在内存中生成一个代表该类的java.lang.Class对象。

这里的来源可以是文件网络数据库等。

验证

对字节码进行验证,比如是否符合jvm对于字节码的规范,以及对安全性等做检验。

准备

解析

将字节码常量池中的符号引用转化为直接引用的过程。(这里的常量池以及后面提到的常量池,不是指内存中的常量池,而是指class文件中的某一段,你可以理解成字节码文件中存放各类信息的仓库,供jvm读取)

java源代码在进行javac编译成字节码文件的时候,并没有像c/c++那样,在编译时进行链接,而是在运行时动态链接,因此,class字节码中并没有保存各个变量、方法在内存中的地址布局,由虚拟机动态分配所需内存,然后将这些符号引用解析成如下几种直接引用:

直接引用是和虚拟机的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

解析主要针对的是类或接口、类的字段、类的方法、接口的方法、方法类型、方法句柄和调用点限定符等符号,下面列出常量池中的符号与目标的对应关系(大概知道有这个东西就行,当然不止这些,我懒得写而已):


初始化

类加载过程的最后一个阶段,准备阶段的时候,变量已经赋值过一次初始值,在初始化阶段,代码中通过主观计划初始化类变量,和其他资源,初始化阶段就是执行类构造器方法(<clinit>()方法)的过程。

什么是<clinit>()?

<clinit>()方法是编译器自动收集类中的所有类静态变量的赋值动作(比如public static int value = 666 中的666赋值)和静态初始化块(static代码块)的语句合并产生的方法。

static {
   yu = 6 ;//给变量赋值可以通过编译,
   int ss = yu; //但是这句,引用变量是不能通过编译的
}
public static int yu = 8 ;

初始化时机:

以下情况不会对类进行初始化:

类加载器概念

虚拟机将类的加载过程中获取字节码的工作交给类加载器来完成。

类加载器种类

除启动类加载器外,其他类加载器都是直接或者间接地继承了ClassLoader这个抽象类。

父子关系如下:


说明:通过ClassLoader的getParent()方法可以获得父类加载器,但这种父子关系并不是继承关系,是通过组合实现的,如果不知道组合是什么可以看一下这篇文章《继承与组合》

ClassLoader的主要方法

推荐自己看一下ClassLoader的源码,可以利用IDE的一些快捷键方便自己,通过debug单步执行,这样查看会清晰很多。

双亲委派机制

介绍:

如果一个类加载器收到加载类的请求,它是首先交给父加载器完成加载,依次委托直到顶层类加载器BootStrap ClassLoader,如果不是该类加载器的职责,它会交给下层去加载,依次往下直到找不到合适的类加载器就抛出ClassNotFoundException异常。

优点:

破坏双亲委派机制

双亲委派机制并不是强制规范,是可以破坏的,有些场景是需要破坏它的这种委托机制的。比如某些情况基于特定要求重写了loadClass方法。又比如一个经典的情况:

//1
Class.forName("com.mysql.jdbc.Driver");
//2.
DriverManager.getConnection(url);

1和2这两句代码相信你再熟悉不过了,1是通过类全限定名去加载mysql的驱动,一般自定义类是由AppClassLoader加载,而2是由Bootstrap ClassLoader去加载,那么它怎么获得mysql实现的类的实例的?原来,在DriverManager内部实现中,不是使用启动类加载器去加载,而是使用调用了DriverMannager.getConnection方法的类的当前线程类加载器去加载,默认的当前线程上下文类加载器是AppClassLoader,也就是使用了AppClassLoader加载mysql实现类。这给我们有所启发,可以修改当前线程上下文类加载器,然后用当前线程上下文类加载器去加载具体实现类,实现破坏双亲委派机制,这在SPI(服务提供者接口,Service Provider Interface)中很常见。

//修改的方法。
Thread.currentThread().setContextClassLoader(classLoader);

类加载器主要应用

下面通过一个小demo,介绍一下怎么自定义类加载器,并通过它加载类。

package wyn.test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public class DemoTest {
    @Test
    public void classLoaderTest() throws Exception {
        List list = (List) new MyClassLoader().loadClass("wyn.test.MyList").newInstance();
        System.out.println("list.size = " + list.size() + "\nClassLoader:" + list.getClass().getClassLoader());
    }

}
class MyClassLoader extends ClassLoader{
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Path path = Paths.get("D:\\" + name.replaceAll("\\.","\\\\") + ".class");
        byte[] classData = null;
        try {
            classData = Files.readAllBytes(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return defineClass(name,classData,0,classData.length);
    }
}

//MyList.java
package wyn.test;
import java.util.*;
public class MyList extends ArrayList{
    @Override
    public int size() {
        return 666;
    }
}

将MyList.java放到D盘,然后cmd下面执行javac -d . MyList.java编译,然后就可以通过IDE执行上面的DemoTest的classLoaderTest方法:
运行结果如下:

参考

对你若有帮助点个赞吧

上一篇 下一篇

猜你喜欢

热点阅读