JVM学习笔记4—类加载机制及面试题整理

面试题
1、说说java类加载机制
2、JDK 动态类加载
3、如果加了final,是在类加载的什么时候分配的
4、Java类加载器有哪些,为什么使用双亲委派模型
5、类加载机制详细介绍一下,如何实现一个自定义类加载器
6、Java类加载器有哪些各个类加载器主要负责哪些部分的类加载?一个类的加载过程?一个自己实现的String类, 当new String时,这个String是jdk的String还是自己写的String?如何才能让ApplicationClassLoader加载自己写的String?

一、虚拟机类加载
我们在java编译器中编写的java代码要想在虚拟机上运行并不像我们表面看到的那么简单,它要经过一系列的处理过程才能实现在虚拟机上的最终运行。因为java虚拟机不和包括java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,因此我们首先要使用Java编辑器将Java代码编译为存储字节码的Class文件,如下图所示:


虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

类加载时机
类从被加载到虚拟机内存中开始,到卸载出内存位置,它的生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading) 七个阶段。

如编写面向接口的应用程序,可以等到运行时再指定实际的实现类。在java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的。这种策略虽然会令加载是稍微增加一些性能开销,但是会为java程序提供高度的灵活性。 java里天生可以动态扩展语言的特性就是依赖运行期动态加载到和动态连接到这个特点实现的。
1、加载
1)什么情况下虚拟机需要开始加载一个类呢?
 
虚拟机规范中并没有对此进行强制约束,这点可以交给虚拟机的具体实现来自由把握。类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它。类的加载是通过类加载器(Classloader)完成的,它既可以是饿汉式[eagerly load](只要有其它类引用了它就加载)加载类,也可以是懒加载[lazy load](等到类初始化发生的时候才加载)。不过我相信这跟不同的JVM实现有关,然而他又是受JLS保证的(当有静态初始化需求的时候才被加载)。

2)加载阶段干什么

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口

3)加载过程

 通过一个类的全限定名来获取其定义的二进制字节流。
 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

2、连接
注意: 连接阶段是与加载阶段交叉进行的(如一部分字节码得文件格式验证动作)。

1)验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

 文件格式验证:主要验证字节流是否符合Class文件格式的规范,如果符合则把字节流加载到方法区中进行存储。

 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外、父类是否继承了不被允许的类。类中的方法是否与父类有矛盾等。

 字节码验证:最复杂的一个阶段。主要目的是通过数据量和控制流分析,确定程序语义是合法的,符合逻辑的。保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。

 符号引用验证:符号引用中通过字符串描述的全限定名是否能找到对应的类。在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。

2)准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

 这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。 如: public static int value = 3 另外变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的public static指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。如:public static final int value = 3 编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。
示例变量:也叫对象变量、类成员变量;从属于类由类生成对象时,才分配存储空间,各对象间的实例变量互不干扰,能通过对象的引用来访问实例变量。但在Java多线程中,实例变量是多个线程共享资源,要注意同步访问时可能出现的问题。

<span style="font-size:14px;">public class Demo {
    //以下都是实例变量(成员变量、对象变量)
    private String nameString;
    public int age;
    protected int priority;
    //实例方法
    public String getNameString(){
        return this.nameString;
    }
}</span>
1
2
3
4
5
6
7
8
9
10
类变量:也叫静态变量,是一种比较特殊的实例变量,用static关键字修饰;一个类的静态变量,所有由这类生成的对象都共用这个类变量,类装载时就分配存储空间。一个对象修改了变量,则所以对象中这个变量的值都会发生改变。


<span style="font-size:14px;">public class Demo {
    //类变量(静态变量)
    public static int a = 0;
    //实例变量
    private String nameString;
}</span>
1
2
3
4
5
6
7
3)解析

当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

 符号引用:简单的理解就是字符串,比如引用一个类,java.util.ArrayList 这就是一个符号引用,字符串引用的对象不一定被加载。
 直接引用:指针或者地址偏移量。引用对象一定在内存(已经加载)。
3、初始化
1)什么时候初始化

只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

 创建类的实例,也就是new的方式
 访问某个类或接口的静态变量,或者对该静态变量赋值
 调用类的静态方法
 反射(如Class.forName(“com.shengsiyuan.Test”))
 初始化某个类的子类,则其父类也会被初始化
 Java虚拟机启动时被标明为启动类(mian()方法)的类(Java Test),直接使用java.exe命令来运行某个主类
2)初始化干什么

初始化阶段是执行类构造器<cinit>()方法的过程。

