类加载器

  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载->验证->准备->解析->初始化->使用->卸载。其中,验证、准备、解析统称为链接。

  我们知道,类加载的过程分为:加载->验证->准备->解析->初始化。

  加载:主要完成三件事(1.通过类的全限定名来获取定义此类的二进制字节流;2.将这个字节流所代表的静态;3.在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口),类加载的时机在最后补充中说明。

  验证:类装入器对类的字节码要做许多检测,以确保格式正确、行为正确。

  准备:准备代表类中定义的字段、方法和实现接口所必须的数据结构。(类变量在这一阶段被赋予系统初值)

  解析:装入器装入类所引用的其他类。可以用多种方式引用类,比如超类、接口、字段、方法签名、方法中使用的本地变量。

  初始化:类中包含的静态初始化器都被执行,这一阶段末尾静态字段被初始化默认值。(包括基本数据类型作为成员属性有默认值,而方法内部没有默认值是因为方法内部没有初始化这一阶段;类变量在这一阶段被赋予程序员定义的初值)

 

  虚拟机设计团队把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为"类加载器"。

补充:static 类变量和static final修饰的变量还稍微有点区别

static静态变量:加载时准备阶段(赋默认值)、初始化阶段赋给定值

static final:编译时(准备阶段)赋给定值

例如:

package staticfinal;

public class Client {

    public static final int value = 3; // 编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。

    public static int value2 = 4; // 准备阶段虚拟机值设为默认值0, 初始化阶段改为4

}

反编译查看信息如下;

E:\xiangmu\MvnPro\target\classes\staticfinal>javap -v -l Client.class
Classfile /E:/xiangmu/MvnPro/target/classes/staticfinal/Client.class
  Last modified 2021-3-9; size 397 bytes
  MD5 checksum d4cf3152052f791d7eae69b9b7b9807d
  Compiled from "Client.java"
public class staticfinal.Client
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#21         // staticfinal/Client.value2:I
   #3 = Class              #22            // staticfinal/Client
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               value
   #6 = Utf8               I
   #7 = Utf8               ConstantValue
   #8 = Integer            3
   #9 = Utf8               value2
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lstaticfinal/Client;
  #17 = Utf8               <clinit>
  #18 = Utf8               SourceFile
  #19 = Utf8               Client.java
  #20 = NameAndType        #10:#11        // "<init>":()V
  #21 = NameAndType        #9:#6          // value2:I
  #22 = Utf8               staticfinal/Client
  #23 = Utf8               java/lang/Object
{
  public static final int value;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 3

  public static int value2;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public staticfinal.Client();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lstaticfinal/Client;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_4
         1: putstatic     #2                  // Field value2:I
         4: return
      LineNumberTable:
        line 7: 0
}
SourceFile: "Client.java"

 

1. 类与类加载器 

  类加载器只用于实现类的加载动作,但在java程序中的作用却远远不限于类加载阶段。ClassLoader顾名思义就是类加载器,负责将Class加载到JVM中,还有一个重要的作用就是审查每个类应该由谁加载,还有一个重要作用就是将Class字节码解析成JVM统一要求的对象格式。

  对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每个类加载器都有一个独立的类名称空间。这句话可以理解为:比较两个类是否"相等",只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。这里说的相等包括:类对象的equals方法、isAssignableFrom方法、isInstance()方法返回的结果,也包括使用instance关键字做对象所属关系判断等情况。

如下是不同类加载器对instance关键字结果的影响。:

package cn.qlq;
import java.io.IOException;
import java.io.InputStream;

public class ClassloaderDemo {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //自定义类加载器
        ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream resourceAsStream = getClass().getResourceAsStream(fileName);
                    if (resourceAsStream == null) {
                        return super.loadClass(name);
                    }
                    System.out.println("classloader:"+getClass().getResource(fileName).getPath());
                    byte []bs = new byte[resourceAsStream.available()];
                    resourceAsStream.read(bs);
                    return defineClass(name, bs,0,bs.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return super.loadClass(name);
            }
        };
        
        Object newInstance = classLoader.loadClass("cn.qlq.ClassloaderDemo").newInstance();
        System.out.println(newInstance.getClass());
        System.out.println(newInstance.getClass().getClassLoader());
        
        System.out.println(newInstance instanceof cn.qlq.ClassloaderDemo);
    }
}

结果: (我们知道匿名内部类也会产生class文件并进行加载,匿名内部类和外部类使用的相同的类加载器)

