zoukankan      html  css  js  c++  java
  • 实战java虚拟机(五)——Class装载

    前言


    上一篇文章简单学习了Class文件的结构,这次看看虚拟机如何加载Class文件,并且在加载过后做什么额外的处理

    Class文件的装载流程


      Class文件的装载流程可以分为加载,连接和初始化 3 步,其中连接又分为验证,准备和解析 3 步,整体流程如下图

    1. 类装载的条件

      Class 文件只有在必须使用的时候才装载,Java虚拟机规定,一个类或接口在初次使用前,必须进行初始化。这里的使用是指主动使用,只有下列的情况才会主动使用

    • 创建一个类的实例时,比如使用new关键字或反射、克隆、反序列化
    • 调用类的静态方法时,即使用了字节码的invokestatic指令
    • 使用类或接口的静态字段时(final常量除外),比如使用getstatic或putstatic指令
    • 使用java.lang.reflect包中的方法反射类的方法时
    • 初始化子类时,要求先初始化父类
    • 作为启动虚拟机,含有main方法的那个类

    除了以上的情况属于主动使用,其余都是被动使用,被动使用不会引起类的初始化

    主动使用容易理解,下面举一个被动使用的例子方便记忆

    public class Parent {
        static{
            System.out.println("Parent init");
        }
        public static int v = 100;
    }
    
    public class Child extends Parent{
        static {
            System.out.println("Child init");
        }
    }
    
    public class Test1 {
        public static void main(String[] args) {
            System.out.println(Child.v);
        }
    }

    运行以上代码的输出如下

    Parent init
    100

    虽然在Test1中直接访问了子类对象,但是Child并没有被初始化,只有Parent被初始化,由此可见,在使用一个字段时,只有直接定义了该字段的类才会被初始化(虽然此处Child没有被初始化,但是它已经被加载,只是没有进入初始化阶段)

    使用-XX:+TraceClassLoading运行这段代码,可以得到下列日志(仅截取小部分)

    [Loaded com.xxxx.Parent from file:/D:/ideaWorkSpace/xxxx/target/classes/]
    [Loaded com.xxxx.Child from file:/D:/ideaWorkSpace/xxxx/target/classes/]
    Parent init
    100

    可以看到两个类都已经被加载到系统

    2. 加载类

      加载类处于类装载的第一个阶段。加载类时,虚拟机会完成以下工作:

    • 通过类的全名获取类的二进制数据流
    • 解析类的二进制数据流为方法区内的数据结构
    • 创建java.lang.Class类的实例,表示该类型

      类的二进制数据流可以从文件系统中的Class文件,也可以是zip等文件中提取类文件,也可以从网络加载,甚至在运行时生成一段Class的二进制信息。获取二进制信息后,Java虚拟机会处理这些数据并最终转为一个java.lang.Class的实例,Class实例是访问类型元数据的接口,也是实现反射的关键数据。

    3. 验证类

      类加载到系统后就开始连接操作,验证是连接的第一步,目的是保证加载的字节码是合法、合理且符合规范的,验证的步骤比较复杂,大体上需要做的检查如下图:

    4. 准备

      类验证通过时,虚拟机就会进入准备阶段。这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。各类型变量的初始值如下表

    各类型变量默认初始值
    类型 默认初始值
    int 0
    long 0L
    short (short)0
    char u0000
    boolean false
    reference null
    float 0f
    double 0f

    Java并不支持boolean类型,对于boolean实际上是int,由于int默认为0,故boolean默认为false

    如果类存在常量字段,那么常量字段也会在准备阶段附上正确的值。

    5. 解析类

      连接的第三步是解析阶段,解析阶段的工作就是将类、接口、字段和方法的符号引用转为直接引用。符号引用就是一些字面量的引用,与虚拟机内部数据结构与内存布局无关。在Class类文件中,通过常量池进行了大量的符号引用。

    invokevirtual #24 <java/io/PrintStream.priintln>

      这个是System.out.println()的字节码,可以看到它使用了常量池的第24项,查看并分析常量池,可以看到如下的结构

     常量池第24项被invokevirtual引用,顺着CONSTANT_Methodref #24的引用关系查找,最终发现所有对于Class及NameAndType类型的引用都是基于字符串的。因此,可以认为invokevirtual函数调用通过字面量的引用描述已经表达清楚。这就是符号引用。

      程序实际运行时只有符号引用是不够的,当方法被调用时,系统需要明确知道方法的位置。以方法为例,Java虚拟机为每个方法都准备了一张方法表,所有的方法都列在其中,当需要调用一个类的方法时,只要知道这个方法在方法表中的偏移量就可以直接调用。通过解析操作,符号引用可以转变为目标方法在类的方法表中的位置。

      综上所述,解析就是将符号引用转换为直接引用,得到类、字段或方法在内存中的指针或偏移量。

    6. 初始化

      初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,表示类可以顺利装载到系统中。此时,类才会开始执行Java字节码。初始化阶段的重要工作就是执行类的初始化方法<clinit>。方法<clinit>是由编译器自动生成的,它是由类静态成员的赋值语句及static语句块共同产生的。

    public class SimpleDemo {
        public static int id = 1;
        public static int number;
        static {
            number = 4;
        }
    }

    比如上面这个类,编译后的字节码如下

    public class SimpleDemo {
         <ClassVersion=52>
         <SourceFile=SimpleDemo.java>
    
         public static int id;
         public static int number;
    
         public SimpleDemo() { // <init> //()V
             <localVar:index=0 , name=this , desc=LSimpleDemo;, sig=null, start=L1, end=L2>
    
             L1 {
                 aload0 // reference to self
                 invokespecial java/lang/Object.<init>()V
                 return
             }
             L2 {
             }
         }
    
         static  { // <clinit> //()V
             L1 {
                 iconst_1
                 putstatic SimpleDemo.id:int
             }
             L2 {
                 iconst_4
                 putstatic SimpleDemo.number:int
             }
             L3 {
                 return
             }
         }
    }

    可以看到,生成的<clinit>方法中,整合了这个类的static赋值语句和static语句块,先后对id和number进行赋值。

    前面提到过加载一个类之前,虚拟机总会尝试加载该类的父类,因此父类的<clinit>方法总在子类的<clinit>方法之前被调用,也就是说,子类的static块是在父类之后执行。

    并非所有的类都会产生<clinit>方法,如果一个类没有赋值语句,也没有static语句块,那么编译器就不会为该类生成<clinit>方法。

    还有一个重要的点是,虚拟机会确保<clinit>方法执行的线程安全,当多个线程试图初始化同一个类时,只有一个线程可以进入<clinit>方法,如果第一个线程成功执行,则后面的线程就不会再执行该方法了。

    也是由于<clinit>是带锁线程安全的,所以在多线程环境下进行类初始化时,可能引发死锁,并且这种死锁很难发现。下面有一个死锁的例子

    class StaticA{
        static{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e){
    
            }
            try {
                Class.forName("com.blog.test.StaticB");
            } catch (ClassNotFoundException e){
    
            }
            System.out.println("com.blog.test.StaticA init success");
        }
    }
    
    class StaticB {
        static{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e){
    
            }
            try {
                Class.forName("com.blog.test.StaticA");
            } catch (ClassNotFoundException e){
    
            }
            System.out.println("com.blog.test.StaticB init success");
        }
    }
    
    public class StaticDeadLockMain extends Thread {
        private char flag;
        public StaticDeadLockMain(char flag){
            this.flag = flag;
            this.setName("Thread"+flag);
        }
    
        @Override
        public void run() {
            try {
                Class.forName("com.blog.test.Static" + flag);
            } catch (ClassNotFoundException e){
                e.printStackTrace();
            }
            System.out.println(getName() + "over");
        }
    
        public static void main(String[] args) {
            StaticDeadLockMain loadA = new StaticDeadLockMain('A');
            loadA.start();
            StaticDeadLockMain loadB = new StaticDeadLockMain('B');
            loadB.start();
        }
    }

    上面的代码简单来说就是一个类在静态代码块中初始化另一个类,形成相互初始化抢占资源。上述代码中执行main方法,main方法一直不停止,也没有输出,通过jstack查看堆栈信息如下

     堆栈信息中也没有足够信息可以判断发生死锁,但是死锁确实存在,所以我们在初始化类时,要格外小心这种情况的死锁。

  • 相关阅读:
    一道经典的JavaScript面试题
    模拟学信网登录,Cookie 序列化,在反序列化之后不能用的问题
    EF搜索数据自动将表名变复数问题
    EF查看sql的方法
    查看当前正在被执行的sql
    mmsql查看最近操作日志
    查询存储过程里面的含有的关键字
    mmsql 查询每个分类的前3条数据
    C# checked关键字当属性
    windows2012 IIS8.5 不能在此路径中使用此配置节
  • 原文地址:https://www.cnblogs.com/gtblog/p/11654875.html
Copyright © 2011-2022 走看看