<cinit>()方法是由编辑器收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在原文件中出现的顺序所决定的,静态语句中只能访问到定义到静态语句之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如下:
public class Test {
    static {
        i = 1;          // 给变量赋值会正常通过
        System.out.println(i);       //这句编译器会提示“非法前向引用”
    }
    static int i = 0;
}
1
2
3
4
5
6
7
<cinit>()方法与类构造函数(或者说实例构造器<init>()方法)不同,它不需要显示的调用父类构造器,虚拟机在保证子类的<cinit>()方法执行之前,父类的<cinit>()已经执行完毕。因此在虚拟机中第一个被执行的<cinit>()方法的类肯定是java.lang.Object 。
由于父类的<cinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,字段B的值将会是2不是1。特别地,类构造器()对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器()。如下所示:

父类SuperClass.java 
public class SuperClass {  
    static{  
         System.out.println("SuperClass init!");  
      }  
     public static int value = 123;  
}  
子类SubClass.java
     public class SubClass extends SuperClass {  
     static{  
        System.out.println("SubClass init!");  
    }  
}  
 
主类NotInitialization.java
public class NotInitialization {  
    public static void main(String[] args) {  
        System.out.println(SubClass.value);  
    }  
}
输出:
    SuperClass init! 
    123  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
由结果可以看出只输出了“SuperClass init!”,没有输出“SubClass init!”。这是因为对于静态字段,只有直接定义该字段的类才会被初始化,因此当我们通过子类来引用父类中定义的静态字段时,只会触发父类的初始化,而不会触发子类的初始化。


public class ConstClass {
 
   static {
 
    system.out.printl("const");
 
}
 
   public static final int age =123;  
 
}
 
public class NotInitialization{
 