classloader:/E:/xiangmu/Mytest/bin/cn/qlq/ClassloaderDemo.class
classloader:/E:/xiangmu/Mytest/bin/cn/qlq/ClassloaderDemo$1.class
class cn.qlq.ClassloaderDemo
cn.qlq.ClassloaderDemo$1@570f80a9
false

 

  上面代码的加载器可以加载与自己在同一路径下的class文件,使用instanceof返回的是false,是因为虚拟机中存在了两个ClassloaderDemo类,一个是由系统应用程序类加载器加载的,另一个是由自定义类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时返回false。

 

补充:ClassLoader的几个重要的常用的方法

  defineClass方法用于将byte字节流解析成JVM能够识别的Class对象,这个方法意味着我们不仅可以通过class文件实例化对象,还可以通过其他方式实例化对象,如我们通过网络接收到一个类的字节码,拿这个字节码流直接创建类的Class对象形式实例化对象。注意:如果直接调用这个方法生成类的Class对象,这个类的Class对象还没有resolve,这个resolve将会在这个对象真正实例化时才进行。

  这个方法通常是和findClass()一起使用的,我们通过覆盖ClassLoader的findClass方法来实现类的加载规则,从而取得要加载类的字节码。然后调用defineClass方法生产类的Class对象,如果你想在类被加载到JVM中时就被链接(Link),那么接着可以调用另外一个resolvClass方法,当然你也可以选择让JVM来解决什么时候才链接这个类。

  如果不想重新定义加载类的规则,也没有复杂的处理逻辑,只想在运行时能够加载自己指定的一个类,那么你可以用 this.getClass().getClassLoader().loadClass("class")调用ClassLoder的loadClass方法以及获取这个类的Class对象,这个loadClass还有重载方法。

  ClassLoader是一个抽象类,它还有很多子类,我们如果要实现自己的ClassLoader,一般都会继承URLClassLoader这个类,因为这个类已经帮我们实现了大部分工作。

  ClassLoader还有一些辅助方法,比如获取class文件的方法 getResource、getResourceAsStream等,还有就是SystemClassLoader的方法等。

 

补充:JVM加载class文件到内存的两种方式

(1)隐士加载:JVM自动加载所需的类到内存。例如,当我们在类中继承或者引用某个类时,JVM在解析这个类时发现引用的类不存在就会自动将这个类加载到内存中。

(2)显示加载:代码中通过调用ClassLoader类来加载,例如:this.getClass().getClassLoader().loadClass("class")或者Class.forName()或者我们自己实现的ClassLoader的findClass()方法等。

上面两种方式是混合用的,例如:我们通过自定义的ClassLoader显示加载一个类时,这个类中引用了其他类,那么这些类就是隐式加载的。

 

补充:加载class的过程

 

  第一个阶段是找到.class文件并把这个文件包含的字节码加载到内存中。

  第二个阶段又可以分为三个步骤:分别是字节码验证、Class类数据结构分析以及相应的内存分配和最后的符号表连接。

  第三个阶段是类中的静态属性和初始化赋值、以及静态块的执行。

 

2.双亲委派模型

  从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

  从Java开发人员的角度来看,类加载器还可以划分的更细一些,绝大部分都会使用到以下3种系统提供的类加载器。

(1)启动类加载器(Bootstrap Classloader):  

  负责加载 jdk.../jre/lib/xxx.jar (例如:rt.jar)或者被 -Xbootclasspath参数指定的路径的jar,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录也无法被加载)类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那么直接使用null代替即可,如下:

 

(2)扩展类加载器(Extension Classloader): 这个类加载器由sun.misc.launcher$ExtClassLoader实现,加载 jdk.../jre/lib/ext/xxx.jar 或者 java.ext.dirs指定的路径的类库,开发者可以直接使用扩展类加载器

(3)应用程序类加载器(Application ClassLoader): 这个类加载器由sun.misc.launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的 getSystemClassLoader()方法的返回值,所以一般称之为系统类加载器。负责加载用户类路径(ClassPath)上指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义类加载器,一般情况下这个就是程序的默认类加载器。

例如:

package cn.qlq;
import java.io.IOException;
import java.io.InputStream;

