精华内容
下载资源
问答
  • 类加载过程

    多人点赞 2019-11-11 20:38:41
    类加载过程的第一步,主要完成下面3件事情: 通过全类名获取定义此类的二进制字节流 将字节流所代表的静态存储结构转换为方法区的运行时数据结构 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的...

    加载

    类加载过程的第一步,主要完成下面3件事情:

    1. 通过全类名获取定义此类的二进制字节流
    2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
    3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

    虚拟机规范多上面这3点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)等等。

    一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。

    数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

    加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

    验证

    准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

    1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
    2. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会复制)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被复制为 111。

    基本数据类型的零值:

    基本数据类型的零值

    解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。

    符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

    综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

    初始化

    初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 <clinit> ()方法的过程。

    对于<clinit>() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit>() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。

    对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化:

    1. 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    2. 使用 java.lang.reflect 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化。
    3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
    4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
    5. 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。
    展开全文
  • JVM类加载过程

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

    1. JVM类加载过程

        1.概述

        从类的生命周期而言,一个类包括如下阶段:

        

            加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序进行,而解析阶段则不一定,它在某些情况下可能在初始化阶段后在开始,因为java支持运行时绑定。

        2. 类加载时机

        加载(loading)阶段,java虚拟机规范中没有进行约束,但初始化阶段,java虚拟机严格规定了有且只有如下5种情况必须立即进行初始化(初始化前,必须经过加载、验证、准备阶段):

        (1)使用new实例化对象时,读取和设置类的静态变量、静态非字面值常量(静态字面值常量除外)时,调用静态方法时。

        (2)对内进行反射调用时。

        (3)当初始化一个类时,如果父类没有进行初始化,需要先初始化父类。

        (4)启动程序所使用的main方法所在类

        (5)当使用1.7的动态语音支持时。

        如上5种场景又被称为主动引用,除此之外的引用称为被动引用,被动引用有如下3种常见情况

    • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
    • 定义对象数组和集合,不会触发该类的初始化
    • 类A引用类B的static final常量不会导致类B初始化(注意静态常量必须是字面值常量,否则还是会触发B的初始化)
    public class TestClass {
        public static void main(String[] args) {
            System.out.println(ClassInit.str);
            System.out.println(ClassInit.id);
        }
    }
    class ClassInit{
        public static final long id=IdGenerator.getIdWorker().nextId();//需要初始化ClassInit类
        public static final String str="abc";//字面值常量
        static{
            System.out.println("ClassInit init");
        }
    }
    • 通过类名获取Class对象,不会触发类的初始化。如System.out.println(Person.class);
    • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化。
    • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作

        注意:被动引用不会导致类初始化,但不代表类不会经历加载、验证、准备阶段。

        3. 类加载方式

        这里的类加载不是指类加载阶段,而是指整个类加载过程,即类加载阶段到初始化完成。

       (1)隐式加载

    • 创建类对象
    • 使用类的静态域
    • 创建子类对象
    • 使用子类的静态域
    • 在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class
    • 在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
    • 在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数所在的类的class文件

        (2)显式加载

    • ClassLoader.loadClass(className),只加载和连接、不会进行初始化
    • Class.forName(String name, boolean initialize,ClassLoader loader); 使用loader进行加载和连接,根据参数initialize决定是否初始化。

    2. 加载阶段

        加载是类加载过程中的一个阶段,不要将这2个概念混淆了。

        在加载阶段,虚拟机需要完成以下3件事情:

    • 通过一个类的全限定名来获取定义此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

        加载.class文件的方式

    • 从本地系统中直接加载
    • 通过网络下载.class文件
    • 从zip,jar等归档文件中加载.class文件
    • 从专有数据库中提取.class文件
    • 将Java源文件动态编译为.class文件    

        相对于类生命周期的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

    3. 连接阶段

      3.1 验证:确保被加载的类的正确性

        确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

    • 文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
    • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等。
    • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,保证方法中的类型转换有效等等。
    • 符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等。

        验证阶段是非常重要的,但不是必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施。

      3.2 准备:为类的静态变量分配内存,并将其赋默认值

        为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

    • 只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。
    • 对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)。

      3.3 解析:将常量池中的符号引用替换为直接引用(内存地址)的过程

        符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

        直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。

        假设:一个类有一个静态变量,该静态变量是一个自定义的类型,那么经过解析后,该静态变量将是一个指针,指向该类在方法区的内存地址。 具体见后续文章。

    4. 初始化:为类的静态变量赋初值

        赋初值两种方式:

    • 定义静态变量时指定初始值。如 private static String x="123";
    • 静态代码块里为静态变量赋值。如 static{ x="123"; } 

        注意:只有对类的主动使用才会导致类的初始化。

    5. clinit 与 init

        在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init。

      5.1 clinit 

        clinit指的是类构造器,主要作用是在类加载过程中的初始化阶段进行执行,执行内容包括静态变量初始化和静态块的执行。

        注意事项:

        1. 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成。

        2. 在执行clinit方法时,必须先执行父类的clinit方法。

        3. clinit方法只执行一次。

        3. static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定。如下代码所示:

    public class TestClass {
        public static void main(String[] args) {
            ClassInit init=ClassInit.newInstance();
    
            System.out.println(init.x);
            System.out.println(init.y);
        }
    }
    
    class ClassInit{
        private static ClassInit init=new ClassInit();
        public static int x;
        public static int y=0;
        static{
            x=10;
            y=10;
        }
        private ClassInit(){
            x++;
            y++;
        }
        public static ClassInit newInstance(){
            return init;
        }
    }
    //在类加载到连接完成阶段,ClassInit类在内存中的状态为:init=null,x=0,y=0
    //初始化阶段时,需要执行clinit方法,该方法类似如下伪代码:
    clinit(){
    	//init=new ClassInit();调用构造方法
        x++;//x=1 因为此时x的值为连接的准备阶段赋的默认值0,然后++变成1
        y++;//y=1 因为此时y的值为连接的准备阶段赋的默认值0,然后++变成1
        //x=0;//为什么这里没有执行x=0,因为程序没有给x赋初值,因此在初始化阶段时,不会执行赋初值操作
        y=0;//因为类变量y在定义时,指定了初值,尽管初值为0,因此在初始化阶段的时候,需要执行赋初值操作
        x++;//第一个静态块的自增操作,结果为x=2;
        y++;//第一个静态块的自增操作,结果为y=1;
    }
    //所以最终结果为x=2,y=1
    //如果private static ClassInit init=new ClassInit(); 代码在public static int y=0;后面,那么clinit方法的伪代码如下:
    clinit(){
        //x=0;//这里虽然没有执行,但此时x的值为连接的准备阶段赋的默认值0
        y=0;//因为类变量y在定义时,指定了初值,尽管初值为0,因此在初始化阶段的时候,需要执行赋初值操作
    	//init=new ClassInit();调用构造方法
        x++;//x=1 因为此时x的值为连接的准备阶段赋的默认值0,然后++变成1
        y++;//y=1 因为此时y的值为初始化阶段赋的初值,只是这个初值刚好等于默认值0而已,然后++变成1
        x++;//第一个静态块的自增操作,结果为x=2;
        y++;//第一个静态块的自增操作,结果为y=2;
    }
    //最终结果为x=2,y=2

        5.2 init

        init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。

        注意事项:

        1. 如果类中没有成员变量和代码块,那么clinit方法将不会被生成。

        2. 在执行init方法时,必须先执行父类的init方法。

        3. init方法每实例化一次就会执行一次。

        3. init方法先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块。如下代码所示:

    public class TestClass {
        public static void main(String[] args) {
            ClassInit init=new ClassInit();
        }
    }
    
    class ClassInit{
        public int x;
        public int y=111;
        public ClassInit(){
            x=1;
            y=1;
        }
        {
            x=2;
            y=2;
        }
        {
            x=3;
            y=3;
        }
    }
    //实例化步骤为:先为属性分配空间,再执行赋默认值,然后按照顺序执行代码块或赋初始值,最后执行构造方法
    //根据上述代码,init方法的伪代码如下:
    init(){
    	x=0;//赋默认值
        y=0;//赋默认值
        y=111;//赋初值
        x=2;//从上到下执行第一个代码块
        y=2;//从上到下执行第一个代码块
        x=3;//从上到下执行第二个代码块
        y=3;//从上到下执行第二个代码块
        //ClassInit();执行构造方法
        x=1;//最后执行构造方法
        y=1;//最后执行构造方法
    }
    //如果上述代码的成员变量x,y的定义在类最后时,那么init方法的伪代码如下:
    init(){
    	x=0;//赋默认值
        y=0;//赋默认值
        x=2;//从上到下执行第一个代码块
        y=2;//从上到下执行第一个代码块
        x=3;//从上到下执行第二个代码块
        y=3;//从上到下执行第二个代码块
        y=111;//赋初值
        //ClassInit();执行构造方法
        x=1;//最后执行构造方法
        y=1;//最后执行构造方法
    }

    6. 卸载阶段

        执行了System.exit()方法

        程序正常执行结束

        程序在执行过程中遇到了异常或错误而异常终止

        由于操作系统出现错误而导致Java虚拟机进程终止
       

    展开全文
  • Class类加载过程与类加载器

    千次阅读 2020-05-02 19:03:55
    在说类加载器和双亲委派模型之前,先来梳理下Class类文件的加载过程,JAVA...类加载过程 Class类从被加载到虚拟机内存开始,到卸载出内存为止,其生命周期包括:加载(Loading)、验证(Verification)、准备(Prepa...

    在说类加载器和双亲委派模型之前,先来梳理下Class类文件的加载过程,JAVA虚拟机为了保证 实现语言的无关性,是将虚拟机只与“Class 文件”字节码这种特定形式的二进制文件格式相关联,而不是与实现语言绑定。所以其不一定是Class文件。

    类加载过程

    Class类从被加载到虚拟机内存开始,到卸载出内存为止,其生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。其中加载过程见下:

    在这里插入图片描述

    加载阶段

    加载阶段做了什么?过程见下图。其中类的全限定名是Class文件(JAVA由编译器自动生成)内的代表常量池内的16进制值所代表的特定符号引用。因为Class文件格式有其自己的一套规范,如第1-4字节代表魔数,第5-6字节代表次版本,第7-8字节代表主版本号等等。

    在这里插入图片描述

    说白了就是,虚拟机不关心我们的这种“特定二进制流”从哪里来的,从本地加载也好,从网上下载的也罢,都没关系。虚拟机要做的就是将该二进制流写在自己的内存中并生成相应的Class对象(并不是在堆中)。在这个阶段,我们能够通过我们自定义类加载器来控制二进制流的获取方式。

    验证阶段

    验证阶段,正因为加载阶段虚拟机不介意二进制的来源,所以就可能存在着影响虚拟机正常运行的安全隐患。所以虚拟机对于该二进制流的校验工作非常重要。校验方式包括但不限于:

    在这里插入图片描述

    准备阶段

    准备阶段在此阶段将正式为类变量分配内存并设置变量的初始化值。注意的是,类变量是指 static 的静态变量,是分配在方法区之中的,而不像对象变量,分配在堆中。还有一点需要注意,final 常量在此阶段就已经被赋值了。如下:

        public static int SIZE = 10; // 初始化值 == 0
        public static final int SIZE = 10; // 初始化值 == 10
    
    解析阶段

    解析阶段是将常量池内的符号引用替换为直接引用的过程。符号引用就是上文说的Class文件格式标准所规定的特定字面量,而直接引用就是我们说的指针,内存引用等概念

    初始化阶段

    到了初始化阶段,就开始真正执行我们的字节码程序了。也可以理解成:类初始化阶段就是虚拟机内部执行类构造 < clinit >() 方法的过程。注意,这个类构造方法可不是虚拟机内部生成的,而是我们的编译器自动生成的,是编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,具体分析见下。

    注意,这里说的是类变量赋值动作,即static 并且具有赋值操作,如果无赋值操作,那么在准备阶段进行的方法区初始化就算完成了。为何还要加上static{} 呢?我们可以把static{} 理解成:是由多个静态初始化动作组织成的一个特殊的“静态子句”,与其他的静态初始化动作一样。这也是为何 static {} 只会执行一遍并在对象构造方法之前执行的原因。如下代码:

    public class Tested {
        public static int T;
        // public static int V; // 无赋值,不在类构造中再次初始化
        public int c = 1; // 不会在类构造中
    
        static {
            T = 10;
        }
    }
    

    还有一点,编辑器收集类变量的顺序,也就是虚拟机在此初始化阶段的执行顺序,这个顺序就是变量在类中语句定义的先后顺序,如上面的:语句 2 : T 在 6 : T 之前,这是两个独立的语句。类构造< clinit >的其他特点如下:

    在这里插入图片描述

    编译期的< clinit >

    我们将流程回溯到编译期阶段,以刚刚的Tested 类代码为例。通过 javap -c /Tested.class (注意:/…/Tested 绝对路径),获取Class文件:

    public class com.tencent.lo.Tested {
      public static int T;
    
      public int c;
    
      public com.tencent.lo.Tested();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: aload_0
           5: iconst_1
           6: putfield      #2                  // Field c:I
           9: return
    
      static {};
        Code:
           0: bipush        10
           2: putstatic     #3                  // Field T:I
           5: return
    }
    

    在Class 文件中我们能很明显的看到 invokespecial 对应的对象构造 “< init >” : () V ,那为什么没有看到< clinit > 类构造方法呢?其实上面的 static {} 就是。我们来看下OpenJDK源码Constants接口,此接口定义了在编译器中所用到的常量,这是一个自动生成的类。

    public interface Constants extends RuntimeConstants {
        public static final boolean tracing = true;
    
        Identifier idClassInit = Identifier.lookup("<clinit>");
        Identifier idInit = Identifier.lookup("<init>");
    }
    

    MemberDefinition类 中,判断是否为类构造器字符:

        public final boolean isInitializer() {
            return getName().equals(idClassInit); // 类构造
        }
        public final boolean isConstructor() {
            return getName().equals(idInit); // 对象构造
        }
    

    而在MemberDefinition 的 toString() 方法中,我们能够看到,当类构造时,会输出特定字符,而不会像对象构造那样输出规范的字符串。

        public String toString() {
            Identifier name = getClassDefinition().getName();
            if (isInitializer()) { // 类构造
                return isStatic() ? "static {}" : "instance {}";
            } else if (isConstructor()) { // 对象构造
                StringBuffer buf = new StringBuffer();
                buf.append(name);
                buf.append('(');
                Type argTypes[] = getType().getArgumentTypes();
                for (int i = 0 ; i < argTypes.length ; i++) {
                    if (i > 0) {
                        buf.append(',');
                        }
                    buf.append(argTypes[i].toString());
                    }
                buf.append(')');
                return buf.toString();
            } else if (isInnerClass()) {
                return getInnerClass().toString();
            }
            return type.typeString(getName().toString());
        }
    

    类加载器与双亲委派

    “虚拟机将类加载阶段中的“通过一个全限定名来获取描述此类的二进制字节流”这个动作放到了外部来实现,以便开发者可以自己决定如何获取所需的类文件,而实现这个动作的代码模块就被称为类加载器。对于任意一个类来说,只有在类加载器相同的情况下比较两者是否相同才有意义,否则即使是同个文件,在不同加载器下,在虚拟机看来其仍然是不同的,是两个独立的类。我们可以将类加载器分为三类”:

    在这里插入图片描述
    而所谓的双亲委派模型就是:“如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把加载的操作委托给父类加载器去完成,每一层次加载器都是如此,因此所有的加载请求都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它的搜索范围没有找到所需的类,因为上面所说的启动类加载器和扩展类加载器,只能加载特定目录之下的,或被-x参数所指定的类库),子类才会尝试自己加载”。注意这里说的父类只是形容层次结构,其并不是直接继承关系,而是通过组合方式来复用父类的加载器的。

    在这里插入图片描述
    “双亲委派的好处就是,使加载器也具备了优先级的层次结构。例如,java.lang.Object存放在< JAVA_HOME>\lib 下的rt.jar包内,无论哪个类加载器要加载这个类,最终都会委派给最顶层的启动类加载器,所以保证了Object类在各类加载器环境中都是同一个类。相反,如果没有双亲委派模型,如果用户编写了一个java.lang.Object类,并放在程序的ClassPath下,那么系统将会出现多个不同的Object类”。

    为何?因为每个加载器各自为政,不会委托给父构造器,如上面所说,只要加载器不同,即使类Class文件相同,其也是独立的。

    试想如果自己在项目中编写了一个java.lang.Object 类(当然不能放入rt.jar类库中替换掉同名Object文件,这样做没有意义,如果虚拟机加载校验能通过的话,只是相当于改了源码嘛),我们通过自定义的构造器来加载这个类可以吗?理论上来说,虽然这两个类都是java.lang.Object,但由于构造器不同,对于虚拟机来说这是不同的Class文件,当然可以。但是实际上呢?来段代码见下:

        public void loadPathName(String classPath) throws ClassNotFoundException {
            new ClassLoader() {
                @Override
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    InputStream is = getClass().getResourceAsStream(name);
                    if (is == null)
                        return super.loadClass(name);
                    byte[] b;
                    try {
                        b = new byte[is.available()];
                        is.read(b);
                    } catch (Exception e) {
                        return super.loadClass(name);
                    }
                    return defineClass(name, b, 0, b.length);
                }
            }.loadClass(classPath);
        }
    

    实际的执行逻辑是 defineClass 方法。可以发现,自定义加载器是无法加载以 java. 开头的系统类的。

        protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                             ProtectionDomain protectionDomain)
                throws ClassFormatError {
            
            protectionDomain = preDefineClass(name, protectionDomain);
            ... // 略
    
            return c;
        }
    
        private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
            if (!checkName(name))
                throw new NoClassDefFoundError("IllegalName: " + name);
            // 在这里能看到系统类,自定义的加载器是不能加载的
            if ((name != null) && name.startsWith("java.")) {
                throw new SecurityException
                        ("Prohibited package name: " +
                                name.substring(0, name.lastIndexOf('.')));
            }
            ... // 略
            
            return pd;
        }
    

    如果你用AS直接查看,你会发现,defineClass 内部是没有具体实现的,源码见下。可这并不代表android 的 defineClass 方法实现与 java 不同,因为都是引用的 java.lang 包下的ClassLoader 类,逻辑肯定都是一样的。之所以看到的源码不一样,这是由于SDK和JAVA源码包的区别导致的。SDK内的源码是谷歌提供给我们方便开发查看的,并不完全等同于源码。

        protected final Class<?> defineClass(String name, byte[] b, int off, int len)
            throws ClassFormatError
        {
            throw new UnsupportedOperationException("can't load this type of class file");
        }
    

    参考
    1、周志明,深入理解JAVA虚拟机:机械工业出版社

    展开全文
  • 文章目录类加载器系统类加载过程1.加载2.连接3.初始化类加载器的分类1.启动类加载器(引导类加载器,Bootstrap ClassLoader)2.扩展类加载器(Extension ClassLoader)3.应用程序类加载器(系统类加载器,...

    类加载器系统

    • 类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。
    • ClassLoader只负责class文件的加载,至于他是否可以运行,则由ExecutionEngine决定。
    • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。

    ClassLoader
    > [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r48czkTL-1602061159351)(C:\Users\GO FOR IT\AppData\Roaming\Typora\typora-user-images\1602040984297.png)]

    1. class file存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
    2. class file加载到JVM中,被称为DNA元数据模板,放在方法区。
    3. 在.class文件-> JVM->最终成为元数据模板,此过程就要一个运输工具(类装载器class Loader),扮演一个快递员的角色。

    类加载过程

    在这里插入图片描述

    1.加载

    1. 通过一个类的全限定名获取定义此类的二进制字节流

    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

    补充:加载.class文件的方式

    • 从本地系统中直接加载
    • 通过网络获取,典型场景: web Applet
    • 从zip压缩包中读取,成为日后jar、war格式的基础
    • 运行时计算生成,使用最多的是:动态代理技术
    • 由其他文件生成,典型场景:JSP应用
    • 从专有数据库中提取.class文件,比较少见
    • 从加密文件中获取,典型的防class文件被反编译的保护措施

    2.连接

    1. 验证(Verify):

      • 目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。

      • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

    2. 准备(Prepare) :

      • 为类变量分配内存并且设置该类变量的默认初始值,即零值。
      • 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
      • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
    3. 解析(Resolve):

      • 将常量池内的符号引用转换为直接引用的过程。
      • 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。符号引用就是一组符号来描述所引用的目标。
      • 符号引用的字面量形式明确定义在《java.虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
      • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的
        CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等。

    3.初始化

    • 初始化阶段就是执行类构造器方法()的过程。
    • 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
    • 构造器方法中指令按语句在源文件中出现的顺序执行。
    • ()不同于类的构造器。(关联:构造器是虚拟机视角下的())
    • 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
    • 虚拟机必须保证一个类的()方法在多线程下被同步加锁。

    类加载器的分类

    • JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。

    • 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加教器都划分为自定义类加载器。

    • 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:

    Extension Class Loader
    Bootstrap Class Loader
    System Class Loader
    User Defined Class Loader

    四者之间的关系是包含关系。不是上下层个,也不是父子类的继承关系。

    public class ClassLoaderTest {
        public static void main(String[] args) {
            //获取系统类加载器
            ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
            System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
    
            //获取其上层:扩展类加载器
            ClassLoader extClassLoader = systemClassLoader.getParent();
            System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@4554617c
    
            //获取其上层:获取不到引导类加载器
            ClassLoader bootstrapClassLoader = extClassLoader.getParent();
            System.out.println(bootstrapClassLoader);//null
    
            //对于用户自定义类来说:默认使用系统类加载器进行加载
            ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
            System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
            
            //String类使用引导类加载器进行加载的。java的核心类库都是使用引导类加载器进行加载的
            ClassLoader classLoader1 = String.class.getClassLoader();
            System.out.println(classLoader1);//null
        }
    }
    

    1.启动类加载器(引导类加载器,Bootstrap ClassLoader)

    • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
    • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供
      JVM自身需要的类
    • 并不继承自java.lang.classLoader,没有父加载器。
    • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
    • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

    2.扩展类加载器(Extension ClassLoader)

    • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
    • 派生于classLoader类
    • 父类加载器为启动类加载器
    • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

    3.应用程序类加载器(系统类加载器,AppclassLoader)

    • java语言编写,由sun.misc.Launcher$AppClassLoader实现
    • 派生于classLoader类
    • 父类加载器为扩展类加载器
    • 它负责加载环境变量classpath或系统属性java.class.pat指定路径下的类库
    • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
    • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
    public class ClassLoaderTest1 {
        public static void main(String[] args) {
            System.out.println("---------启动类加载器-----------");
            //获取BootstrapClassLoader能够加载的api的路径
            URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
            for (URL element:urLs){
                System.out.println(element.toExternalForm());
            }
    
            //从上面的路径中随意选择一个类,来看看他的类加载器是着什么:引导类加载器。引导类加载器是不能获取的
            ClassLoader classLoader = Provider.class.getClassLoader();
            System.out.println(classLoader);//null
    
            System.out.println("--------扩展类加载器----------");
            String extDirs = System.getProperty("java.ext.dirs");
            for (String path:extDirs.split(";")){
                System.out.println(path);
            }
            //从上面的路径中随意选择一个类,来看看他的类加载器是着什么:扩展类加载器
            ClassLoader classLoader1 = CurveDB.class.getClassLoader();
            System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@7ea987ac
            
        }
    }
    

    4.自定义类加载器

    • 自定义类的加载常常是通过上述三种类加载器互相配合执行的,必要时可以自定义类加载器。

    • 为什么要自定义类加载器?

      1. 隔离加载类。防止类名和类路径一样是发生的冲突
      2. 修改类的加载方式。Bootstrap ClassLoader是必须使用,但是可以按需求使用扩展类加载器和应用程序类加载器
      3. 扩展加载源。从其他地方加载字节码(如数据库中)
      4. 防止源码泄露。将字节码文件加密,使用自定义类加载器进行解密加载到内存

    ClassLoader

    • ClassLoader类,是一个抽象类,其后所有的类加载器(除启动类加载器)都是继承自ClassLoader。
    • 获取类加载器的方式:
    public class ClassLoaderTest2 {
        public static void main(String[] args) {
            try {
                //方式1:获取当前类的ClassLoader
                ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
                System.out.println(classLoader);//null
    
                //方式2:获取当前线程上下文的CLassLoader(即当前类ClassLoaderTest2的加载器)
                ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
                System.out.println(classLoader1);//sun.misc.Launcher$AppClassLoader@18b4aac2
    
                //方式3:获取系统的ClassLoader
                ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
                System.out.println(classLoader2);//sun.misc.Launcher$ExtClassLoader@4554617c
    
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    

    几种类加载器的继承关系:

    SecureClassLoader
    abstract ClassLoader
    URLClassLoader
    ExtClassLoader
    AppClassLoader
    展开全文
  • 文章目录Java类加载机制与类加载过程一、为什么说Java语言是跨平台的?二、Java虚拟机启动、加载类过程分析Step 1.根据JVM内存配置要求,为JVM申请特定大小的内存空间Step 2. 创建一个引导类加载器实例,初步加载...
  • 思考类加载过程

    2020-03-22 20:00:26
    思考类加载过程 思考类加载过程思考类加载过程什么是类加载?说说类的加载过程?ClassLoader的作用是啥?类加载器种类类加载器间的关系**面试官:那自己怎么去实现一个ClassLoader呢?请举个实际的例子**自定义类加载器...
  • java中类加载机制、类加载过程和类加载器层次
  • java类加载过程和类加载器 一:类加载概述 首先我们了解一下类是怎么进入内存的,jvm把class文件加载到内存,然后对文件进行校验,转换解析和初始化变 成可以被虚拟机直接利用的java类型,这就是虚拟机的类加载...
  • 类加载过程 Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢? 系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->...
  • 面试官:请你谈谈Java的类加载过程

    万次阅读 多人点赞 2018-02-01 00:08:28
    刚刚走出校门的应届毕业生,如果在去寻求一份Java开发的工作时,你的面试官很有可能一边看着你的简历,一边漫不经心地问你:了解过Java类的加载过程吗?...今天,小编就Java类加载过程这个问题,抛砖引玉,说一下...
  • Java类加载过程

    2019-04-21 10:26:07
    1.Java类加载过程 (1)加载 将二进制字节码加载到内存中 (2)验证 验证其格式,语法,语义是否合法安全 (3)准备 对类变量赋零值 (4)解析 将类,属性,方法的符号引用转换为直接引用,指向特定的内存 (5)初始...
  • title: 类加载机制(一):简述类加载过程 date: 2019-03-13 10:06:10 categories: Java虚拟机 tags: 类加载机制 类加载机制(一):简述类加载过程 引言 Java源文件经过编译之后,生成了一连串的16...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 20,864
精华内容 8,345
关键字:

类加载过程