     public static void main(String[ ] args){
 
     system.out.println(ConstClass.age);
 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
此时并不会出现 “const”,因为在NotInitialization类在编译的时候已经把ConstClass中的变量age放在常量池中了,访问时直接取出age即可,不会引发ConstClass的初始化。

虚拟机会保证一个类的类构造器()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器(),其他线程都需要阻塞等待,直到活动线程执行()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的,如下所示:
public class DealLoopTest {
    static{
        System.out.println("DealLoopTest...");
    }
    static class DeadLoopClass {
        static {
            if (true) {
                System.out.println(Thread.currentThread()
                        + "init DeadLoopClass");
                while (true) {      // 模拟耗时很长的操作
                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = new Runnable() {   // 匿名内部类
            public void run() {
                System.out.println(Thread.currentThread() + " start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " run over");
            }
        };

        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}

Output: 
        DealLoopTest...
        Thread[Thread-1,5,main] start
        Thread[Thread-0,5,main] start
        Thread[Thread-1,5,main]init DeadLoopClass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
二、类加载器
1)类加载器作用

Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。除此之外,对于任意一个类,都需要加载它的类加载器和这个类本身来确定这个类在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。也就是说,如果比较两个类是否是同一个类,除了这比较这两个类本身的全限定名是否相同之外,还要比较这两个类是否是同一个类加载器加载的。即使同一个类文件两次加载到同一个虚拟机中,但如果是由两个不同的类加载器加载的,那这两个类仍然不是同一个类。如下面代码:

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException{
                try {
                    String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(filename);
                    if(is == null){
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                }
                catch (IOException e){
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj= myLoader.loadClass("Test.ClassLoaderTest").newInstance();  //生成ClassLoaderTest类示例
        System.out.println(obj.getClass().getClassLoader());    //输出obj的类加载器
        System.out.println(ClassLoaderTest.class.getClassLoader());   //ClassLoaderTest输出的类加载器
        System.out.println(obj instanceof ClassLoaderTest);    
    }
}

output:
    Test.ClassLoaderTest$1@74a14482
    sun.misc.Launcher$AppClassLoader@18b4aac2
    false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
由上面可得,虽然都是ClassLoaderTest类,但是因为类加载器不同,因此输出的是false。

2)类加载器有哪几种

绝大多数Java程序都会使用以下三种系统提供的Java类:

 启动(Bootstrap)类加载器: 引导类装入器是用本地代码实现的类装入器,它负责将 jdk中jre/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
bootstrap classloader是由C++写成的,所以在Java中无法获得它的引用(会返回Null)
 扩展(ExtClassLoader)类加载器: 扩展类加载器是由Sun的(sun.misc.Launcher$ExtClassLoader)实现的。它负责将jdk中jre/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
 系统类加载器(System ClassLoader: 系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。
三、双亲委派模型
1)双亲委派模式原理

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它是一种任务委派模式。
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:


2)双亲委派模型的工作流程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

3)双亲委派模型优点(为什么用)

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

java.lang.SecurityException: Prohibited package name: java.lang
1
四、java类加载器
要实现自定义类加载器,我们首先来看一下Java中类加载器是如何实现的

Java 中ClassLoader类 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。
loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。下面我们通过源码来具体看一下这三个方法:

loadClass()

该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。:

class ClassLoader {

  // 加载入口,定义了双亲委派规则
  Class loadClass(String name) {
    // 是否已经加载了
    Class t = this.findFromLoaded(name);
    if(t == null) {
      // 交给双亲
      t = this.parent.loadClass(name)
    }
    if(t == null) {
      // 双亲都不行,只能靠自己了
      t = this.findClass(name);
    }
    return t;
  }
  
  // 交给子类自己去实现
  Class findClass(String name) {
    throw ClassNotFoundException();
  }
  
  // 组装Class对象
  Class defineClass(byte[] code, String name) {
    return buildClassFromCode(code, name);
  }
}

class CustomClassLoader extends ClassLoader {

  Class findClass(String name) {
    // 寻找字节码
    byte[] code = findCodeFromSomewhere(name);
    // 组装Class对象
    return this.defineClass(code, name);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass(“className”),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。

findClass(String name)

在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。

//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}
1
2
3
4
需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常。ClassLoader类是顶层的类加载器,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),如下图所示:

因此下面具体的类加载器都需要根据实际的情况重写findClass(String name)来具体实现类加载。如URLClassLoader类的findClass(String name)方法如下:

    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);      //尝试使用defineclass将字节码抓转化为class文件。
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
同时应该知道的是findClass方法通常是和defineClass方法一起使用的(如上面代码所示)。

defineClass(String name)

defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用。

五、自定义类加载器
1、什么时候使用自定义类加载器

 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。
 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。
 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。
2、怎么实现类加载器

一般来说,自己开发的类加载器只需要覆写findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了双亲委派模型的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写findClass()方法。如下图所示:


下面来看一个自定义类加载器的示例,这里我们继承ClassLoader实现自定义的特定路径下的文件类加载器并加载编译后DemoObj.class,源码代码如下:

public class DemoObj {
    @Override
    public String toString() {
        return "I am DemoObj";
    }
}
1
2
3
4
5
6
package com.zejian.classloader;

import java.io.*;

/**
 * Created by zejian on 2017/6/21.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class FileClassLoader extends ClassLoader {
    private String rootDir;

    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    /**
     * 编写findClass方法的逻辑
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 获取类的class文件字节数组
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            //直接生成class对象
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 编写获取class文件并转换为字节码流的逻辑
     * @param className
     * @return
     */
    private byte[] getClassData(String className) {
        // 读取类文件的字节
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            // 读取类文件的字节码
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 类文件的完全路径
     * @param className
     * @return
     */
    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

    public static void main(String[] args) throws ClassNotFoundException {
        String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
        //创建自定义文件类加载器
        FileClassLoader loader = new FileClassLoader(rootDir);

        try {
            //加载指定的class文件
            Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
            System.out.println(object1.newInstance().toString());
          
            //输出结果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
显然我们通过getClassData()方法找到class文件并转换为字节流,并重写findClass()方法,利用defineClass()方法创建了类的class对象。在main方法中调用了loadClass()方法加载指定路径下的class文件,由于启动类加载器、拓展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将有自定义类加载器加载,即调用findClass()方法进行加载。

六、能不能自己写个类,也叫java.lang.String?
不可以

因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类,
加载该自定义的String类,该自定义String类使用的加载器是AppClassLoader,根据优先使用父类加载器原理,
AppClassLoader加载器的父类为ExtClassLoader,所以这时加载String使用的类加载器是ExtClassLoader,
但是类加载器ExtClassLoader在jre/lib/ext目录下没有找到String.class类。然后使用ExtClassLoader父类的加载器BootStrap,父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。这就是类加载器的委托机制。
另外,自定义包不能以java.xxx.xxx开头,这是一种安全机制,,如果以java开头,系统直接抛异常。

如果想自己实现String类并让ApplicationClassLoader加载自己写的String不妨重新设定一下包名。

参考
https://zhuanlan.zhihu.com/p/41672523
https://blog.csdn.net/javazejian/article/details/73413292
深入理解Java虚拟机 周志明
————————————————
版权声明:本文为CSDN博主「天天~」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_42784951/article/details/100041363

发布了51 篇原创文章 · 获赞 80 · 访问量 93万+

猜你喜欢

转载自blog.csdn.net/xiyang_1990/article/details/103865766
今日推荐