public class ClassloaderDemo {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //自定义类加载器
        ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream resourceAsStream = getClass().getResourceAsStream(fileName);
                    if (resourceAsStream == null) {
                        return super.loadClass(name);
                    }
                    byte []bs = new byte[resourceAsStream.available()];
                    resourceAsStream.read(bs);
                    return defineClass(name, bs,0,bs.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return super.loadClass(name);
            }
        };
        
        Object newInstance = classLoader.loadClass("cn.qlq.ClassloaderDemo").newInstance();
        System.out.println(newInstance.getClass().getClassLoader());
        
        System.out.println(cn.qlq.ClassloaderDemo.class.getClassLoader());//APP
        System.out.println(cn.qlq.ClassloaderDemo.class.getClassLoader().getParent());//Ext
        System.out.println(cn.qlq.ClassloaderDemo.class.getClassLoader().getParent().getParent());//null
        
        System.out.println(Object.class.getClassLoader());
    }
} 

结果: (启动类加载器获取不到)

cn.qlq.ClassloaderDemo$1@570f80a9
sun.misc.Launcher$AppClassLoader@5736ab79
sun.misc.Launcher$ExtClassLoader@4633c1aa
null
null

 

   我们的应用程序由这三种加载器互相配合进行加载,如果有必要,还可以加入自定义的类加载器。这些类加载器关系一般如下:

  上面的这种层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的方式实现,而都是使用组合关系来复用父加载器的代码。

  双亲委派模型的工作过程是:如果一个类加载器收到了类加载器的请求,它首先不会自己尝试去加载这个类,而是吧这个请求委派给父加载器完成,每一个层次的类加载器都是如此,因此所有的请求都会委派给最终的启动类加载器,只有当父加载器反馈自己无法加载这个类(它的搜索范围没有找到所需的类)时,子加载器才尝试自己完成这个加载请求。

  使用双亲委派模型来组织类加载器之间的关系,有一个好处就是Java类随着类加载器一起具备了一种带有优先级的层次关系。例如:Java.lang.Object,它存在于rt.jar中,无论哪一个类加载器要加载这个类都是交给启动类加载器,因此Object类在程序的各种类加载器环境中都是一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类并放在程序的ClassPath中,那么系统会出现多个Object类,Java最基础的行为也就无法保证,应用程序也将变得一片混乱。(我们如果重写一个rt.jar中存在的类可以正常编译,但是永远无法被加载执行)

  双亲委派模型的实现非常简单,实现代码都集中在java.lang.ClassLoader的loadClass()方法之中:先检查是否已经被加载过,若没有被加载过,调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

 

3.破坏双亲委派模型

 双亲委派模型并不是一个强制性的约束,是Java设计者推荐给开发者的类加载实现方式。在java世界大部分都遵循双亲委派模型,但有三次破坏。

 (1)第一次是在双亲委派模型之前---也就是JDK1.2发布之前。双亲委派模型是JDK1.2之后引入的,而类加载器和抽象类java.lang.ClassLoader在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时为了向前兼容,JDK1.2之后的ClassLoader增加了一个protected的findClass(),在这之前,用户去继承ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用类加载的私有方法loadClassInternal,这个方法唯一的逻辑就是调用自己的loadClass方法。JDK1.2之后不提倡用户覆盖loadClass方法,而应当把自己的逻辑写在findClass()方法来完成加载,在loadClass方法的逻辑里如果父类加载失败,则会调用自己的findClass来完成加载,以此保证双亲委派模型。

(2)第二次是这个模型自身缺陷,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为基础,是因为它们总被用户代码调用。但是在基础类调用用户代码时,怎么办?

  比如说JNDI(Java Naming and Directory Interface,Java命名和目录接口),JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码。

(3)第三次破坏是用户对程序动态性的追求导致的,这里说的动态性一般指:代码热替换、模块热部署等,说白了就是希望程序能像计算机外设那样即插即用不用重启电脑。

 

4.常见的错误分析:

1.ClassNotFoundException

  这个异常通常发生在显示加载类的时候,通常是因为当JVM要加载指定的字节码到内存时,没有找到这个文件对应的字节码,就是这个文件并不存在。解决办法就是检查当前的classpath目录下有没有指定的文件存在。如果不知道当前的classpath路径,可以通过如下命名获取:

ClassloaderDemo.class.getClassLoader().getResource("").toString()

 

显示类加载的方式如下:

(1)通过类Class中的forName()方法

(2)通过ClassLoader中的loadClass()方法

(3)通过ClassLoader的findSystemClass方法。

例如:

package cn.qlq;

