zoukankan      html  css  js  c++  java
  • JVM类加载机制

    一、从JVM架构说起

    image-20200507160216558

    JVM分为三个子系统:

    (1)类加载器子系统 (2)运行时数据区 (3)执行引擎

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

    package com.darchrow;
    
    /**
     * @author mdl
     * @date 2020/5/7 16:12
     */
    public class HelloWorld {
        public static void main(String[] args) {
            System.out.println("Hello World!");
        }
    }
    

    编译: 源文件-->class文件,这个时候只是将源文件翻译为JVM可识别的文件,落到磁盘上。

    javac HelloWorld.java
    

    运行:类加载器在此期间负责将class加载到内存,最终形成可以被JVM直接使用的java类型。

    java HelloWord
    

    系统将调用{JAVA_HOME}/bin/java.exe完成以下步骤:

    1. 为JVM申请内存空间
    2. 创建引导加载器BootstrapClassLoader实例,加载系统类到内存方法区域
    3. 创建启动类实例Launcher,并取得类加载器ClassLoader
    4. 使用获取到的ClassLoader加载我们定义的HelloWorld类
    5. 加载完成JVM执行main方法
    6. 结束,java程序结束,JVM销毁
    
    1. 引导类加载器 Bootstrap ClassLoader,它加载的是 {JRE_HOME}/lib下的类

      image-20200507174150684

    2. 创建启动类实例Launcher,并取得类加载器ClassLoader,这个时候获取到的有

      ExtClassLoader、AppClassLoader

      image-20200507174213370

    三、类的加载

    ​ 类加载器并不需要程序“首次主动使用”时加载它,JVM规范允许加载器在预料某个类将要被使用时就预先加载它,如果在预先加载时出现错误,类加载器必须在程序首次主动时报告错误,如果这个类一直没有被程序主动使用,那么加载器不会报告此错误。

    ​ 加载后数据的存储大概是这个样子的

    image-20200508105612803

    ​ 类加载的过程

    image-20200508105844967

    1) 加载

    	1. .class文件到内存
    
     		2. 类的数据结构到方法区
              		3. 堆区生成一个java.lang.Class对象,作为方法区数据结构的入口
    

    2)连接

    2.1) 校验:确保被加载的类的正确性

    文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

    元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

    字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

    符号引用验证:确保解析动作能正确执行。

    2.2) 准备:为类变量在方法区分配内存,并将其初始化为零值

    ​ 类变量是被static修饰的变量

    public static int value = 123;
    

    ​ 准备阶段过后初始值为0

    ​ 也有特例

    public static final int value = 123;
    

    final修饰的static变量为常量,准备阶段后就是123了

    ​ 实例变量不会在此阶段分配内存,它将会在对象实例化时随着对象一起分配到堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只能进行一次,实例化可以进行多次)

    2.3)解析:把类中的符号引用转换为直接引用

    符号引用就是一组符号来描述目标,可以是任何字面量。

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

    举例:

    package com.darchrow.demo;
    
    /**
     * @author mdl
     * @date 2020/5/8 11:44
     */
    public class Animal {
    }
    
    
    package com.darchrow.demo;
    
    /**
     * @author mdl
     * @date 2020/5/8 11:44
     */
    public class Person {
    
        private Animal pet;
    
    }
    
    

    Person类中引用了Animal,编译阶段只能用com.darchrow.demo.Animal代表Animal的真实地址,解析阶段,JVM将解析该符号引用,来确定com.darchrow.demo.Animal的真实地址(如果该类还没加载过,则先加载)。

    3)初始化:虚拟机执行类构造器 <clinit>() 方法的过程

    1. 声明类变量为初始值

    2. 使用静态代码块为类变量指定初始值

    <clinit>()方法有如下特点:

    • 按顺序收集。特别注意的是,static{}代码块只能访问定义在它之前的变量,定义在它之后的变量,只能赋值,不能访问。

      package com.darchrow.demo;
      
      /**
       * @author mdl
       * @date 2020/5/8 14:24
       */
      public class TestStatic {
      
          static {
              int i = 0;
              // System.out.println(i); 这句编译器会提示“非法向前引用”
          }
      
          static int i = 1;
      
          public static void main(String[] args) {
              System.out.println(TestStatic.i);
          }
      }
      
      

      image-20200508143339845

    • 虚拟机会保证子类() 方法运行之前,父类的 () 方法已经执行结束。

      package com.darchrow.demo;
      
      /**
       * @author mdl
       * @date 2020/5/8 14:36
       */
      public class Parent {
      
          static{
              System.out.println("父类静态代码块");
          }
      
          public Parent() {
              // 实例化发生在类加载之后
              System.out.println("父类构造函数");
          }
      }
      
      
      package com.darchrow.demo;
      
      /**
       * @author mdl
       * @date 2020/5/8 14:36
       */
      public class Children extends Parent {
      
          static {
              System.out.println("子类静态代码块");
          }
      
          public Children() {
              // 实例化发生在类加载之后
              System.out.println("子类构造函数");
          }
      
          public static void main(String[] args) {
              new Children();
          }
      
      }
      
      

      image-20200508145338927

    • () 方法对于类非必须,如果类中不包含静态变量、静态代码块,编译器可以不生成此方法

    • 接口中不可以使用静态语句块,但可以声明静态变量。区别于类,接口的() 方法不需要先去执行父类的() 方法,只有当父类中定义的变量使用时,父接口才初始化。另外,接口的实现类在初始化时同样不会调用接口的() 方法。

    • () 方法多线程下已被正确的加锁和同步,如果多线程初始化一个类,将只有一个线程执行,其他线程将阻塞等待知道执行线程执行完毕。注意避免在一个类的() 方法中有耗时操作。

      初始化时机:其实就是主动引用
      1. new对象、访问类变量(final static除外)、类方法
      2. 使用反射:java.lang.reflect,如果类未被初始化,则先初始化
      3. 初始化类时发现父类未被初始化,先触发父类的初始化
      4. 虚拟机启动,用于指定主类并执行main方法,其实我们发现main方法就是类方法
      5. jdk1.7 动态语言支持

      例外的情况即被动引用

      • 子类引用父类的静态字段,不会触发子类的初始化

        package com.darchrow.demo.passive;
        
        /**
         * @author mdl
         * @date 2020/5/8 15:16
         */
        public class SuperClass {
        
            public static int value =100;
        
            static {
                System.out.println("父类静态代码块");
            }
        
        }
        
        package com.darchrow.demo.passive;
        
        /**
         * @author mdl
         * @date 2020/5/8 15:16
         */
        public class SubClass extends SuperClass {
        
            static {
                System.out.println("子类静态代码块被调用");
            }
        
        }
        
        package com.darchrow.demo.passive;
        
        /**
         * @author mdl
         * @date 2020/5/8 15:19
         */
        public class Test {
        
            public static void main(String[] args) {
                System.out.println(SuperClass.value);
            }
        
        }
        

        image-20200508152113469

      • 通过数组来定义引用类,不会触发此类的初始化,数组继承Object,包含数组的属性和方法。

        还是上面的例子,改下Test类

        package com.darchrow.demo.passive;
        
        /**
         * @author mdl
         * @date 2020/5/8 15:19
         */
        public class Test {
        
            public static void main(String[] args) {
        //        System.out.println(SuperClass.value);
                SuperClass[] sca = new SuperClass[10];
            }
        
        }
        

        控制台无任何打印

      • 常量在编译阶段会进入常量池(方法区),本质上并没有直接引用到定义常量的类,因此不会触发常量的初始化

        package com.darchrow.demo;
        
        /**
         * @author mdl
         * @date 2020/5/8 15:25
         */
        public class ConstClass {
        
            static {
                System.out.println("我被调用了!");
            }
        
            public static final String HELLO_WORLD="hello world";
        
        }
        
        package com.darchrow.demo.passive;
        
        import com.darchrow.demo.ConstClass;
        
        /**
         * 被动引用
         *
         * @author mdl
         * @date 2020/5/8 15:19
         */
        public class Test {
        
            public static void main(String[] args) {
        //        System.out.println(SuperClass.value);
        //        SuperClass[] sca = new SuperClass[10];
                System.out.println(ConstClass.HELLO_WORLD);
            }
        }
        

        image-20200508152842618

    每一步脚印都要扎得深一点!
  • 相关阅读:
    高通电池曲线(转)
    随笔分类
    海思uboot启动流程详细分析(转)
    GPRS以TCP上传数据到服务器OK,但收不到服务器下发的数据
    WIS800C TCP Client 非透传模式连接远程服务器使用方法(转)
    MySQL 不允许从远程访问的解决方法
    Linux下将Mysql和Apache加入到系统服务里的方法
    linux服务器出现严重故障后的原因以及解决方法
    Linux防火墙该如何设置
    Linux下的两个聊天命令的使用方法
  • 原文地址:https://www.cnblogs.com/bloodthirsty/p/12851300.html
Copyright © 2011-2022 走看看