精华内容
下载资源
问答
  • jvm之java类加载机制和类加载器(ClassLoader)的详解

    万次阅读 多人点赞 2018-08-13 15:05:46
    当程序主动使用某个类时,如果该类还未被加载到内存中,则...如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。 一、类加载过程 1.加载 加载指的是将类的class文件...

    手把手写代码:三小时急速入门springboot—企业级微博项目实战--->csdn学院

          当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

                                                        

    一、类加载过程

    1.加载    

        加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

        类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

        通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

    • 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
    • 从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
    • 通过网络加载class文件。
    • 把一个Java源文件动态编译,并执行加载。

        类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

    2.链接

        当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

        1)验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

        四种验证做进一步说明:

        文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

        元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

        字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。

        符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

       2)准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

       3)解析:将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

    3.初始化

        初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

    二、类加载时机

    1. 创建类的实例,也就是new一个对象
    2. 访问某个类或接口的静态变量,或者对该静态变量赋值
    3. 调用类的静态方法
    4. 反射(Class.forName("com.lyj.load"))
    5. 初始化一个类的子类(会首先初始化子类的父类)
    6. JVM启动时标明的启动类,即文件名和类名相同的那个类    

         除此之外,下面几种情形需要特别指出:

         对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

    三、类加载器

        类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

       JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

     1)根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

    下面程序可以获得根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:

    public class ClassLoaderTest {
    
    	public static void main(String[] args) {
    		
    		URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
    		for(URL url : urls){
    			System.out.println(url.toExternalForm());
    		}
    	}
    }

    运行结果:

      2)扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

      3)系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

    类加载器加载Class大致要经过如下8个步骤:

    1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
    2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
    3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
    4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
    5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
    6. 从文件中载入Class,成功后跳至第8步。
    7. 抛出ClassNotFountException异常。
    8. 返回对应的java.lang.Class对象。

    四、类加载机制:

    1.JVM的类加载机制主要有如下3种。

    • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
    • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
    • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

    2.这里说明一下双亲委派机制:

           双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

          双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

    展开全文
  • 类加载

    千次阅读 2020-10-10 17:23:34
    程序运行过程 从源文件创建到程序运行,Java程序要经过两大步骤:编译,运行 编译: 创建完源程序(*.java文件)后,程序会被Java编译器通过javac.exe...字节码文件通过java.exe命令通过类加载器将字节码文件对应的类

    程序运行过程

    从源文件创建到程序运行,Java程序要经过两大步骤:编译,运行

    1. 编译: 创建完源程序(*.java文件)后,程序会被Java编译器通过javac.exe命令编译为.class文件。Java编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后引用,否则直接引用。

    2. 运行:java类运行的过程大概可分为两个过程:

      类的加载;

      执行。

    类加载

    java程序经过编译后形成*.class文件。字节码文件通过java.exe命令通过类加载器将字节码文件对应的类加载到内存中。

    当程序主动使用某个类的时候,如果该类还未被加载到内存中,则系统会通过三个步骤完成对该类的初始化,这三个步骤何在一起,泛泛讲称为类的加载,细讲的话,看作第一步骤
    在这里插入图片描述

    类的加载:比如说Person类,通过java.exe命令将此类字节码文件加载到内存,并创建一个Class实例,此过程由类的加载器完成
    在这里插入图片描述

    类的链接:将类的二进制数据合并到JRE中。将类中声明为static的变量设置默认初始值。比如说类中有变量static int n ,在此链接过程中将n赋值为0

    类的初始化:JVM负责对类进行初始化。如果定义时给变量n显式赋了个值2,那么在初始化环节,会将n的值有2覆盖0。初始化中有个类构造器方法,不是之前创建类时的构造器,作用是将静态变量有显式值的话,做个初始化操作

    public class ClassLoadingTest {
        public static void main(String[] args) {
            System.out.println(A.m);  //100
        }
    }
    
    class A{
        static {
            m = 300;
        }
        static int m = 100;
    }
    
    /**
    1.首先将类A字节码加载到内存中,产生一个Class实例,对应方法区中的某一区域
    2.链接,将类变量赋值为0
    3.初始化,由构造器方法来完成,A的构造器方法由类变量的赋值和讲台代码块中的语句按照顺序合并产生
    m = 300;
    m = 100;
    静态代码块和显式赋值看谁先出现先赋值
    */
    
    

    ClassLoader 类加载器

    在这里插入图片描述

    类加载器是用来将类装载进内存的
    在这里插入图片描述

    对于自定义类,使用系统类加载器进行加载;

    调用系统类加载器的getparent(),能获取扩展类加载器;

    调用扩展类加载器的getparent(),无法获取引导类加载器;

    引导类加载器主要负责加载java的核心类库,我们无法获取到

     @Test
        public void test(){
            ClassLoader classLoader = ClassLoadingTest.class.getClassLoader();
            System.out.println(classLoader);
    
            ClassLoader loaderParent = classLoader.getParent();
            System.out.println(loaderParent);
    
            ClassLoader parent = loaderParent.getParent();
            System.out.println(parent);
        }
    

    结果展示:

    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@54bedef2
    null
    
    展开全文
  • 【9】JVM-类加载

    万次阅读 2019-12-09 23:04:06
    知识点1:类加载的机制的层次结构 步骤一、类加载机制 步骤二、连接过程 步骤三、初始化 知识点2:类加载器的层次结构 1、启动(Bootstrap)类加载器 2、扩展(Extension)类加载器 3、系统(System)类加载器 ...

    目录

    知识点1:类加载的机制的层次结构

    步骤一、类加载机制

    步骤二、连接过程

    步骤三、初始化

    知识点2:类加载器的层次结构

    1、启动(Bootstrap)类加载器

    2、扩展(Extension)类加载器

    3、系统(System)类加载器

    知识点3:双亲委派模式

    1、双亲委派模式优势

    2、类加载器间的关系

    知识点4:类加载器常用方法

    1、loadClass(String)

    2、findClass(String)

    3、defineClass(byte[] b, int off, int len)

    4、resolveClass(Class≺?≻ c)

    知识点5:热部署

    1、热部署的原理是什么

    2、热部署与热加载

    (1)Java热部署与Java热加载的联系和区别

    3、相关代码


    知识点1:类加载的机制的层次结构

    每个编写的”.java”拓展名类文件都存储着需要执行的程序逻辑,这些”.java”文件经过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下:

    Jvm执行class文件

    步骤一、类加载机制

    将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。

    当系统运行时,类加载器将.class文件的二进制数据从外部存储器(如光盘,硬盘)调入内存中,CPU再从内存中读取指令和数据进行运算,并将运算结果存入内存中。内存在该过程中充当着"二传手"的作用,通俗的讲,如果没有内存,类加载器从外部存储设备调入.class文件二进制数据直接给CPU处理,而由于CPU的处理速度远远大于调入数据的速度,容易造成数据的脱节,所以需要内存起缓冲作用。

    类将.class文件加载至运行时的方法区后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象,Class类的构造方法是私有的,只有JVM能够创建。因此Class对象是反射的入口,使用该对象就可以获得目标类所关联的.class文件中具体的数据结构。

     

    类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口。

    步骤二、连接过程

    将java类的二进制代码合并到JVM的运行状态之中的过程

    验证:确保加载的类信息符合JVM规范,没有安全方面的问题

    准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配

    解析:虚拟机常量池的符号引用替换为字节引用过程

    步骤三、初始化

    初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。

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

    虚拟机会保、、、证一个类的<clinit>()方法在多线程环境中被正确加锁和同步

    当范围一个Java类的静态域时,只有真正声名这个域的类才会被初始化


    知识点2:类加载器的层次结构

    启动(Bootstrap)类加载器

    扩展(Extension)类加载器

    系统(-)类加载器

    1、启动(Bootstrap)类加载器

    启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

    2、扩展(Extension)类加载器

    扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

    3、系统(System)类加载器

    也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

    在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。


    知识点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

    所以无论如何都无法加载成功的。下面我们从代码层面了解几个Java中定义的类加载器及其双亲委派模式的实现,它们类图关系如下

    双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成

    1、双亲委派模式优势

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

    2、类加载器间的关系

    我们进一步了解类加载器间的关系(并非指继承关系),主要可以分为以下4点

    启动类加载器,由C++实现,没有父类。

    拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null

    系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

    自定义类加载器,父类加载器肯定为AppClassLoader。


    知识点4:类加载器常用方法

    1、loadClass(String)

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

    正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。

    2、findClass(String)

    在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析)

    3、defineClass(byte[] b, int off, int len)

    defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象

    4、resolveClass(Class? c)

    使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。


    知识点5:热部署

    对于Java应用程序来说,热部署就是在运行时更新Java类文件。

    1、热部署的原理是什么

    想要知道热部署的原理,必须要了解java类的加载过程。一个java类文件到虚拟机里的对象,要经过如下过程。

    首先通过java编译器,将java文件编译成class字节码,类加载器读取class字节码,再将类转化为实例,对实例newInstance就可以生成对象。

    类加载器ClassLoader功能,也就是将class字节码转换到类的实例。

    在java应用中,所有的实例都是由类加载器,加载而来。

    一般在系统中,类的加载都是由系统自带的类加载器完成,而且对于同一个全限定名的java类(如com.csiar.soc.HelloWorld),只能被加载一次,而且无法被卸载。

    这个时候问题就来了,如果我们希望将java类卸载,并且替换更新版本的java类,该怎么做呢?

         既然在类加载器中,java类只能被加载一次,并且无法卸载。那是不是可以直接把类加载器给换了?答案是可以的,我们可以自定义类加载器,并重写ClassLoader的findClass方法。想要实现热部署可以分以下三个步骤:

    1、销毁该自定义ClassLoader

    2、更新class类文件

    3、创建新的ClassLoader去加载更新后的class类文件。

    2、热部署与热加载

    (1)Java热部署与Java热加载的联系和区别

    Java热部署与热加载的联系

    1.不重启服务器编译/部署项目

    2.基于Java的类加载器实现

     Java热部署与热加载的区别

    • 部署方式

    热部署在服务器运行时重新部署项目

    热加载在运行时重新加载class

    • 实现原理

    热部署直接重新加载整个应用

    热加载在运行时重新加载class

    •  使用场景

    热部署更多的是在生产环境使用

    热加载则更多的实在开发环境使用

    3、相关代码

    User没有被修改类

    public class User {
    
    	public void add() {
    		System.out.println("addV1,没有修改过...");
    	}
    
    }

    User更新类

    public class User {
    
    	public void add() {
    		System.out.println("我把之前的user add方法修改啦!");
    	}
    
    }

    自定义类加载器

    public class MyClassLoader extends ClassLoader {
    
    	@Override
    	protected Class<?> findClass(String name) throws ClassNotFoundException {
    		try {
    			// 文件名称
    			String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
    			// 获取文件输入流
    			InputStream is = this.getClass().getResourceAsStream(fileName);
    			// 读取字节
    			byte[] b = new byte[is.available()];
    			is.read(b);
    			// 将byte字节流解析成jvm能够识别的Class对象
    			return defineClass(name, b, 0, b.length);
    		} catch (Exception e) {
    			throw new ClassNotFoundException();
    		}
    
    	}
    
    }
    

    更新代码

    public class Hotswap {
    
    	public static void main(String[] args)
    			throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException,
    			SecurityException, IllegalArgumentException, InvocationTargetException, InterruptedException {
    		loadUser();
    		System.gc();
    		Thread.sleep(1000);// 等待资源回收
    		// 需要被热部署的class文件
    		File file1 = new File("F:\\test\\User.class");
    		// 之前编译好的class文件
    		File file2 = new File(
    				"F:\\itmayiedujiangke2018-02-24\\itmayiedu_itmayiedu_day_17\\target\\classes\\com\\itmayiedu\\User.class");
    		boolean isDelete = file2.delete();// 删除旧版本的class文件
    		if (!isDelete) {
    			System.out.println("热部署失败.");
    			return;
    		}
    		file1.renameTo(file2);
    		System.out.println("update success!");
    		loadUser();
    	}
    
    	public static void loadUser() throws ClassNotFoundException, InstantiationException, IllegalAccessException,
    			NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
    		MyClassLoader myLoader = new MyClassLoader();
    		Class<?> class1 = myLoader.findClass("com.itmayiedu.User");
    		Object obj1 = class1.newInstance();
    		Method method = class1.getMethod("add");
    		method.invoke(obj1);
    		System.out.println(obj1.getClass());
    		System.out.println(obj1.getClass().getClassLoader());
    	}
    }

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • 2 Java虚拟机类加载器结构简述 2.1 JVM三种预定义类型类加载器 2.2 类加载双亲委派机制介绍和分析 2.3 类加载双亲委派示例 3 java程序动态扩展方式 3.1 调用java.lang.Class.forName(…)加载类 3.2 用户自定义...

    目录

    1 基本信息

    2 Java虚拟机类加载器结构简述

    2.1 JVM三种预定义类型类加载器

    2.2 类加载双亲委派机制介绍和分析

    2.3 类加载双亲委派示例

    3 java程序动态扩展方式

    3.1 调用java.lang.Class.forName(…)加载类

    3.2 用户自定义类加载器

    4 常见问题分析

    4.1 由不同的类加载器加载的指定类还是相同的类型吗?

    4.2 在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

    4.3 在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?

    4.4 在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

    4.5 编写自定义类加载器时,一般有哪些注意点?

    4.6 如何在运行时判断系统类加载器能加载哪些路径下的类?

    4.7 如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

    5 开发自己的类加载器

    5.1 文件系统类加载器

    5.2 网络类加载器

    参考文献:


    1 基本信息

      每个开发人员对Java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,虽然和大部分开发人员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对理解java虚拟机的连接模型和java语言的动态性都有很大帮助。

    2 Java虚拟机类加载器结构简述

    2.1 JVM三种预定义类型类加载器

      我们首先看一下JVM预定义的三种类型类加载器,当一个 JVM启动的时候,Java缺省开始使用如下三种类型类装入器:

      启动(Bootstrap)类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

      扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

      系统(System)类加载器:系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

      除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器,这个将在后面单独介绍。

    2.2 类加载双亲委派机制介绍和分析

      在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。

      图一 标准扩展类加载器继承层次图

      图二系统类加载器继承层次图

      通过图一和图二我们可以看出,类加载器均是继承自java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下java.lang.ClassLoader中几个最重要的方法:

    //加载指定名称(包括包名)的二进制类型,供用户调用的接口  
    public Class<?> loadClass(String name) throws ClassNotFoundException{ … }  
      
    //加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用  
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }  
      
    //findClass方法一般被loadClass方法调用去加载指定名称类,供继承用  
    protected Class<?> findClass(String name) throws ClassNotFoundException { … }  
      
    //定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承  
    //(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)  
    protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }  

      通过进一步分析标准扩展类加载器(sun.misc.Launcher$ExtClassLoader)和系统类加载器(sun.misc.Launcher$AppClassLoader)的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码可以看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。既然这样,我们就可以通过分析java.lang.ClassLoader中的loadClass(String name)方法的代码就可以分析出虚拟机默认采用的双亲委派机制到底是什么模样:

    public Class<?> loadClass(String name) throws ClassNotFoundException {  
        return loadClass(name, false);  
    }  
      
    protected synchronized Class<?> loadClass(String name, boolean resolve)  
            throws ClassNotFoundException {  
      
        // 首先判断该类型是否已经被加载  
        Class c = findLoadedClass(name);  
        if (c == null) {  
            //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载  
            try {  
                if (parent != null) {  
                    //如果存在父类加载器,就委派给父类加载器加载  
                    c = parent.loadClass(name, false);  
                } else {  
                    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,  
                    //通过调用本地方法native findBootstrapClass0(String name)  
                    c = findBootstrapClass0(name);  
                }  
            } catch (ClassNotFoundException e) {  
                // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能  
                c = findClass(name);  
            }  
        }  
        if (resolve) {  
            resolveClass(c);  
        }  
        return c;  
    }  

      通过上面的代码分析,我们可以对JVM采用的双亲委派类加载机制有了更感性的认识,下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:

      图三 类加载器默认委派关系图

      上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:

    public class LoaderTest {  
      
        public static void main(String[] args) {  
            try {  
                System.out.println(ClassLoader.getSystemClassLoader());  
                System.out.println(ClassLoader.getSystemClassLoader().getParent());  
                System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }  

      说明:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器。

      代码输出如下:

    sun.misc.Launcher$AppClassLoader@6d06d69c  
    sun.misc.Launcher$ExtClassLoader@70dea4e  
    null  

      通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null。我们还是借助于代码分析一下。

      我们首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:

    protected ClassLoader() {  
        SecurityManager security = System.getSecurityManager();  
        if (security != null) {  
            security.checkCreateClassLoader();  
        }  
        //默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器  
        this.parent = getSystemClassLoader();  
        initialized = true;  
    }  
      
    protected ClassLoader(ClassLoader parent) {  
        SecurityManager security = System.getSecurityManager();  
        if (security != null) {  
            security.checkCreateClassLoader();  
        }  
        //强制设置父类加载器  
        this.parent = parent;  
        initialized = true;  
    }  

      我们再看一下ClassLoader抽象类中parent成员的声明:

    // The parent class loader for delegation  
    private ClassLoader parent;  

      声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:

      1. 系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

      2. 扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

      现在我们可能会有这样的疑问:扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?

      图四 标准扩展类加载器和系统类加载器成员大纲视图

      图五 扩展类加载器和系统类加载器公共父类成员大纲视图

      通过图四和图五可以看出,标准扩展类加载器和系统类加载器及其父类(java.NET.URLClassLoader和java.security.SecureClassLoader)都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。有关java.lang.ClassLoader中默认的加载委派规则前面已经分析过,如果父加载器为null,则会调用本地方法进行启动类加载尝试。所以,图三中,启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系事实上是仍就成立的。(在后面的用户自定义类加载器部分,还会做更深入的分析)。

    2.3 类加载双亲委派示例

      以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子。首先在IDE中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:

    package classloader.test.bean;  
      
    public class TestBean {  
          
        public TestBean() { }  
    }  

      在现有当前工程中另外建立一测试类(ClassLoaderTest.java)内容如下:

      测试一:

    package classloader.test.bean;  
      
    public class ClassLoaderTest {  
      
        public static void main(String[] args) {  
            try {  
                //查看当前系统类路径中包含的路径条目  
                System.out.println(System.getProperty("java.class.path"));  
                //调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean  
                Class typeLoaded = Class.forName("classloader.test.bean.TestBean");  
                //查看被加载的TestBean类型是被那个类加载器加载的  
                System.out.println(typeLoaded.getClassLoader());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }  

      对应的输出如下:

    C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes  
    sun.misc.Launcher$AppClassLoader@73d16e93  

      说明:当前类路径默认的含有的一个条目就是工程的输出目录。

      测试二:

      将当前工程输出目录下的TestBean.class打包进test.jar剪贴到<Java_Runtime_Home>/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:

    C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes  
    sun.misc.Launcher$ExtClassLoader@15db9742  

      对比测试一和测试二,我们明显可以验证前面说的双亲委派机制,系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。

      测试三:

      将test.jar拷贝一份到<Java_Runtime_Home>/lib下,运行测试代码,输出如下:

    C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes  
    sun.misc.Launcher$ExtClassLoader@15db9742  

      测试三和测试二输出结果一致。那就是说,放置到<Java_Runtime_Home>/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载<Java_Runtime_Home>/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除<Java_Runtime_Home>/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。

    3 java程序动态扩展方式

      Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。

      运行时动态扩展java应用程序有如下两个途径:

    3.1 调用java.lang.Class.forName(…)加载类

      这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发哪个类加载器开始加载任务。这里需要说明的是多参数版本的forName(…)方法:

    public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException  

      这里的initialize参数是很重要的。它表示在加载同时是否完成初始化的工作(说明:单参数版本的forName方法默认是完成初始化的)。有些场景下需要将initialize设置为true来强制加载同时完成初始化。例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题。因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用。这就要求驱动程序类必须被初始化,而不单单被加载。Class.forName的一个很常见的用法就是在加载数据库驱动的时候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载 Apache Derby 数据库的驱动。

    3.2 用户自定义类加载器

      通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般用户自定义类加载器的工作流程吧(可以结合后面问题解答一起看):

      1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;

      2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;

      3、调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转而抛异常,终止加载过程(注意:这里的异常种类不止一种)。

      说明:这里说的自定义类加载器是指JDK 1.2以后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下。

      整个加载类的过程如下图:

      图六 自定义类加载器加载类的过程

    4 常见问题分析

    4.1 由不同的类加载器加载的指定类还是相同的类型吗?

      在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。

    4.2 在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

      Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:

    //java.lang.Class.java  
    publicstatic Class<?> forName(String className) throws ClassNotFoundException {  
        return forName0(className, true, ClassLoader.getCallerClassLoader());  
    }  
      
    //java.lang.ClassLoader.java  
    // Returns the invoker's class loader, or null if none.  
    static ClassLoader getCallerClassLoader() {  
        // 获取调用类(caller)的类型  
        Class caller = Reflection.getCallerClass(3);  
        // This can be null if the VM is requesting it  
        if (caller == null) {  
            return null;  
        }  
        // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader  
        return caller.getClassLoader0();  
    }  
      
    //java.lang.Class.java  
    //虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法  
    native ClassLoader getClassLoader0();  

    4.3 在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?

      前面讲过,在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:

    //摘自java.lang.ClassLoader.java  
    protected ClassLoader() {  
        SecurityManager security = System.getSecurityManager();  
        if (security != null) {  
            security.checkCreateClassLoader();  
        }  
        this.parent = getSystemClassLoader();  
        initialized = true;  
    }  
      我们再来看一下对应的getSystemClassLoader()方法的实现:
    [java] view plain copy
    private static synchronized void initSystemClassLoader() {  
        //...  
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  
        scl = l.getClassLoader();  
        //...  
    }  

      我们可以写简单的测试代码来测试一下:

    System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());  

      本机对应输出如下:

    sun.misc.Launcher$AppClassLoader@73d16e93  

      所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

      1. <Java_Runtime_Home>/lib下的类;

      2. < Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类;

      3. 当前工程类路径下或者由系统变量java.class.path指定位置中的类。

    4.4 在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

      JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:

      即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<Java_Runtime_Home>/lib下的类,但此时就不能够加载<Java_Runtime_Home>/lib/ext目录下的类了。

      说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。

    4.5 编写自定义类加载器时,一般有哪些注意点?

      1、一般尽量不要覆写已有的loadClass(...)方法中的委派逻辑

      一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

    //用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)  
    public class WrongClassLoader extends ClassLoader {  
      
        public Class<?> loadClass(String name) throws ClassNotFoundException {  
            return this.findClass(name);  
        }  
      
        protected Class<?> findClass(String name) throws ClassNotFoundException {  
            // 假设此处只是到工程以外的特定目录D:\library下去加载类  
            // 具体实现代码省略  
        }  
    }  

      通过前面的分析我们已经知道,这个自定义类加载器WrongClassLoader的默认类加载器是系统类加载器,但是现在问题4种的结论就不成立了。大家可以简单测试一下,现在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程类路径上的类都加载不上了。

    //问题5测试代码一  
    public class WrongClassLoaderTest {  
      
        publicstaticvoid main(String[] args) {  
            try {  
                WrongClassLoader loader = new WrongClassLoader();  
                Class classLoaded = loader.loadClass("beans.Account");  
                System.out.println(classLoaded.getName());  
                System.out.println(classLoaded.getClassLoader());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }  

      这里D:"classes"beans"Account.class是物理存在的。输出结果:

    java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)  
        at java.io.FileInputStream.open(Native Method)  
        at java.io.FileInputStream.<init>(FileInputStream.java:106)  
        at WrongClassLoader.findClass(WrongClassLoader.java:40)  
        at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
        at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)  
        at java.lang.ClassLoader.defineClass1(Native Method)  
        at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
        at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
        at WrongClassLoader.findClass(WrongClassLoader.java:43)  
        at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
        at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
    Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object  
        at java.lang.ClassLoader.defineClass1(Native Method)  
        at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
        at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
        at WrongClassLoader.findClass(WrongClassLoader.java:43)  
        at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
        at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  

      这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。

    //问题5测试二  
    //用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)  
    public class WrongClassLoader extends ClassLoader {  
      
        protected Class<?> findClass(String name) throws ClassNotFoundException {  
            //假设此处只是到工程以外的特定目录D:\library下去加载类  
            //具体实现代码省略  
        }  
    }  

      将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:

    beans.Account  
    WrongClassLoader@1c78e57  

      2、正确设置父类加载器

      通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。

      3、保证findClass(String name)方法的逻辑正确性

      事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

    4.6 如何在运行时判断系统类加载器能加载哪些路径下的类?

      一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。

      二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty("java.class.path")。

    4.7 如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

      方法之一:

    import java.net.URL;  
    import java.net.URLClassLoader;  
      
    public class ClassLoaderTest {  
      
        /** 
         * @param args the command line arguments 
         */  
        public static void main(String[] args) {  
            try {  
                URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();  
                for (int i = 0; i < extURLs.length; i++) {  
                    System.out.println(extURLs[i]);  
                }  
            } catch (Exception e) {  
                //…  
            }  
        }  
    }  

      本机对应输出如下:

    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar  
    file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar  
    

    5 开发自己的类加载器

      在前面介绍类加载器的代理委派模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

      方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

      类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

      在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

    5.1 文件系统类加载器

      第一个类加载器用来加载存储在文件系统上的Java字节代码。完整的实现如下所示。

    package classloader;  
      
    import java.io.ByteArrayOutputStream;  
    import java.io.File;  
    import java.io.FileInputStream;  
    import java.io.IOException;  
    import java.io.InputStream;  
      
    // 文件系统类加载器  
    public class FileSystemClassLoader extends ClassLoader {  
      
        private String rootDir;  
      
        public FileSystemClassLoader(String rootDir) {  
            this.rootDir = rootDir;  
        }  
      
        // 获取类的字节码  
        @Override  
        protected Class<?> findClass(String name) throws ClassNotFoundException {  
            byte[] classData = getClassData(name);  // 获取类的字节数组  
            if (classData == null) {  
                throw new ClassNotFoundException();  
            } else {  
                return defineClass(name, classData, 0, classData.length);  
            }  
        }  
      
        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;  
        }  
      
        private String classNameToPath(String className) {  
            // 得到类文件的完全路径  
            return rootDir + File.separatorChar  
                    + className.replace('.', File.separatorChar) + ".class";  
        }  
    }  

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

      类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

      加载本地文件系统上的类,示例如下:

    package com.example;  
      
    public class Sample {  
      
        private Sample instance;  
      
        public void setSample(Object instance) {  
            System.out.println(instance.toString());  
            this.instance = (Sample) instance;  
        }  
    }  
    package classloader;  
      
    import java.lang.reflect.Method;  
      
    public class ClassIdentity {  
      
        public static void main(String[] args) {  
            new ClassIdentity().testClassIdentity();  
        }  
      
        public void testClassIdentity() {  
            String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";  
            FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);  
            FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);  
            String className = "com.example.Sample";  
            try {  
                Class<?> class1 = fscl1.loadClass(className);  // 加载Sample类  
                Object obj1 = class1.newInstance();  // 创建对象  
                Class<?> class2 = fscl2.loadClass(className);  
                Object obj2 = class2.newInstance();  
                Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);  
                setSampleMethod.invoke(obj1, obj2);  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }  

      运行输出:

    com.example.Sample@7852e922

    5.2 网络类加载器

      下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。

      类 NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。它的实现与FileSystemClassLoader类似。

    package classloader;  
      
    import java.io.ByteArrayOutputStream;  
    import java.io.InputStream;  
    import java.net.URL;  
      
    public class NetworkClassLoader extends ClassLoader {  
      
        private String rootUrl;  
      
        public NetworkClassLoader(String rootUrl) {  
            // 指定URL  
            this.rootUrl = rootUrl;  
        }  
      
        // 获取类的字节码  
        @Override  
        protected Class<?> findClass(String name) throws ClassNotFoundException {  
            byte[] classData = getClassData(name);  
            if (classData == null) {  
                throw new ClassNotFoundException();  
            } else {  
                return defineClass(name, classData, 0, classData.length);  
            }  
        }  
      
        private byte[] getClassData(String className) {  
            // 从网络上读取的类的字节  
            String path = classNameToPath(className);  
            try {  
                URL url = new URL(path);  
                InputStream ins = url.openStream();  
                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 (Exception e) {  
                e.printStackTrace();  
            }  
            return null;  
        }  
      
        private String classNameToPath(String className) {  
            // 得到类文件的URL  
            return rootUrl + "/"  
                    + className.replace('.', '/') + ".class";  
        }  
    }  

      在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。我们使用接口的方式。示例如下:

      客户端接口:

    package classloader;  
      
    public interface Versioned {  
      
        String getVersion();  
    }  
    package classloader;  
      
    public interface ICalculator extends Versioned {  
      
        String calculate(String expression);  
    }  

      网络上的不同版本的类:

    package com.example;  
      
    import classloader.ICalculator;  
      
    public class CalculatorBasic implements ICalculator {  
      
        @Override  
        public String calculate(String expression) {  
            return expression;  
        }  
      
        @Override  
        public String getVersion() {  
            return "1.0";  
        }  
      
    }  
    package com.example;  
      
    import classloader.ICalculator;  
      
    public class CalculatorAdvanced implements ICalculator {  
      
        @Override  
        public String calculate(String expression) {  
            return "Result is " + expression;  
        }  
      
        @Override  
        public String getVersion() {  
            return "2.0";  
        }  
      
    }  

      在客户端加载网络上的类的过程:

    package classloader;  
      
    public class CalculatorTest {  
      
        public static void main(String[] args) {  
            String url = "http://localhost:8080/ClassloaderTest/classes";  
            NetworkClassLoader ncl = new NetworkClassLoader(url);  
            String basicClassName = "com.example.CalculatorBasic";  
            String advancedClassName = "com.example.CalculatorAdvanced";  
            try {  
                Class<?> clazz = ncl.loadClass(basicClassName);  // 加载一个版本的类  
                ICalculator calculator = (ICalculator) clazz.newInstance();  // 创建对象  
                System.out.println(calculator.getVersion());  
                clazz = ncl.loadClass(advancedClassName);  // 加载另一个版本的类  
                calculator = (ICalculator) clazz.newInstance();  
                System.out.println(calculator.getVersion());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
      
    }  
    

    参考文献:

    http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html

    http://www.ibm.com/developerworks/cn/java/j-lo-classloader/

    展开全文
  • 1 线程上下文类加载器 2 何时使用Thread.getContextClassLoader()? 3 类加载器与Web容器 4 类加载器与OSGi 总结 1 线程上下文类加载器  线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的...
  • 深入理解Java类加载器(ClassLoader)

    万次阅读 多人点赞 2017-06-26 09:34:08
    【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) ... 出自【zejian的博客】 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) ...深入理解Java注解类型(@...
  • JVM教你怎么类加载

    万次阅读 2020-03-02 20:47:38
    类加载
  • 类加载

    万次阅读 2020-04-26 21:18:03
    类加载器 类与类加载器 判断类是否“相等” 任意一个类,都由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。 因此,比较两个类是否“相等”,只有在...
  • 类加载机制 java类从被加载到JVM到卸载出JVM,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。...
  • JVM类加载过程

    万次阅读 多人点赞 2019-06-20 15:10:25
    1. JVM类加载过程 1.概述 从类的生命周期而言,一个类包括如下阶段: 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序进行,而解析阶段则不一定,它在某些情况下...
  • JVM入门第一弹--类加载子系统
  • 我竟然不再抗拒 Java 的类加载机制了

    万次阅读 多人点赞 2019-07-10 12:47:50
    很长一段时间里,我对 Java 的类加载机制都非常的抗拒,因为我觉得太难理解了。但为了成为一名优秀的 Java 工程师,我决定硬着头皮研究一下。 01、字节码 在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,...
  • JVM类加载类的分类

    千次阅读 2020-09-10 20:20:43
    JVM类加载类的分类 虚拟机自带的加载器 启动类加载器(引导类加载器,Bootstrap ClassLoader) 这个类加载使用C/C++语言实现的,嵌套在JVM内部。 它用来加载Java中的核心库((JAVA_HOME/jre/lib/rt.jar、...
  • 类加载和反射

    万次阅读 2019-10-08 10:25:59
    类加载过程和机制的讲解和反射的使用
  • Java类加载机制与Tomcat类加载器架构

    万次阅读 热门讨论 2017-02-26 10:58:11
    Java类加载机制 类加载器 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的...
  • Class类加载过程与类加载

    千次阅读 2020-05-02 19:03:55
    在说类加载器和双亲委派模型之前,先来梳理下Class类文件的加载过程,JAVA虚拟机为了保证 实现语言的无关性,是将虚拟机只与“Class 文件”字节码这种特定形式的二进制文件格式相关联,而不是与实现语言绑定。...
  • 类加载的过程

    万次阅读 2020-04-20 18:29:44
    类加载 类的加载概念 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。 什么时候才会启动...
  • 一、为什么要使用类加载器? Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性。例如: 1.编写一个面向接口的应用程序,可能等到...
  • 类加载器和类加载机制

    千次阅读 2016-07-07 12:28:17
    类加载器,类的加载机制,自定义类加载器。
  • 类加载过程和类加载

    千次阅读 2018-03-05 22:03:16
    在Java中,类加载都是在运行期间执行的,这种策略虽然令类加载稍微增加一些性能,但是会给java应用程序提供高度的灵活性。类加载的过程 和其他语言一样,java编译器同样能够将.java文件编译成.class,但是对于JVM来...
  • 类加载机制及类加载器加载Class流程

    千次阅读 2017-06-08 17:00:52
    1、类加载机制 JVM的类加载机制主要有以下三种 全盘负责。当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入。父类委托。先让父类加载器试图加载该Class,只有在...
  • 类加载机制概念 Java虚拟机把描述类的class文件加载到内存,对其进行校验、转换解析、初始化等操作,最终得到可以被虚拟机直接使用的java类型,这就是虚拟机的加载机制。 加载 将class文件读入到内存中,并将其...
  • 深入理解Java类加载器(一):Java类加载原理解析

    万次阅读 多人点赞 2017-05-15 20:47:44
    每个开发人员对java.lang...本文简述了JVM三种预定义类加载器,即启动类加载器、扩展类加载器和系统类加载器,并介绍和分析它们之间的关系和类加载所采用的双亲委派机制,给出并分析了与Java类加载原理相关的若干问题。
  • JVM类加载

    千次阅读 2020-09-29 23:49:19
    文章目录一、类加载过程1、加载2、链接(1)验证(2)准备(3)解析3、初始化二、类加载时机1、创建类的实例,也就是new一个对象2、调用类的静态方法3、访问某个类或接口的静态变量,或者对该静态变量赋值。...
  • 一、什么是类加载机制?虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。二、类加载的时机类从被加载到...
  • 类加载器原理 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。 类加载器树状结构、双亲...
  • JVM系列之类加载

    万次阅读 2020-06-05 20:07:07
    今天我们主要从下面两个方面说下类加载类加载时机和类加载过程。 类加载时机 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 129,814
精华内容 51,925
关键字:

类加载