public class ClassloaderDemo {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        Class<?> forName = Class.forName("cn.qlq.ClassloaderDemo");
        Object newInstance = forName.newInstance();
        System.out.println(newInstance instanceof ClassloaderDemo);
        System.out.println(newInstance.getClass().getClassLoader());
        System.out.println("======================");
        Class<?> loadClass = ClassLoader.getSystemClassLoader().loadClass("cn.qlq.ClassloaderDemo");
        Object newInstance2 = loadClass.newInstance();
        System.out.println(newInstance2 instanceof ClassloaderDemo);
        System.out.println(newInstance2.getClass().getClassLoader());
    }
}

结果

true
sun.misc.Launcher$AppClassLoader@7d05e560
======================
true
sun.misc.Launcher$AppClassLoader@7d05e560

 

2.NoClassDefFoundError

  这个异常在第一次使用命令行执行Java类时很可能会碰到,例如:(执行jar包中某个类的方式)

java -cp exam.jar exam1

 

 原因是exam1的包名没加,所以执行jar包中的类的时候类也要加上包名。

  在JVM规范中描述了  NoClassNefFoundError 可能的情况就是使用new 关键字,属性引用某个类、继承了某个接口或类,以及方法的某个参数中引用了某个类,这时会触发JVM隐士的加载这个类发现类不存在的异常。

  解决办法同样是确保每个类都在当前的classpath下。

 

3.UnsatisfiedLinkError

  这个异常不是很常见,但是出错的话通常是在JVM启动的时候,一不小心将在JVM中的某个lib删除了,可能报这个错。也可能是在解析native标识的方法时JVM找不到对应的本机库文件时出现。

 

4.ClassCaseException

  这个错误很常见,通常在程序中出现强制类型转换时出现这个错误。

 

5.ExceptionInInitializerError

 这个错误在JVM规范是这样定义的:

(1)如果JVM虚拟机如果试图创建类   ExceptionInInitializerError  的实例,但是因为出现 Out-Of-Memory-error 而无法创建新实例,那么就会抛出OutOfMemoryError对象代替

(2)如果初始化器抛出一些Exception,而且Exception类不是Error或者它的某个子类,那么就会创建 ExceptionInInitializerError 类的一个新实例,并用Exception作为参数,用这个实例代替Exception。

 

5.如何实现自己的类加载器

ClassLoader能够完成的事情无非有以下几种情况:

(1)在自定义路径下查找自定义的class文件,也许我们需要的class文件并不总是在已经设置好的ClassPath下面,那么我们必须想办法找到这个类,在这种情况下需要自己实现一个ClassLoader

(2)对我们自己的要加载的类做特殊处理,如保证通过网络传输的类的安全性,可以将类进行加密后进行传输,在加载到JVM之前对类的字节码进行解密,这个过程可以在自定义ClassLoader中实现。

(3)可以定义类的实现机制,如果我们可以检查已经加载的class文件是否被修改,如果已经被修改了,可以重新加载这个类实现热部署。

1.加载自定义路径下的class文件

自定义类加载器加载指定路径的class文件:

package cn.qlq;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class PathClassLoader extends ClassLoader{
    private String classPath;

    public PathClassLoader(String classPath) {
        this.classPath = classPath;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte []bs=getData(name);
            if (bs == null) {
                throw new ClassNotFoundException();
            }
            return defineClass(name, bs, 0, bs.length);
    }
    
    private byte[] getData(String className) {
        String path = classPath +File.separator+className.replace(".", File.separator)+".class";
        try {
            InputStream inputStream =new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte []buffer = new byte[2048];
            int num = 0;
            while((num = inputStream.read(buffer))!=-1){
                stream.write(buffer,0,num);
            }
            inputStream.close();
            return stream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getClassPath() {
        return classPath;
    }

    public void setClassPath(String classPath) {
        this.classPath = classPath;
    }
}

 

测试:G盘下放Test.class,代码如下:

public class Test {
    public Test(){
        System.out.println("constructor");
    }
}

 

测试代码:

        PathClassLoader pathClassLoader = new PathClassLoader("G:/");
        Class<?> loadClass = pathClassLoader.loadClass("Test");
        Object newInstance = loadClass.newInstance();
        System.out.println(newInstance.getClass().getClassLoader());

结果:

constructor
cn.qlq.PathClassLoader@15db9742

 

  • 我们可以改造上面的代码读取指定包中的类,如果不是指定的包就交给父类加载器执行:修改上面findClass的代码
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 如果不是cn.qlq包下的就交给父类加载器加载
        if (!name.startsWith("cn.qlq")) {
            return super.findClass(name);
        }

        byte[] bs = getData(name);
        if (bs == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, bs, 0, bs.length);
    }

 

测试代码仍然同上,结果:

Exception in thread "main" java.lang.ClassNotFoundException: Test
    at java.lang.ClassLoader.findClass(ClassLoader.java:530)
    at cn.qlq.PathClassLoader.findClass(PathClassLoader.java:20)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at cn.qlq.PathClassLoader.main(PathClassLoader.java:59)

 

修改我们的Test.java如下:

package cn.qlq;
public class Test {
    public Test(){
        System.out.println("constructor");
    }
}

 

测试代码如下:

        PathClassLoader pathClassLoader = new PathClassLoader("G:/");
        Class<?> loadClass = pathClassLoader.loadClass("cn.qlq.Test");
        Object newInstance = loadClass.newInstance();
        System.out.println(newInstance.getClass().getClassLoader());

 

补充:或者自定义类继承   URLClassLoader

package cn.qlq;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class URLPathClassLoader extends URLClassLoader {

    public URLPathClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> findLoadedClass = findLoadedClass(name);
        if (findLoadedClass != null) {
            return findLoadedClass;
        }
        return super.findClass(name);// 直接调用父类加载即可
    }
}

 

测试代码:

        URL[] urls = new URL[] { new URL("file:///G:\\") };
        ClassLoader parent = ClassLoader.getSystemClassLoader();
        URLPathClassLoader urlPathClassLoader = new URLPathClassLoader(urls, parent);
        Class<?> loadClass = urlPathClassLoader.findClass("cn.qlq.Test");
        Object newInstance = loadClass.newInstance();
        System.out.println(newInstance.getClass().getClassLoader());

结果:

constructor
cn.qlq.URLPathClassLoader@15db9742

 

注意:URL的协议,我们访问本地文件的协议是file,file协议的基本格式如下:

file:///文件路径

 

比如我们通过浏览器打开本地pdf文件,URL如下:

获取ClassPath的完整路径

        String string = URLPathClassLoader.class.getClassLoader().getResource("").toString();
        System.out.println(string);

结果:

file:/E:/xiangmu/Mytest/bin/

 

2.加载自定义格式的class文件

  假设从远程上下载一个class文件的字节码,但是为了安全,在传输的时候需要进行加密,然后通过网络传输,这时候我们加载的时候再获取到数据之后需要进行解码才能还原成原来的格式。这个就是在获取到数据之后进行一下解码就可以了。

 

3.实现类的热部署

  我们知道,JVM在加载类之前会检查请求的类是否已经被加载过来,也就是调用findLoadedClass查看是否能够返回类实例。如果已经加载过来,再调用loadClass()会导致类冲突。但是JVM表示一个类是否是同一个类有两个条件:类的完整类名和加载类的类加载器是否是同一个实例。即使是一个ClassLoader类的两个实例加载同一个类也会不一样。所以要实现热部署可以创建不同的ClassLoader的实例对象,然后通过这个不同的实例对象来加载同名的类。

代码还是上面代码,测试如下:

        PathClassLoader pathClassLoader = new PathClassLoader("G:/");
        Class<?> loadClass = pathClassLoader.findClass("cn.qlq.Test");
        Object newInstance = loadClass.newInstance();
        System.out.println(newInstance.getClass().getClassLoader());
        System.out.println("===========================");
        PathClassLoader pathClassLoader2 = new PathClassLoader("G:/");
        Class<?> loadClass2 = pathClassLoader2.findClass("cn.qlq.Test");
        Object newInstance2 = loadClass2.newInstance();
        System.out.println(newInstance2.getClass().getClassLoader());

结果:

constructor
cn.qlq.PathClassLoader@15db9742
===========================
constructor
cn.qlq.PathClassLoader@7852e922

 

如果是一个ClassLoader多次加载同一个类:

        PathClassLoader pathClassLoader = new PathClassLoader("G:/");
        Class<?> loadClass = pathClassLoader.findClass("cn.qlq.Test");
        Object newInstance = loadClass.newInstance();
        System.out.println(newInstance.getClass().getClassLoader());
        System.out.println("===========================");
        Class<?> loadClass2 = pathClassLoader.findClass("cn.qlq.Test");
        Object newInstance2 = loadClass2.newInstance();
        System.out.println(newInstance2.getClass().getClassLoader());

