类加载机制_java类加载机制 - CSDN
精华内容
参与话题
  • 一、什么是类加载机制?虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。二、类加载的时机类从被加载到...

    一、什么是类加载机制?

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

    二、类加载的时机

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如图:

    这里写图片描述

    加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

    什么情况下需要开始类加载过程的第一个阶段:加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

    1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

    2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

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

    4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

    5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

    三、类加载过程

    1、加载

    “加载”是“类加载”(Class Loading)过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事情:

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

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

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

    相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。

    加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。

    2、验证

    验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
    验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
    1.文件格式验证

    验证class文件格式规范,例如: class文件是否已魔术0xCAFEBABE开头 , 主、次版本号是否在当前虚拟机处理范围之内等

    2.元数据验证

    这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求。验证点可能包括:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)、这个类是否继承了不允许被继承的类(被final修饰的)、如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法。

    3.字节码验证

    进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如:保证访法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型、保证跳转命令不会跳转到方法体以外的字节码命令上。

    4.符号引用验证

    3、准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:
    public static int value = 12;

    那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。

    上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建设上面类变量value定义为:
    public static final int value = 123;

    编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

    4、解析

    解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
    符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。
    直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

    虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

    解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。

    1.类、接口的解析
    2.字段解析
    3.类方法解析
    4.接口方法解析

    5、初始化

    类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器< clinit >()方法的过程。在以下四种情况下初始化过程会被触发执行:

    1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。

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

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

    4.jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类

    在上面准备阶段 public static int value = 12; 在准备阶段完成后 value的值为0,而在初始化阶调用了类构造器< clinit >()方法,这个阶段完成后value的值为12。

    *类构造器< clinit >()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句快可以赋值,但是不能访问。

    *类构造器< clinit >()方法与类的构造函数(实例构造函数< init >()方法)不同,它不需要显式调用父类构造,虚拟机会保证在子类< clinit >()方法执行之前,父类的< clinit >()方法已经执行完毕。因此在虚拟机中的第一个执行的< clinit >()方法的类肯定是java.lang.Object。

    *由于父类的< clinit >()方法先执行,也就意味着父类中定义的静态语句快要优先于子类的变量赋值操作。

    *< clinit >()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句,也没有变量赋值的操作,那么编译器可以不为这个类生成< clinit >()方法。

    *接口中不能使用静态语句块,但接口与类不太能够的是,执行接口的< clinit >()方法不需要先执行父接口的< clinit >()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的< clinit >()方法。

    *虚拟机会保证一个类的< clinit >()方法在多线程环境中被正确加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行这个类的< clinit >()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit >()方法完毕。如果一个类的< clinit >()方法中有耗时很长的操作,那就可能造成多个进程阻塞。

    四、类加载器

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

    1、类与类加载器

    对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。

    2、双亲委派模型

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

    从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:

    1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。

    2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\,该加载器可以被开发者直接使用。

    3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    我们的应用程序都是由这三类加载器互相配合进行加载的。
    另外还有自定义类加载器。
    4)自定义类加载器(必须继承 ClassLoader)。
    这些类加载器之间的关系如下图所示:

    这里写图片描述

    如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。

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

    使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。

    双亲委派模型的实现:

    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{
    c=findBootstrapClassOrNull(name);
    }
    }catch(ClassNotFoundException e){
    //如果父类加载器抛出ClassNotFoundException
    //说明父类加载器无法完成加载请求
    }
    if(c==null){
    //在父类加载器无法加载的时候
    //再调用本身的findClass方法来进行类加载
    c=findClass(name);
    }
    }
    if(resolve){
    resolveClass(c);
    }
    return c;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,如上代码所示,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

    内容参考《深入理解Java虚拟机:JVM高级特性与最佳实践》(第2版)周志明

    展开全文
  • Java类加载机制

    万次阅读 2019-03-02 10:21:04
    Java类加载机制 类加载的时机 隐式加载 new 创建类的实例, 显式加载:loaderClass,forName等 访问类的静态变量,或者为静态变量赋值 调用类的静态方法 使用反射方式创建某个类或者接口对象的Class对象。 初始化某个...

    Java类加载机制

    类加载的时机

    • 隐式加载 new 创建类的实例,
    • 显式加载:loaderClass,forName等
    • 访问类的静态变量,或者为静态变量赋值
    • 调用类的静态方法
    • 使用反射方式创建某个类或者接口对象的Class对象。
    • 初始化某个类的子类
    • 直接使用java.exe命令来运行某个主类

    类加载的过程

    我们编写的java文件都是保存着业务逻辑代码。java编译器将 .java 文件编译成扩展名为 .class 的文件。.class 文件中保存着java转换后,虚拟机将要执行的指令。当需要某个类的时候,java虚拟机会加载 .class 文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程被称为类的加载。

    在这里插入图片描述

    加载

    类加载过程的一个阶段,ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个class对象。

    验证

    目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。

    准备

    为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5),这里不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中。

    解析

    这里主要的任务是把常量池中的符号引用替换成直接引用

    初始化

    这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。(前面已经对static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)

    类记载器的任务是根据类的全限定名来读取此类的二进制字节流到 JVM 中,然后转换成一个与目标类对象的java.lang.Class 对象的实例,在java 虚拟机提供三种类加载器,引导类加载器,扩展类加载器,系统类加载器。

    forName和loaderClass区别

    • Class.forName()得到的class是已经初始化完成的。
    • Classloader.loaderClass得到的class是还没有链接(验证,准备,解析三个过程被称为链接)的。

    双亲委派

    双亲委派模式要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器,但是在双亲委派模式中父子关系采取的并不是继承的关系,而是采用组合关系来复用父类加载器的相关代码。

    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 { // ExtClassLoader没有继承BootStrapClassLoader
                        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();
                    // AppClassLoader去我们项目中查找是否有这个文件,如有加载进来
                    // 没有就到用户自定义ClassLoader中加载。如果没有就抛出异常
                    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;
        }
    }
    

    工作原理

    如果一个类收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最后到达顶层的启动类加载器,如果弗雷能够完成类的加载任务,就会成功返回,倘若父类加载器无法完成任务,子类加载器才会尝试自己去加载,这就是双亲委派模式。就是每个儿子都很懒,遇到类加载的活都给它爸爸干,直到爸爸说我也做不来的时候,儿子才会想办法自己去加载。

    优势

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

    java.lang.SecurityException:Prohibited package name: java.lang
    

    类与类加载器

    • 在JVM中标识两个Class对象,是否是同一个对象存在的两个必要条件
    • 类的完整类名必须一致,包括包名。
    • 加载这个ClassLoader(指ClassLoader实例对象)必须相同。

    双亲委派模式的破坏者:线程上下文类加载器

    在这里插入图片描述
    在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。

    线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例

    对象的创建过程

    当虚拟机遇到一个new的指令的时候,首先去检查这个指令是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过。如果没有则执行相应初始化的过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需要的内存的大小在类加载完成后便可以完成确定内存分配完成以后,虚拟机需要将分配的内存空间都初始化为零值,保证了对象的实例字段在Java代码中可以不赋予初值就直接使用,程序能访问到这些字段的数据类型对应的零值。再接下来对象需要进行必要的设置,这个对象是哪个类的实例,如何才能找到这个类的元数据信息,如何找到对象的哈希码,对象的GC分带年龄。

    • Java堆如果是规整的采取:指针碰撞,
    • Java堆如果不是规整的话:空闲列表,在内存中直接分配一个足够大的内存空间划分给对象。
    • 对象创建是非常平凡的,在多线程的程序中会产生线程安全的问题,所以解决这个问题有两种方式
    • 使用CSA配上失败重试的方式来保证原子性
    • 内存分配动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一个小块的内存成为本地分配缓冲,TLAB,哪个线程需要分配内存就在哪个线程的TALB上分配,只有在TALB用完之后才会重新分配新的TALB的时候才会同步锁定。

    对象的内存布局

    对象的内存布局一般分为三个部分:对象头,示例数据,对齐填充

    对象头中存放着对象自身的运行时数据,如哈希码,GC分带年龄,锁状态标志,偏向线程ID,线程持有的锁。

    对象头另外一部分还有类型指针,对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,那在对象头中还必须用一块用于记录数组长度的数据。因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小。

    对象的访问定位方式

    句柄和直接指针

    • 如果使用句柄的话,要在java堆中开辟一个句柄池,用来存放句柄地址,句柄地址中包含对象实例数据(堆)和类型数据(方法区)各自的地址信息。
    • 是用句柄的好处就是引用中存储的是稳定的句柄地址,当被移动时只会修改句柄中的实例数据指针,而引用地址不会被改变。
    • 使用直接指针访问方式的最大好处就是速度更快,它节省了一次访问指针定位的时间开销,引用直接指向存放实例数据的堆内存,在该内存中存放着指向方法区的类型数据地址。
    展开全文
  • jvm之java类加载机制和类加载器(ClassLoader)的详解

    万次阅读 多人点赞 2020-05-29 11:15:41
    当程序主动使用某个类时,如果该类还未被加载到内存中,则...如果没有意外,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库被随意篡改。

    展开全文
  • JVM--详解类加载机制

    万次阅读 2018-07-14 19:30:58
    JVM--详解类加载机制转载:https://blog.csdn.net/championhengyi/article/details/78680700Java虚拟机的体系结构前面我们探讨了Class文件的结构,如果你还没有学习,将不利于这部分知识的吸收与掌握,所以请移步:...

    JVM--详解类加载机制

    转载:https://blog.csdn.net/championhengyi/article/details/78680700

    Java虚拟机的体系结构

    前面我们探讨了Class文件的结构,如果你还没有学习,将不利于这部分知识的吸收与掌握,所以请移步:JVM–Class类文件结构(一)

    学习一个东西之前,我们务必要知道,这东西大概是干什么的,有什么作用。

    为了更清楚的阐释类加载机制到底是干什么的,我先将JVM的结构图贴给大家:

    这里写图片描述

    如上图,我们要学的类加载机制就是要搞清楚类加载器是如何找到指定的Class文件以及怎样将Class文件装载进内存,以便执行引擎执行Class文件中存在的数据和指令,从而使你的Java程序跑起来。

    上面的黑体字就是这玩意大概是干啥的,至于学习它有什么作用,有助于你了解Java源代码是怎么从一个普通的文件变成一个可以正在运行的程序这其中的过程。而且,学习了这部分知识,你再回过头看反射机制,会有一种醍醐灌顶的感觉。


    类的生命周期

    先来看一下类的生命周期吧:

    这里写图片描述

    结合上图,类加载机制主要学习加载、验证、准备、解析、初识化这些过程,然后就是需要了解真正可以将类加载进内存的一个玩意(还是代码实现)—类加载器!

    其实,有了前面Class文件结构的基础,这些东西都很简单,不要怕~

    额外补充

    上图中解析和初始化的位置是可以互换的,如果解析一旦在初始化之后开始,这就是我们经常所说的“动态绑定”~~

    除此之外,这些阶段通常都是互相交叉的混合式进行,各个阶段只保证按部就班的开始,并不保证按部就班的进行或完成。


    类加载的过程

    我们根据上面所说的类的生命周期来一点点剖析类的加载过程。

    加载

    我们首先要明白一件事情:什么开始进行类加载过程的第一阶段:加载?Java虚拟机没有进行强制约束,交由虚拟机的具体实现自由把握。

    看完上面的话,我们来看在加载阶段,虚拟机需要完成哪些事情:

    • 通过一个类的全限定名来获取定义此类的二进制字节流
    • 将获取到的二进制字节流转化成一种数据结构并放进方法区
    • 在内存中生成一个代表此类的java.lang.Class对象,作为访问方法区中各种数据的接口

    我们需要注意一些事情:

    对于方法区的认识:被加载的类的信息存储在方法区中,可以被线程所共享,也就是说,加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在了方法区之中。然后你能想起来那个二进制流中都存储了哪些信息吗?

    对于Class对象认识:Class对象虽然是在内存中,但并未明确规定是在Java堆中,对于HotSpot来说,Class对象存储在方法区中。它作为程序访问方法区中二进制字节流中所存储各种数据的接口。你能大概想到反射机制中的Class对象是怎么一回事了吗?为什么可以在运行期通过反射机制得到那么多的类信息你能猜测到吗?


    验证

    从上面类的生命周期一图中我们可以看出,验证是连接的第一步,这一阶段的目的主要是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,从而不会危害虚拟机自身安全。也就是说,当加载阶段将字节流加载进方法区之后,JVM需要做的第一件事就是对字节流进行安全校验,以保证格式正确,使自己之后能正确的解析到数据并保证这些数据不会对自身造成危害。

    验证阶段主要分成四个子阶段:

    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证

    我不在这里详细的说明每一阶段的校验主要干了什么事情,有兴趣的同学可以自行百度。

    挑点重点来说吧,对字节流进行校验是由一个叫做Class文件检验器的东西所完成,其实还是代码实现。

    而什么叫做元数据呢?

    所谓的元数据是指用来描述数据的数据,更通俗一点就是描述代码间关系,或者代码与其它资源(例如数据库表)之间内在联系的数据,你也可以更简单的认为成框架中的各种@注解,因为这些@注解很简介的描述了大量有关各个类、方法、字段额外的信息或之间的联系。

    元数据验证也就是验证这些额外的信息或它们之间的联系是否正确。

    我们还得注意字节码验证,在字节码验证中涉及到了一个概念:字节码流。

    字节码流 = 操作码 + 操作数。

    操作码就是伪指令,操作数就是普通的Java数据,如int,float等等。

    所以对字节码验证的过程就是对字节码流验证的过程,也就是验证操作码是否合法,操作数是否合法。

    而符号引用验证涉及到常量池解析的知识,在下文中我们顺带着将符号引用验证带过就行,现在先不说。


    准备

    准备阶段你只要掌握两个知识点:

    1.准备阶段的目的:正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存将在方法区中分配。

    注意我的重点:是类变量(static)不是实例变量,还有,我们又知道了在JVM的方法区中不仅存储着Class字节流(按照运行时方法区的数据结构进行存储,上述的二进制字节流是不严谨的说法,只是为了大家好理解),还有我们的类变量。

    2.这里的类变量初始值通常是指数据类型的零值。比如int的零值为0,long为0L,boolean为false… …真正的初始化赋值是在初始化阶段进行的。

    额外一点,如果你设置的类变量还具有final字段,如下:

    public static final int value = 123;

    那么在准备阶段变量的初始值就会被直接初始化为123,具体原因是由于拥有final字段的变量在它的字段属性表中会出现ConstantValue属性。


    解析

    这一阶段我个人觉得不太好理解并且非常重要,但我还是会一点点剖析难点,保证你能听懂,所以开始吧~~

    先来看一下解析阶段的目的:虚拟机将常量池内的符号引用替换为直接引用。

    然后说一下解析阶段最大的特点:发生时间不可预料,有可能和初始化阶段互相交换位置。至于原因,我们等下再说。

    先来说看完解析阶段的目的吧,你有可能有三个疑问。哪个常量池?什么符号引用?什么直接引用?Ok,搞清这三个问题,解析这部分你也就学会了。

    首先来说常量池:在Class的文件结构中我们就花了大量的篇幅去介绍了常量池,我们再来总结一下:常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。

    然后这段话中的常量池指的就是存在于.class文件中的常量池,结果在运行期被JVM装载,并且可以扩充的存在于方法区中的运行时常量池

    然后来看符号引用:在Class文件中我们也讲述了什么是符号引用。总的来说就是常量池中存储的那些描述类、方法、接口的字面量,你可以简单的理解为就是那些所需要信息的全限定名,目的就是为了虚拟机在使用的时候可以定位到所需要的目标。

    最后来看直接引用:直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。

    现在我们对上面那句话进行重新解读:虚拟机将运行时常量池中那些仅代表其他信息的符号引用解析为直接指向所需信息所在地址的指针。

    大概就是这样,我觉得你应该已经完全明白了。

    解决一个遗留的问题:还记得刚才没有说到的符号引用吗?

    这一阶段就是发生在JVM将符号引用转换为直接引用的时候,它的作用就是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,以确保解析动作能够正常执行!

    在解析阶段主要有以下不同的动作,我只给大家罗列出来,不细讲,有兴趣的同学可以自行百度:

    • 类或接口的解析(注意数组类和非数组类)
    • 字段(简单名称+字段描述符)解析(注意递归搜索)
    • 类方法解析(注意递归搜索)
    • 接口方法解析(注意递归搜索)

    在解析阶段还有一个很有意思的东西:动态连接

    它也是上面解析阶段发生时间不确定的直接原因:大部分JVM的实现都是延迟加载或者叫做动态连接。它的意思就是JVM装载某个类A时,如果类A中有引用其他类B,虚拟机并不会将这个类B也同时装载进JVM内存,而是等到执行的时候才去装载。

    而这个被引用的B类在引用它的类A中的表现形式主要被登记在了符号表中,而解析的过程就是当需要用到被引用类B的时候,将引用类B在引用类A的符号引用名改为内存里的直接引用。这就是解析发生时间不可预料的原因,而且这个阶段是发生在方法区中的。


    初始化

    虚拟机规范定义了5种情况,会触发类的初始化阶段,也正是这个阶段,JVM才真正开始执行类中定义的Java程序代码:

    • new一个对象、读取一个类静态字段、调用一个类的静态方法的时候
    • 对类进行反射调用的时候
    • 初始化一个类,发现父类还没有初始化,则先初始化父类
    • main方法开始执行时所在的类
    • 最后一种情况我也不懂,就不贴了

    额外补充:

    有三种引用类的方式不会触发初始化(也就是类的加载),为以下三种:

    • 通过子类引用父类的静态字段,不会导致子类初始化
    • 通过数组定义来引用类,不会触发此类的初始化
    • 引用另一个类中的常量不会触发另一个类的初始化,原因在于“常量传播优化

    来说一说常量传播优化吧(先看一份代码):

    public class ConstClass {
        static {
            System.out.println("ConstClass init!");
        }
    
        public static final String HELLOWORLD = "hello world";
    }
    
    public class NotInitialization {
    
        public static void main(String[] args) {
            System.out.println(ConstClass.HELLOWORLD);
        }
    }

    这种调用方式不会触发ConstClass的初始化,因为常量传播优化,常量“hello world”已经被存储到了NotInitialization类的常量池中,以后NotInitialization对常量ConstClass.HELLOWORLD的引用实际上都被转化为NotInitialization对自身常量池的引用。

    然后在初识化阶段我们重点掌握的知识就是类构造器<clinit>()了。

    这个东西我也只是提几点重要的: 
    1.<clinit>()是编译器自动收集类中的所有类变量的赋值动作和静态语句块合并产生的。 
    2.父类中定义的静态语句块要优先于子类的变量赋值操作。 
    3.虚拟机保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步。


    方法区使用实例

    上面讲了那么多,不知道大家吸收了没有,学习的过程中一定要学会总结和抓中重点哦~

    咱们来看一个例子将上面类加载的过程来串一下吧,加深一下自己的印象:(其中还涉及了一点执行引擎的知识,没关系,很容易理解)

    class Lava {
    
        private int speed = 5;
        void flow() {
    
        }
    }
    
    public class Volcano {
    
        public static void main(String[] args) {
            Lava lava = new Lava();
            lava.flow();
        }
    }

    不同的虚拟机实现可能会用完全不同的方法来操作,下面描述的只是其中一种可能——但并不是仅有的一种。

    加载:读取一个类的class文件并将其中的二进制字节流组织成正确的数据结构放进运行时方法区中:

    要运行Volcano程序,首先得以某种“依赖于实现的”方式告诉虚拟机“Volcano”这个名字。之后,虚拟机将找到并读入相应的class文件“Volcano.class”,然后它会从导入的class文件里的二进制数据中提取类型信息并放到方法区中。通过执行保存在方法区中的字节码,虚拟机开始执行main()方法,在执行时,它会一直持有指向当前类(Volcano类)的常量池(方法区中的一个数据结构)的指针。

    注意:虚拟机开始执行Volcano类中main()方法的字节码的时候,尽管Lava类还没被装载,但是和大多数(也许所有)虚拟机实现一样,它不会等到把程序中用到的所有类都装载后才开始运行。恰好相反,它只会需要时才装载相应的类。(延迟加载、动态连接

    main()的第一条指令告知虚拟机为列在常量池第一项的类分配足够的内存。所以虚拟机使用指向Volcano常量池的指针找到第一项,发现它是一个对Lava类的符号引用,然后它就检查方法区,看Lava类是否已经被加载了。

    这个符号引用仅仅是一个给出了类Lava的全限定名“Lava”的字符串。为了能让虚拟机尽可能快地从一个名称找到类,虚拟机的设计者应当选择最佳的数据结构和算法。

    当虚拟机发现还没有装载过名为“Lava”的类时,它就开始查找并装载文件“Lava.class”,并把从读入的二进制数据中提取的类型信息放在方法区中。

    解析

    紧接着,虚拟机以一个直接指向方法区Lava类数据的指针来替换常量池第一项(就是那个字符串“Lava”),以后就可以用这个指针来快速地访问Lava类了。这个替换过程称为常量池解析,即把常量池中的符号引用替换为直接引用。

    终于,虚拟机准备为一个新的Lava对象分配内存。此时它又需要方法区中的信息。还记得刚刚放到Volcano类常量池第一项的指针吗?现在虚拟机用它来访问Lava类型信息,找出其中记录的这样一条信息:一个Lava对象需要分配多少堆空间。

    JAVA虚拟机总能够通过存储在方法区的类型信息来确定一个对象需要多少内存,当JAVA虚拟机确定了一个Lava对象的大小后,它就在堆上分配这么大的空间,并把这个对象实例的变量speed初始化为默认初始值0。

    当把新生成的Lava对象的引用压到栈中,main()方法的第一条指令也完成了。接下来的指令通过这个引用调用Java代码(该代码把speed变量初始化为正确初始值5)。另一条指令将用这个引用调用Lava对象引用的flow()方法。


    类加载器

    其实这一部分的知识并不多,你需要了解、掌握的知识只有两点:

    1.类加载器的命名空间 
    2.双亲委派模型

    说一点啊,看到这些高大上的名词你们不要怕,又不让你拿代码去实现,其实其中的原理都是很简单的。

    上面说了那么多,类加载器就是用于实现类加载动作的一段代码实现。好了,明白了它的作用,我们来看看什么是它命名空间。

    类加载器的命名空间:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。也就是说,你现在要比较两个类是否相等,只有在这两个类是同一个类加载器加载的前提下才有意义。

    这就是类加载器的命名空间,不难吧~但是读完上面这段话我们可以提取出另一个重要的信息:你上面所说的话中好像包含这样一种意思:类加载器在JVM中不止一个?你很聪明!好了,接下来说明何谓双亲委派模型。

    双亲委派模型:首先你得知道在JVM中有三种系统提供的类加载器:启动类加载器,扩展类加载器、应用程序类加载器。关于这三种加载器的描述大家自行百度,这也不是重点。

    贴一张图:

    这里写图片描述

    如图,这种层次结构就是双亲委派模型。

    好了,为了让大家印象深刻,我在给大家描述一下双亲委派模型的工作过程吧:

    它是一个递归调用类加载器的模型,也就是说如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是不断请求父加载器,如果父加载器可以完成这个加载请求,那么就由父加载器进行加载,如果父加载器不能完成加载请求(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。

    那么使用这种模型有什么好处?

    Java类随着类加载器一起具备了带有优先级的层级关系。例如java.lang.Object,在程序的各种类加载器环境中都是同一个类。

    关于双亲委派模型的实现代码非常简单,我就不再贴出,有兴趣的同学可以研读《深入理解Java虚拟机》第二版中232页的内容。


    JVM基本结构

    说到最后,我在给大家贴一张图吧(Class对象并没有规定实在Java堆中,但对于HotSpot而言,就是存放在方法区中,因此下面所贴的图有点不符):

    这里写图片描述

    这张图包含的信息量可不小,希望大家结合我上面所总结的,自己理清Class对象、ClassLoad、字节码、class文件之间的联系!


    展开全文
  • 【深入Java虚拟机】之四:类加载机制

    万次阅读 多人点赞 2014-01-20 15:43:14
    其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是...
  • 类加载机制详解

    2020-09-02 23:17:07
    wfr=spider&for=pc 我们知道,我们写的java文件是不能直接运行的,我们可以在IDEA中右键...这篇文章,我们只讨论我们的代码在运行之前的一个环节,叫做加载。按照我写文章的常规惯例,先给出这篇文章的大...
  • 理解类加载机制

    千次阅读 2018-11-20 19:32:30
    一般来说,我们日常的开发都是在IDE上进行的,这能让我们将更多的注意力放在...在完成代码的编写之后,编译器会将我们的java文件编译成对应的class文件(二进制字节码文件),而类加载器的作用便是在用到这些class...
  • 我竟然不再抗拒 Java 的类加载机制

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

    万次阅读 多人点赞 2020-08-02 16:35:27
    有这样一道面试题: class Singleton{ private static Singleton singleton = new Singleton(); public static int value1; public static int value2 = 0; private Singleton(){ value1++;... ...
  • Java虚拟机类加载机制

    万次阅读 多人点赞 2020-04-23 17:24:24
    看到这个题目,很多人会觉得我写我的java代码,至于,JVM爱怎么加载就怎么加载,博主有很长一段时间也是这么认为的。随着编程经验的日积月累,越来越感觉到了解虚拟机相关要领的重要性。闲话不多说,老规矩,先来...
  • java类加载机制详解

    千次阅读 2018-09-04 09:52:36
     引子:编程过程中,大家肯定遇到过java.lang.ClassNotFoundException错误,特别是当我们自定义类加载器进行类的加载或者利用java的反射机制获取Class对象时。  虚拟机整体架构的第一部分就是类装载器,Java类加...
  • 聊一聊JVM的类加载机制

    万次阅读 2020-02-24 09:30:58
    聊一聊JVM的类加载机制 目录 聊一聊JVM的类加载机制 01 怎么获取类加载器?哪几种类加载器 02 各个加载器加载的路径? 01 怎么获取类加载器?哪几种类加载器 BootStrapClassLoader 引导类加载器 ...
  • Java类加载机制与Tomcat类加载器架构

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

    千次阅读 2019-04-11 20:51:53
    附录前言网上有很多的Java类加载机制的介绍, 但是对于初学者而言看起来都太过于深疏, 因此在本文用图解和例子的方式为本文的读者介绍Java的类加载机制。类加载的概述双亲委派加载机制委派模型介绍: 双亲委派模型
  • 深入JVM类加载机制

    万次阅读 多人点赞 2017-02-26 10:07:51
    类加载的流程以及双亲委派模型
  • 类加载机制 2.Java9的改变 类加载器 类加载机制   一、类加载  类加载分为三个步骤:加载、连接、初始化。 1.加载 类加载指的是将class文件读入内存,并为之创建一个java.lang.Class对象,即程序中使用...
  • title: 类加载机制(一):简述类加载过程 date: 2019-03-13 10:06:10 categories: Java虚拟机 tags: 类加载机制 类加载机制(一):简述类加载过程 引言 Java源文件经过编译之后,生成了一连串的16...
  • tomcat学习之四:tomcat的类加载机制

    千次阅读 2017-10-29 01:09:11
    tomcat的类加载机制继承了java类加载机制中经典的双亲委派模型。所以要了解tomcat的类加载机制需要先了解双亲委派模型。  在程序中用到的类需要由类加载器将类的class文件加载到内存中,然后经由JVM验证、解析、...
  • JVM 类加载机制详解

    千次阅读 2018-08-30 17:08:17
    类加载机制: 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。 如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化...
  • 深入理解java虚拟机——类加载机制 定义   类加载机制指的是:虚拟机将描述类的数据从class文件加载到内存中,对加载的数据进行验证,解析,初始化,最后得到虚拟机认可后转化为直接可以使用的java类型的过程   ...
1 2 3 4 5 ... 20
收藏数 442,985
精华内容 177,194
关键字:

类加载机制