精华内容
下载资源
问答
  • 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虚拟机进程终止
       

    展开全文
  • 二、Java虚拟机启动、加载类过程分析 三、类加载器有哪些?其组织结构是怎样的? 四、双亲加载模型的逻辑和底层代码实现是怎样的? 五、类加载器与Class<T> 实例的关系 六、线程上下文加载器 一、为什么...

    0、前言

    读完本文,你将了解到:

    一、为什么说Jabalpur语言是跨平台的

    二、Java虚拟机启动、加载类过程分析

    三、类加载器有哪些?其组织结构是怎样的?

    四、双亲加载模型的逻辑和底层代码实现是怎样的?

    五、类加载器与Class<T>  实例的关系

    六、线程上下文加载器

    一、为什么说Java语言是跨平台的?

      Java语言之所以说它是跨平台的、可以在当前绝大部分的操作系统平台下运行,是因为Java语言的运行环境是在Java虚拟机中。

      Java虚拟机消除了各个平台之间的差异,只要操作系统平台下安装了Java虚拟机,那么使用Java开发的东西都能在其上面运行。如下图所示:

    这里写图片描述

      Java虚拟机对各个平台而言,实质上是各个平台上的一个可执行程序。例如在windows平台下,java虚拟机对于windows而言,就是一个java.exe进程而已。

    二、Java虚拟机启动、加载类过程分析

      下面我将定义一个非常简单的java程序并运行它,来逐步分析java虚拟机启动的过程。

    package org.luanlouis.jvm.load;  
    import sun.security.pkcs11.P11Util;  
    
    /** 
     * Created by louis on 2016/1/16. 
     */  
    public class Main{  
    
        public static void main(String[] args) {  
            System.out.println(”Hello,World!”);  
    
            ClassLoader loader = P11Util.class.getClassLoader();  
    
            System.out.println(loader);  
        }  
    }  

      在命令行下输入:

    java org.luanlouis.jvm.load.Main

      当输入上述的命令时,windows开始运行{JRE_HOME}/bin/java.exe程序,java.exe 程序将完成以下步骤:

    1. 根据JVM内存配置要求,为JVM申请特定大小的内存空间

    2. 创建一个引导类加载器实例,初步加载系统类到内存方法区区域中

    3.创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader

    4.使用上述获取的ClassLoader实例加载我们定义的 org.luanlouis.jvm.load.Main类

    5.加载完成时候JVM会执行Main类的main方法入口,执行Main类的main方法

    6. 结束,java程序运行结束,JVM销毁

    Step 1.根据JVM内存配置要求,为JVM申请特定大小的内存空间

      为了不降低本文的理解难度,这里就不详细介绍JVM内存配置要求的话题,今概括地介绍一下内存的功能划分。

      JVM启动时,按功能划分,其内存应该由以下几部分组成:

    这里写图片描述

      如上图所示,JVM内存按照功能上的划分,可以粗略地划分为方法区(Method Area) 和堆(Heap),而所有的类的定义信息都会被加载到方法区中。

      关于具体方法区里有什么内容,读者可以参考我的另一篇博文: 《Java虚拟机原理图解》3、JVM运行时数据区

    Step 2. 创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;

      JVM申请好内存空间后,JVM会创建一个引导类加载器(Bootstrap Classloader)实例,引导类加载器是使用C++语言实现的,负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等。

      引导类加载器(Bootstrap Classloader)会读取 {JRE_HOME}/lib下的jar包和配置,然后将这些系统类加载到方法区内。

      本例中,引导类加载器是用 {JRE_HOME}/lib加载类的,不过,你也可以使用参数 -Xbootclasspath 或系统变量sun.boot.class.path来指定的目录来加载类。

      一般而言,{JRE_HOME}/lib下存放着JVM正常工作所需要的系统类,如下表所示:

    文件名描述
    rt.jar运行环境包,rt即runtime,J2SE 的类定义都在这个包内
    charsets.jar字符集支持包
    jce.jar是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现
    jsse.jar安全套接字拓展包Java(TM) Secure Socket Extension
    classlist该文件内表示是引导类加载器应该加载的类的清单
    net.propertiesJVM 网络配置信息

     

      引导类加载器(Bootstrap ClassLoader)加载系统类后,JVM内存会呈现如下格局:

    这里写图片描述

      引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有运行时常量池、类型信息、字段信息、方法信息、类加载器的引用对应class实例的引用等信息。

      类加载器的引用,由于这些类是由引导类加载器(Bootstrap Classloader)进行加载的,而 引导类加载器是有C++语言实现的,所以是无法访问的,故而该引用为NULL

      对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

      小测试,当我们在代码中尝试获取系统类如java.lang.Object的类加载器时,你会始终得到NULL

    System.out.println(String.class.getClassLoader());//null  
    System.out.println(Object.class.getClassLoader());//null  
    System.out.println(Math.class.getClassLoader());//null  
    System.out.println(System.class.getClassLoader());//null  

    Step 3. 创建JVM 启动器实例 Launcher,并取得类加载器ClassLoader

      上述步骤完成,JVM基本运行环境就准备就绪了。接着,我们要让JVM工作起来了:运行我们定义的程序 org.luanlouis,jvm.load.Main。

      此时,JVM虚拟机调用已经加载在方法区的类sun.misc.Launcher 的静态方法getLauncher() 获取sun.misc.Launcher 实例:

    sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher(); //获取Java启动器  
    ClassLoader classLoader = launcher.getClassLoader();          //获取类加载器ClassLoader用来加载class到内存来

      sun.misc.Launcher使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。在Launcher的内部,其定义了两个类加载器(ClassLoader),分别是sun.misc.Launcher.ExtClassLoader和sun.misc.Launcher.AppClassLoader,这两个类加载器分别被称为拓展类加载器(Extension ClassLoader)和应用类加载器(Application ClassLoader)。如下图所示:

      图例注释:除了引导类加载器(Bootstrap Class Loader )的所有类加载器,都有一个能力,就是判断某一个类是否被引导类加载器加载过,如果加载过,可以直接返回对应的Class<T> instance,如果没有,则返回null.  图上的指向引导类加载器的虚线表示类加载器的这个有限的访问 引导类加载器的功能。

      此时的launcher.getClassLoader()方法将会返回AppClassLoader实例,AppClassLoader将ExtClassLoader作为自己的父加载器。

      当ppClassLoader加载类时,会首先尝试让父加载器ExtClassLoader进行加载,如果父加载器ExtClassLoader加载成功,则AppClassLoader直接返回父加载器ExtClassLoader加载的结果;如果父加载器ExtClassLoader加载失败,AppClassLoader则会判断该类是否是引导的系统类(即是否是通过Bootstrap类加载器加载,这会调用Native方法进行查找);若要加载的类不是系统引导类,那么ClassLoader将会尝试自己加载,加载失败将会抛出“ClassNotFoundException”。

      具体AppClassLoader的工作流程如下所示:

      双亲委派模型(parent-delegation model)

      上面讨论的应用类加载器AppClassLoader的加载类的模式就是我们常说的双亲委派模型(parent-delegation model)。对于某个特定的类加载器而言,应该为其指定一个父类加载器,当用其进行加载类的时候:

      1. 委托父类加载器帮忙加载;
      2. 父类加载器加载不了,则查询引导类加载器有没有加载过该类;
      3. 如果引导类加载器没有加载过该类,则当前的类加载器应该自己加载该类;
      4. 若加载成功,返回 对应的Class<T> 对象;若失败,抛出异常“ClassNotFoundException”。

      请注意

      双亲委派模型中的”双亲”并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器。上面的步骤中,有两个角色:

      1. 父类加载器(parent classloader):它可以替子加载器尝试加载类
      2. 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。

      一般情况下,双亲加载模型如下所示:

     

    Step 4. 使用类加载器ClassLoader加载Main类

      通过 launcher.getClassLoader()方法返回AppClassLoader实例,接着就是AppClassLoader加载 org.luanlouis.jvm.load.Main类的时候了。

    ClassLoader classloader = launcher.getClassLoader();//取得AppClassLoader类  
    classLoader.loadClass(”org.luanlouis.jvm.load.Main”);//加载自定义类  

      上述定义的org.luanlouis.jvm.load.Main类被编译成org.luanlouis.jvm.load.Main class二进制文件,这个class文件中有一个叫常量池(Constant Pool)的结构体来存储该class的常亮信息。常量池中有CONSTANT_CLASS_INFO类型的常量,表示该class中声明了要用到那些类:

      当AppClassLoader要加载 org.luanlouis.jvm.load.Main类时,会去查看该类的定义,发现它内部声明使用了其它的类: sun.security.pkcs11.P11Util、java.lang.Object、java.lang.System、java.io.PrintStream、java.lang.Class;org.luanlouis.jvm.load.Main类要想正常工作,首先要能够保证这些其内部声明的类加载成功。所以AppClassLoader要先将这些类加载到内存中。(注:为了理解方便,这里没有考虑懒加载的情况,事实上的JVM加载类过程比这复杂的多)

      加载顺序:

      1. 加载java.lang.Object、java.lang.System、java.io.PrintStream、java,lang.Class

      AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader发现不是其加载范围,其返回null;AppClassLoader发现父类加载器ExtClassLoader无法加载,则会查询这些类是否已经被BootstrapClassLoader加载过,结果表明这些类已经被BootstrapClassLoader加载过,则无需重复加载,直接返回对应的Class<T>实例;

      2. 加载sun.security.pkcs11.P11Util

       此在{JRE_HOME}/lib/ext/sunpkcs11.jar包内,属于ExtClassLoader负责加载的范畴。AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader发现其正好属于加载范围,故ExtClassLoader负责将其加载到内存中。ExtClassLoader在加载sun.security.pkcs11.P11Util时也分析这个类内都使用了哪些类,并将这些类先加载内存后,才开始加载sun.security.pkcs11.P11Util,加载成功后直接返回对应的Class<sun.security.pkcs11.P11Util>实例;

      3. 加载org.luanlouis.jvm.load.Main

      AppClassLoader尝试加载这些类的时候,会先委托ExtClassLoader进行加载;而ExtClassLoader发现不是其加载范围,其返回null;AppClassLoader发现父类加载器ExtClassLoader无法加载,则会查询这些类是否已经被BootstrapClassLoader加载过。而结果表明BootstrapClassLoader 没有加载过它,这时候AppClassLoader只能自己动手负责将其加载到内存中,然后返回对应的Class<org.luanlouis.jvm.load.Main>实例引用;

      以上三步骤都成功,才表示classLoader.loadClass(“org.luanlouis.jvm.load.Main”)完成,上述操作完成后,JVM内存方法区的格局会如下所示:

      如上图所示:

      JVM方法区的类信息区是按照类加载器进行划分的,每个类加载器会维护自己加载类信息;

      某个类加载器在加载相应的类时,会相应地在JVM内存堆(Heap)中创建一个对应的Class<T>,用来表示访问该类信息的入口

    Step 5. 使用Main类的main方法作为程序入口运行程序

    Step 6. 方法执行完毕,JVM销毁,释放内存

    三、类加载器有哪些?其组织结构是怎样的?

      类加载器(Class Loader):顾名思义,指的是可以加载类的工具。JVM自身定义了三个类加载器:引导类加载器(Bootstrap Class Loader)、拓展类加载器(Extension Class Loader )、应用加载器(Application Class Loader)。当然,我们有时候也会自己定义一些类加载器来满足自身的需要。

      引导类加载器(Bootstrap Class Loader): 该类加载器使JVM使用C/C++底层代码实现的加载器,用以加载JVM运行时所需要的系统类,这些系统类在{JRE_HOME}/lib目录下。由于类加载器是使用平台相关的底层C/C++语言实现的, 所以该加载器不能被Java代码访问到。但是,我们可以查询某个类是否被引导类加载器加载过。我们经常使用的系统类如:java.lang.String,java.lang.Object,java.lang*……. 这些都被放在 {JRE_HOME}/lib/rt.jar包内, 当JVM系统启动的时候,引导类加载器会将其加载到 JVM内存的方法区中。

       拓展类加载器(Extension Class Loader): 该加载器是用于加载 java 的拓展类 ,拓展类一般会放在 {JRE_HOME}/lib/ext/ 目录下,用来提供除了系统类之外的额外功能。拓展类加载器是是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的。

      应用类加载器(Applocatoin Class Loader): 该类加载器是用于加载用户代码,是用户代码的入口。我经常执行指令 java   xxx.x.xxx.x.x.XClass , 实际上,JVM就是使用的AppClassLoader加载 xxx.x.xxx.x.x.XClass 类的。应用类加载器将拓展类加载器当成自己的父类加载器,当其尝试加载类的时候,首先尝试让其父加载器-拓展类加载器加载;如果拓展类加载器加载成功,则直接返回加载结果Class<T> instance,加载失败,则会询问是否引导类加载器已经加载了该类;只有没有加载的时候,应用类加载器才会尝试自己加载。由于xxx.x.xxx.x.x.XClass是整个用户代码的入口,在Java虚拟机规范中,称其为 初始类(Initial Class)。

       用户自定义类加载器(Customized Class Loader):用户可以自己定义类加载器来加载类。所有的类加载器都要继承java.lang.ClassLoader类。

    四、双亲加载模型的逻辑和底层代码实现是怎样的?

      上面已经不厌其烦地讲解什么是双亲加载模型,以及其机制是什么,这些东西都是可以通过底层代码查看到的。

      我们也可以通过JDK源码看java.lang.ClassLoader的核心方法 loadClass()的实现:

    //提供class类的二进制名称表示,加载对应class,加载成功,则返回表示该类对应的Class<T> instance 实例  
    public Class<?> loadClass(String name) throws ClassNotFoundException {  
        return loadClass(name, false);  
    }  
    
    
    protected Class<?> loadClass(String name, boolean resolve)  
        throws ClassNotFoundException  
    {  
        synchronized (getClassLoadingLock(name)) {  
            // 首先,检查是否已经被当前的类加载器记载过了,如果已经被加载,直接返回对应的Class<T>实例  
            Class<?> c = findLoadedClass(name);  
                //初次加载  
                if (c == null) {  
                long t0 = System.nanoTime();  
                try {  
                    if (parent != null) {  
                        //如果有父类加载器,则先让父类加载器加载  
                        c = parent.loadClass(name, false);  
                    } else {  
                        // 没有父加载器,则查看是否已经被引导类加载器加载,有则直接返回  
                        c = findBootstrapClassOrNull(name);  
                    }  
                } catch (ClassNotFoundException e) {  
                    // ClassNotFoundException thrown if class not found  
                    // from the non-null parent class loader  
                }  
                // 父加载器加载失败,并且没有被引导类加载器加载,则尝试该类加载器自己尝试加载  
                if (c == null) {  
                    // If still not found, then invoke findClass in order  
                    // to find the class.  
                    long t1 = System.nanoTime();  
                    // 自己尝试加载  
                    c = findClass(name);  
    
                    // this is the defining class loader; record the stats  
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);  
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);  
                    sun.misc.PerfCounter.getFindClasses().increment();  
                }  
            }  
            //是否解析类   
            if (resolve) {  
                resolveClass(c);  
            }  
            return c;  
        }  
    }  

      相对应地,我们可以整理出双亲模型的工作流程图:

      相信读者看过这张图后会对双亲加载模型有了非常清晰的脉络。当然,这是JDK自身默认的加载类的行为,我们可以通过继承复写该方法,改变其行为。

    五、类加载器与Class<T>  实例的关系

    六、线程上下文加载器

      Java 任何一段代码的执行,都有对应的线程上下文。如果我们在代码中,想看当前是哪一个线程在执行当前代码,我们经常是使用如下方法:

    Thread  thread = Thread.currentThread();//返回对当当前运行线程的引用 

      相应地,我们可以为当前的线程指定类加载器。在上述的例子中, 当执行:

    java org.luanlouis.jvm.load.Main

    的时候,JVM会创建一个Main线程,而创建应用类加载器AppClassLoader的时候,会将AppClassLoader设置成Main线程的上下文类加载器:

    public Launcher() {  
          Launcher.ExtClassLoader var1;  
          try {  
              var1 = Launcher.ExtClassLoader.getExtClassLoader();  
          } catch (IOException var10) {  
              throw new InternalError(“Could not create extension class loader”, var10);  
          }  
    
          try {  
              this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);  
          } catch (IOException var9) {  
              throw new InternalError(“Could not create application class loader”, var9);  
          }  
    //将AppClassLoader设置成当前线程的上下文加载器  
          Thread.currentThread().setContextClassLoader(this.loader);  
          //…….  
    
      }  

      线程上下文类加载器是从线程的角度来看待类的加载,为每一个线程绑定一个类加载器,可以将类的加载从单纯的 双亲加载模型解放出来,进而实现特定的加载需求。

    展开全文
  • JVM类加载过程-面试题

    2020-10-26 11:37:11
    类加载过程大致分为三部分:加载、连接、初始化,其中连接过程分为:验证、准备、解析,其中顺序没有明确要求,总的来说在初始化之前前面步骤都要完成,具体以什么顺序完成则没有明确规定。 加载 加载过程分为三...

    前言

    我们知道虚拟机JVM要运行一个代码,需要我们先把.java文件编译成.class文件,然后把.class文件加载到JVM中,最后运行main方法。本篇class文件加载到JVM过程,面试经常问到。

    类周期

    类加载过程

    类加载过程大致分为三部分:加载、连接、初始化,其中连接过程分为:验证、准备、解析,其中顺序没有明确要求,总的来说在初始化之前前面步骤都要完成,具体以什么顺序完成则没有明确规定。

    加载

    加载过程分为三步骤:

    1、获取定义此类的二进制字节流,通过包名类名定位到class文件流,也可以自定义通过网络或者其他方式获取,通过这个性质,可以自定义类加载器实现热加载等等。

    2、二进制字节流代表的静态存储结构转化成运行时方法区的数据结构,static修饰的变量

    3、内存中生成java.lang.Class对象代表此类,作为方法区这个类的各种数据访问接口(java.lang.Class也是对象但存储在方法区,不存储在堆)

    验证(连接)

    验证过程大致分为四大类,当然验证的逻辑不仅仅这些,详细过程方式不细讲。

    1、文件格式验证:字节流是否符合Class文件规范

    2、元数据验证:类描述是否符合规范

    3、字节码验证:语义定义是否符合规范

    4、符合引用验证:根据全限名是否能找到类等等

    准备(连接)

    正式为类变量分配内存和初始化(类变量可以理解为static修饰的变量),初始化是把变量赋值为数据类型的零值(java中基本类型的默认值是0,引用类型会默认为null),除非被final修饰的类变量,会直接初始化为指定值。

    解析(连接)

    将常量池内的符号引用替换成直接引用的过程

    初始化

    类加载过程的最后步骤,根据程序员的主观计划去初始化类变量和其他资源,执行clinit()方法过程,根据编写的代码对类变量进行赋值等操作,执行父类、子类的static变量和static静态块,注意如果是接口,不用执行父类的static变量和static静态块,除非子类用到父类的static变量和static静态块。

    展开全文
  • JVM类加载机制详解(一)JVM类加载过程

    万次阅读 多人点赞 2016-05-05 16:07:06
    1、什么是类加载类加载的时机? 2、什么是初始化?什么时候进行初始化? 3、什么时候会为变量分配内存? 4、什么时候会为变量赋默认初值?什么时候会为变量赋人为设定的初值? 5、类加载器是什么? 6、如何...

    首先Throws(抛出)几个自己学习过程中一直疑惑的问题:

    1、什么是类加载?什么时候进行类加载?

    2、什么是类初始化?什么时候进行类初始化?

    3、什么时候会为变量分配内存?

    4、什么时候会为变量赋默认初值?什么时候会为变量赋程序设定的初值?

    5、类加载器是什么?

    6、如何编写一个自定义的类加载器?


    首先,在代码编译后,就会生成JVM(Java虚拟机)能够识别的二进制字节流文件(*.class)。而JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM的类加载机制


    Class文件中的“类”从加载到JVM内存中,到卸载出内存过程有七个生命周期阶段。类加载机制包括了前五个阶段。

    如下图所示:


    其中,加载、验证、准备、初始化、卸载的开始顺序是确定的,注意,只是按顺序开始,进行与结束的顺序并不一定。解析阶段可能在初始化之后开始。


    另外,类加载无需等到程序中“首次使用”的时候才开始,JVM预先加载某些类也是被允许的。(类加载的时机


    一、类的加载

    我们平常说的加载大多不是指的类加载机制,只是类加载机制中的第一步加载。在这个阶段,JVM主要完成三件事:

    1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。

    2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)

    3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。


    二、类的连接

    类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段。

    1、验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。

    2、准备:为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100;

    静态变量a就会在准备阶段被赋默认值0。

    对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。

    另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 666;  静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。

    3、解析:将类的二进制数据中的符号引用换为直接引用。


    三、类的初始化

    类初始化是类加载的最后一步,除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码。

    类的初始化的主要工作是为静态变量赋程序设定的初值。

    如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100。


    Java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化

    1、使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。

    2、通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。

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

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

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


    注意,虚拟机规范使用了“有且只有”这个词描述,这五种情况被称为“主动引用”,除了这五种情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”。


    被动引用的例子一:

    通过子类引用父类的静态字段,对于父类属于“主动引用”的第一种情况,对于子类,没有符合“主动引用”的情况,故子类不会进行初始化。代码如下:

    //父类
    public class SuperClass {
    	//静态变量value
    	public static int value = 666;
    	//静态块,父类初始化时会调用
    	static{
    		System.out.println("父类初始化!");
    	}
    }
    
    //子类
    public class SubClass extends SuperClass{
    	//静态块,子类初始化时会调用
    	static{
    		System.out.println("子类初始化!");
    	}
    }
    
    //主类、测试类
    public class NotInit {
    	public static void main(String[] args){
    		System.out.println(SubClass.value);
    	}
    }
    输出结果:


    被动引用的例子之二:

    通过数组来引用类,不会触发类的初始化,因为是数组new,而类没有被new,所以没有触发任何“主动引用”条款,属于“被动引用”。代码如下:

    //父类
    public class SuperClass {
    	//静态变量value
    	public static int value = 666;
    	//静态块,父类初始化时会调用
    	static{
    		System.out.println("父类初始化!");
    	}
    }
    
    //主类、测试类
    public class NotInit {
    	public static void main(String[] args){
    		SuperClass[] test = new SuperClass[10];
    	}
    }

     没有任何结果输出! 
    

    被动引用的例子之三:

    刚刚讲解时也提到,静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类,这是一个特例,需要特别记忆,不会触发类的初始化!

    //常量类
    public class ConstClass {
    	static{
    		System.out.println("常量类初始化!");
    	}
    	
    	public static final String HELLOWORLD = "hello world!";
    }
    
    //主类、测试类
    public class NotInit {
    	public static void main(String[] args){
    		System.out.println(ConstClass.HELLOWORLD);
    	}
    }
     
    


    下一篇:JVM类加载机制详解(二)类加载器与双亲委派模型


    展开全文
  • JVM类加载过程.pptx

    2021-05-07 15:16:58
    JVM类加载过程
  • JVM类加载过程简述

    2020-12-25 13:46:17
    jvm加载一个的时候会根据全类名也就是带有包名的完整类名找到相应的字节码文件也就是编译生成的.class文件, 然后生成一个java.lang.Class的,这个就是要加载, 然后还需要 ①校验,包括字节码校验、元...
  • JVM把Class文件中的描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM类加载机制。Class文件中的“”...
  • JVM类加载过程实例

    2020-09-08 20:05:26
    JVM类加载过程实例 package com.lbl; public class ClassInitTest { private static int num=1; static { num=2; number=20; } private static int number=10; public static void main(String[] args) {...
  • 一、类加载过程分析 我们通过ide写的java代码,毫无疑问是最终需要加载到JVM来运行的。试想JVM作为跨语言的平台,能同时支持多种编程语言(js、groory、scala…等)的字节码文件运行,那么在字节码文件和JVM之间,...
  • JVM类加载过程详解

    2018-06-07 13:33:47
    jvm将class文件加载到内存,并对其进行校验,转换和初始化,最终形成jvm可以直接操作的内容,这就是虚拟机的加载机制的生命周期:加载到内存,再到使用,再到被卸载,整个生命周期经历的步骤如下图:类加载...
  • JVM类加载过程

    千次阅读 2018-04-17 20:52:12
    (一)类加载JVM内存中图解: (二)(JVM)工作机制:(1)装载:查找和导入Class(二进制)文件。(2)链接:把的二进制数据合并到JRE中; ①验证:检查载入Class文件数据的正确性。 ②准备:给的静态...
  • 看图说话,加载->创建对象->垃圾回收流程图 相关资料 JVM 关于对象分配在堆、栈、TLAB的理解
  • JVM类加载器详解

    2021-01-11 23:12:03
    在上一篇中,通过下面这幅图大致了JVM整体的内部运行结构图,在JVM的结构中,类加载子系统作为连接外部class文件与真正将class文件加载到运行时数据区,承担着重要的作用 类加载器是什么?有什么作用? 1、负责从...
  • jvm类加载和解析过程(总结性质)

    千次阅读 2018-03-18 22:47:30
    类加载机制的定义:把class文件加载到内存,对数据进行校验解析和初始化最终形成虚拟机直接使用的java类型。具体来说:1 通过一个的全限定名来获取定义此类的二进制字节流。2 将这个字节流所代表的静态存储结构...
  • JVM(四)—一道面试题搞懂JVM类加载机制

    万次阅读 多人点赞 2017-07-05 19:35:20
    参考:《深入理解Java虚拟机》 MyBlog:https://nomico271.github.io/2017/07/07/JVM%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/ 2017/07/05 In NJ — PS 链接加上一个面试题,巩固下加载过程。...
  • 主要是为了安全,避免用户恶意加载破坏JVM正常运行的字节码文件,比如说加载一个自己写的java.util.HashMap.class。这样就有可能造成包冲突问题。 类加载器种类 启动类加载器:用于加载jdk中rt.jar的字节码文件 ...
  • JVM类加载过程

    2021-05-29 16:12:19
    近来读了《深入理解JVM虚拟机》的部分内容,对JVM也慢慢有个整体的认识,今天就来分享一下我对JVM类加载过程的学习和理解。 基础知识 我们平时写的Java写代码一般都是.java文件,编译成为.class字节码文件,然后...
  • jvm类加载机制

    2020-11-27 14:37:28
    过程如下图: 加载、验证、准备、初始化、卸载这5步顺序是一定的。而初始化解析阶段可能在初始化之后。因为java支持运行时绑定 一、加载阶段 jvm会做下面的事情: 通过的全限定名取到的二进制数据流 把...
  • JVM规范允许类加载器在预料某个将要被使用时就预先加载它,下图为实例方法被调用时的JVM内存模型,1~7完整的描述了从类加载开始到方法执行前的预备过程,后面将对每一个步骤进行解释 在我们加载类过程中经过了...
  • JVM类加载机制全面解析 什么是类加载机制 JVM把描述的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型,这就是JVM类加载机制。 的生命周期 从被...
  • 前言: 写了不少java代码,以前一直都是关心java程序跑起来后...为此,JVM专门有一个类加载机制,用于处理.class文件加载到内存这个过程。 正文: JVM类加载机制分为5个步骤: 1.加载 2.验证 3.准备 4.解析 5.验证 ...
  • jvm类加载顺序

    千次阅读 2017-11-29 16:52:59
    JVM类加载顺序 今天,梳理一下类加载顺序,从测试结果来推测类加载顺序。 第一步:基础准备 父类: public class Parent { public String parentProperty="ParentPropertyValue"; public static ...
  • java程序在对某个进行引用、使用时,就会开始对该进行加载,比如直接使用类加载器进行显式加载、创建该的对象、使用该变量等情况。加载是通过java虚拟机的类加载子系统完成的。加载主要分为三个...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 207,633
精华内容 83,053
关键字:

jvm类加载过程