结果:

constructor
cn.qlq.PathClassLoader@15db9742
===========================
Exception in thread "main" java.lang.LinkageError: loader (instance of cn/qlq/PathClassLoader): attempted duplicate class definition for name: "cn/qlq/Test"
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at cn.qlq.PathClassLoader.findClass(PathClassLoader.java:22)
at cn.qlq.PathClassLoader.main(PathClassLoader.java:58)

 

修改上面的findClass代码,判断是否已经加载过:

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> findClass = findLoadedClass(name);
        if (findClass != null) {
            System.out.println("已经存在");
            return findClass;
        }
        byte[] bs = getData(name);
        if (bs == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, bs, 0, bs.length);
    }

结果:

constructor
cn.qlq.PathClassLoader@15db9742
===========================
已经存在
constructor
cn.qlq.PathClassLoader@15db9742

 

  使用不同的ClassLoader实例加载同一个类,会不会导致JVM的PermGen区无限增大?答案是否定的,因为ClassLoader对象和其他对象一样,没有对象引用它时也会被GC。但是,被这个ClassLoader加载的类的字节码会保存在JVM的PermGen区,这个区在执行FullGC时才会被回收,所以如果应用程序中大量的动态类加载,FullGC又不是太频繁,也要注意PermGen区的大小,防止内存溢出。

 

6.Java应不应该动态加载类

  java有一个痛处,修改一个类,必须重启,很费时。于是就想能不能来个动态类的加载而不重启JVM。

  Java的优势正是基于共享对象的创建,达到信息的高度共享,也就是通过保存并持有对象的状态而省去类信息的重复创建和回收。我们知道,对象一旦被创建就可以被其他对象持有和利用。

  假如,我们能动态加载一个对象进入JVM,但是如何做到JVM中对象的平滑过渡?几乎不可能。虽然在JVM中对象只有一份,在理论上可以直接替换这个对象,然后更新java栈中原有对象的引用关系。看起来像是被替换了,但是仍然不可以,违反了JVM的设计原则,对象的引用关系只有对象的创建者持有和使用,JVM不可以干预对象的引用关系,因为JVM不知道对象是怎么被使用的,这就涉及JVM并不知道对象的运行时类型而只知道编译时类型。

补充:类加载的时机 

  什么时候进行类加载java虚拟机并没有进行强制约束,这点可以交给虚拟机的具体实现自己来把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有五种情况必须对类进行初始化(而加载、验证、准备自然要在此之前开始)。需要注意一个误区是并不是import类的时候就会加载相应的类。class.forName()显示加载时候也会触发加载。

(1)遇到new、getstatis、putstatic或invokestatic这4条字节码指令时,如果类没有初始化需要先触发初始化。最常见的代码场景是:new 对象,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段)的时候,以及调用一个类的静态方法的时候(类名.class除外除外,调用.class不回触发加载)。

(2)使用java.lang.reflect反射包对类进行反射调用的时候

(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

(4)虚拟机启动时,用户需要知道一个要执行的主类(带main方法的类),虚拟机会先初始化这个主类

(5)使用JDK1.7的动态语言支持时

  对于这五种会触发类初始化的场景,虚拟机规范使用了一个限定语:"有且只有",这五种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用

被动引用的例子:

例子1:通过子类引用父类的静态字段不会触发子类加载

package loader;

public class SuperClass {

    static {
        System.out.println("SuperClass init");
    }

    public static int value = 5;
}

 

package loader;

public class SubClass extends SuperClass {

    static {
        System.out.println("SubClass extends init");
    }
}

 

package loader;

public class Client {

    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
} 

结果:

SuperClass init
5

 

例子2:通过数组定义来引用类,不会触发类的初始化

package loader;

public class Client {

    public static void main(String[] args) {
        SuperClass[] superClasses = new SuperClass[20];
    }
}

  运行之后没输出SuperClass init,证明没有触发loader.SuperClass类的初始化阶段。但是这段代码触发了另一个名为"[loader.SuperClass"类的初始化,它是虚拟机自动生成的、直接继承于java.object.lang的子类,创建动作由字节码指令newarray触发。这个类代表了一个元素类型为"loader.SuperClass"的一维数组。

 

例子3:常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

package loader;

public class Constants {

    static {
        System.out.println("Constants init");
    }

    public static final String NAME = "admin";
}

 

package loader;

public class Client {

    public static void main(String[] args) {
        System.out.println(Constants.NAME);
    }
}

结果:

admin

  上述代码没有输出Constants init,这是因为在源码中虽然Client类用到了Constants类的常量NAME,但其实在编译阶段通过常量传播优化,已经将此常量的值"admin"存储到了Client类的常量池中,以后Client类对Constants.NAME的引用实际都被转化为Client类对自身常量池的引用。也就是说,这两个类在编译成Class文件之后就不存在任何联系了。

 

如果我们将声明 NAME 的关键词 final去掉会打印   Constants init, 或者我们将引用类型改为其他可变类型即使加了final也会进行打印 Constants init,如下:(因为final修饰可变引用类型表示引用不可变,但是内部属性可以改变,所以无法优化)

package loader;

import java.util.HashMap;
import java.util.Map;

public class Constants {

    public static final Map<String, Object> MAP = new HashMap<>();

    static {
        System.out.println("Constants init");
        MAP.put("name", "admin");
    }

}

 

package loader;

public class Client {

    public static void main(String[] args) {
        System.out.println(Constants.MAP);
    }
}

结果:

Constants init
{name=admin}

 

二次补充:

package cn.qz;

public class User {


    public static final int NUM_A = 10;

    static {
        System.out.println("User static ~~~");
    }

}

package cn.qz2;

import cn.qz.User;

public class Client {

    public static void main(String[] args) {
//        User user = null;
//        int numA = User.NUM_A;
        System.out.println(User.NUM_A);
    }
}

1)通过类型定义变量,类会被加载吗?
1》定义变量的形式是不会加载ClassA的
User user = null;    // 不会加载
2》常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
System.out.println(User.NUM_A); // 不会触发
int numA = User.NUM_A; // 也不会触发
UNM_A 取消final 修饰会触发加载:
int numA = User.NUM_A; // 会触发
System.out.println(User.NUM_A); //会触发
赋值时,隐式的加载了ClassA这个类,并且执行了静态代码块,也就是说隐式加载时,这个类会被读到内存中,并同时执行静态代码块

2)类加载时一定会执行静态代码块吗?
不是所有的类加载时都会执行静态代码块, 要分情况而论。
用ClassLoader类中的loadClass加指定类的全限定名的方式去 加载一个类,就是显示加载,不会触发
ClassLoader.getSystemClassLoader().loadClass("cn.qz.User"); // 不会触发

// Class.forName("cn.qz.User"); // 会触发
Class.forName("cn.qz.User", false, ClassLoader.getSystemClassLoader()); // 不会触发

可以看到这种加载的形式,类会加载,同时静态代码块也执行了。同时还可以在forName这个方法中增加参数以控制类加载时是否 执行初始化操作,也就是静态代码块是否被执行。

3)类加载时什么时候会执行静态代码块呢?
当类采用隐式加载 时会执行静态代码块,当类调用Class.forName方法执行时,默认会执行静态代码块,注意是默认,一旦修改了其中的initialize参数,也不会执行静态代码块。

 

补充:instanceof, isinstance,isAssignableFrom的区别 

1. instanceof运算符 只被用于对象引用变量,检查左边的被测试对象 是不是 右边类或接口的 实例化。如果被测对象是null值,则测试结果总是false。(自身实例或子类实例 instanceof 自身类 返回true)

String s = new String("javaisland");
System.out.println(s instanceof String); // true
System.out.println(null instanceof String); // falseString s = new String("javaisland");
System.out.println(s instanceof String); // true
System.out.println(null instanceof String); // false

2. Class类的isInstance(Object obj)方法,obj是被测试的对象,如果obj是调用这个方法的class或接口 的实例,则返回true。这个方法是instanceof运算符的动态等价(自身类.class.isInstance(自身实例或子类实例) 返回true)

String s=new String("javaisland");
System.out.println(String.class.isInstance(s)); // true

3. Class类的isAssignableFrom(Class cls)方法,如果调用这个方法的class或接口 与 参数cls表示的类或接口相同,或者是参数cls表示的类或接口的父类,则返回true。

System.out.println(ArrayList.class.isAssignableFrom(Object.class)); // false
System.out.println(Object.class.isAssignableFrom(ArrayList.class)); // true

 

posted @ 2019-01-23 18:22  QiaoZhi  阅读(778)  评论(3编辑  收藏  举报