JVM面纱初试探(二)

首先分享下官网网址,所有的开发说明官网才是权威,网上传播的多少会有些曲解https://www.oracle.com/index.html和https://docs.oracle.com/javase/8/,讲解JVM前我们先准备一份JAVA文件

一个java文件交给JVM的过程分为编译和加载两个过程,下面先讲解编译过程编译过程

一、源码文件到类文件

class Person{
private String name="LuJiaXing";
private int age;
private final double salary=100;
private static String address;
private final static String hobby="abc";

public void say(){
System.out.println("jvm.....");
}
public static int calc(int op1,int op2){
op1=3;
int result=op1+op2;
return result;
}
public static void main(String[] args){
System.out.println(calc(1,2));
}
}

通过命令我们 生成class文件

 

 打开我们生成的class文件会发现生成一个16进制的表示方式,当然你也可以理解为是一个二进制的表式方式

 

当然上面的16进制表示方式我们现在还是看不懂,但我们可以进入官网查找JDK的帮助文档进行查看https://docs.oracle.com/javase/specs/jvms/se8/html/index.html,在开发文档第4章节中详细解析了class文件格

 

 

 

 整个 .class 文件本质上就是一张表,由上表所示的数据项构成,由于官网上直译的不太准,所以我在网上找了一份中文的表格,表格如下

 上表其实可以划分7个部分,.class字节码文件包括:

        1.魔数与class文件版本

         2.常量池

         3.访问标志

         4.类索引、父类索引、接口索引

         5.字段表集合

         6.方法表集合

         7.属性表集合

下面将详解.class的7个部分

     a.魔数(Magic Number):.class 文件的第 1 - 4 个字节,它唯一的作用就是确定这个文件是否是一个能被虚拟机接受的 class 文件,其固定值是:0xCAFEBABE(咖啡宝贝)。如果一个 class 文件的魔术不是 0xCAFEBABE,那么虚拟机将拒绝运行这个文件

     b.次版本号(minor version):.class 文件的第 5 - 6 个字节,即编译生成该 .class 文件的 JDK 次版本号

     c.主版本号(major version):.class 文件的第 7 - 8个字节,即编译生成该 .class 文件的 JDK 主版本号,根据官网的截图我们可以知道JDK1.1的十进制是45,由此我们就可以根据科学计算器算出自己的JDK版本号

    d.常量池:紧接着版本号之后的是常量池的入口,常量池可以理解为 class 文件之中的资源仓库,它是占用 class 文件空间最大的数据项之一。常量池是一个集合,它由两部分组成:常量池计数器和常量池

          1.常量池计数器(constant_pool_count) 是一个 u2 的无符号数

          2.常量池(constant_pool):紧跟在常量池计数器后面的内容就是该 .class 文件的常量池内容了,常量池中存放的数据一般分为两种类型:字面量和符号引用;

                    字面量:是指文本字符串、声明为 final 的常量值等
                   符号引用: 是一个更偏向于编译原理方面的概念,主要包括三类常量:1). 类和接口的全限定名,2).字段的名称和描述符,3). 方法的名称和描述符
       在常量池中的常量共有 14 种类型,每个常量都是一个表,每一个表都有各自的组成结构。这 14 个常量有一个公共的特点,就是每个常量开始是一个用 u1 类型的无符号数表示的标志位(tag,取值见下表),表示此常量属于哪种常量类型

        

       首先是常量计数器(constant_pool_coun),数值是:0x 0038,表示此class 文件中共有 55 个常量;cp_info_constant_pool[1]:偏移地址是 0x000A,内容是:0x0a00 0d00 23。0x0A 标志位表示是一个 CONSTANT_Methodref_info 常量,0x00 0d 是一个索引,指向常量池中第 13 个常量所表示的信息;0x00 23 是一个索引,指向常量池第 35 个常量所表示的信息。CONSTANT_Methodref_info 常量的结构如下所示:

    

         e.访问标志:常量池之后是 u2 类型的访问标志位(access_flags),这个访问标志位用于标识类或者接口层次的访问信息,包括:这个 Class 是类还是接口、是否定义为public类型、是否定义为abstract类型,如果是类的话,是否被 final 关键字修饰。具体的标志位以及标志的含义见下表

          

           f.类索引、父类索引、接口索引-------类索引:u2 数据类型,用于确定这个类的全限定名。父类索引:u2 数据类型,用于确定这个类的父类的全限定名。接口索引:u2 数据类型的集合,用于描述类实现了哪些接口,这些被实现的接口将按照 implements 语句 后的顺序从左至右排列在接口索引集合中。接口索引集合分为两部分,第一部分表示接口计数器(interfaces_count),是一个 u2 类型的数据,第二部分是接口索引表表示接口信息,紧跟在接口计数器之后。若一个类实现的接口为 0,则接口计数器的值为 0,接口索引表不占用任何字节。

       后面的字段表集合什么的官网上都有详细解说,可以根据官网的说明一步一步解析就可以看懂这16进制的class文件

二、反编译验证

   经过上面讲解相信如果相看字节码文件大家也可以看的懂了,但是上面字节码那么多,如果一点一点验证查找的看会恶心死,那有没有简单的方式进行查看呢,还真有,那就是反编译

编译指令:javap -v -p Person.class

编译后的文件;下面颜色标记的部分分别是magic、版本号、常量池、字段表集合、方法表集合

G:\jvm>javap -v -p Person.class
Classfile /G:/jvm/Person.class
  Last modified 2022-1-28; size 785 bytes
  MD5 checksum c47cf28574c5b2ebe838241fa8b3893e
  Compiled from "Person.java"
class Person
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #13.#35        // java/lang/Object."<init>":()V
   #2 = String             #36            // LuJiaXing
   #3 = Fieldref           #12.#37        // Person.name:Ljava/lang/String;
   #4 = Double             100.0d
   #6 = Fieldref           #12.#38        // Person.salary:D
   #7 = Fieldref           #39.#40        // java/lang/System.out:Ljava/io/PrintStream;
   #8 = String             #41            // jvm.....
   #9 = Methodref          #42.#43        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = Methodref          #12.#44        // Person.calc:(II)I
  #11 = Methodref          #42.#45        // java/io/PrintStream.println:(I)V
  #12 = Class              #46            // Person
  #13 = Class              #47            // java/lang/Object
  #14 = Utf8               name
  #15 = Utf8               Ljava/lang/String;
  #16 = Utf8               age
  #17 = Utf8               I
  #18 = Utf8               salary
  #19 = Utf8               D
  #20 = Utf8               ConstantValue
  #21 = Utf8               address
  #22 = Utf8               hobby
  #23 = String             #48            // abc
  #24 = Utf8               <init>
  #25 = Utf8               ()V
  #26 = Utf8               Code
  #27 = Utf8               LineNumberTable
  #28 = Utf8               say
  #29 = Utf8               calc
  #30 = Utf8               (II)I
  #31 = Utf8               main
  #32 = Utf8               ([Ljava/lang/String;)V
  #33 = Utf8               SourceFile
  #34 = Utf8               Person.java
  #35 = NameAndType        #24:#25        // "<init>":()V
  #36 = Utf8               LuJiaXing
  #37 = NameAndType        #14:#15        // name:Ljava/lang/String;
  #38 = NameAndType        #18:#19        // salary:D
  #39 = Class              #49            // java/lang/System
  #40 = NameAndType        #50:#51        // out:Ljava/io/PrintStream;
  #41 = Utf8               jvm.....
  #42 = Class              #52            // java/io/PrintStream
  #43 = NameAndType        #53:#54        // println:(Ljava/lang/String;)V
  #44 = NameAndType        #29:#30        // calc:(II)I
  #45 = NameAndType        #53:#55        // println:(I)V
  #46 = Utf8               Person
  #47 = Utf8               java/lang/Object
  #48 = Utf8               abc
  #49 = Utf8               java/lang/System
  #50 = Utf8               out
  #51 = Utf8               Ljava/io/PrintStream;
  #52 = Utf8               java/io/PrintStream
  #53 = Utf8               println
  #54 = Utf8               (Ljava/lang/String;)V
  #55 = Utf8               (I)V
{
  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

  private int age;
    descriptor: I
    flags: ACC_PRIVATE

  private final double salary;
    descriptor: D
    flags: ACC_PRIVATE, ACC_FINAL
    ConstantValue: double 100.0d

  private static java.lang.String address;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC

  private static final java.lang.String hobby;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: String abc

  Person();
    descriptor: ()V
    flags:
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String LuJiaXing
         7: putfield      #3                  // Field name:Ljava/lang/String;
        10: aload_0
        11: ldc2_w        #4                  // double 100.0d
        14: putfield      #6                  // Field salary:D
        17: return
      LineNumberTable:
        line 1: 0
        line 2: 4
        line 4: 10

  public void say();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #8                  // String jvm.....
         5: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8

  public static int calc(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=2
         0: iconst_3
         1: istore_0
         2: iload_0
         3: iload_1
         4: iadd
         5: istore_2
         6: iload_2
         7: ireturn
      LineNumberTable:
        line 12: 0
        line 13: 2
        line 14: 6

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_1
         4: iconst_2
         5: invokestatic  #10                 // Method calc:(II)I
         8: invokevirtual #11                 // Method java/io/PrintStream.println:(I)V
        11: return
      LineNumberTable:
        line 17: 0
        line 18: 11
}
SourceFile: "Person.java"

三、类加载机制

官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html

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

     (1)装载

              a.先找到类文件所在位置

                  通过类装载器ClassLoader.find录找根类(通过不同的类装载器装载不同的东西)这里面就可以聊会类装载器ClassLoader

                   类装载器定义:在装载(Load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的。通过一个类的全限定名获取定义此类的二进制字节流

                   分类:

                           1)Bootstrap ClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。
                           2)Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
                           3)App ClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。
                           4)Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

                  

     

类加载器的双亲委派加载机制(重点):当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

 破坏双亲委派加载机制:重写ClassLoader类的loadClass()方法

               b.类文件的信息交给JVM

              C.类文件所有的对象class交给jvm

       (2)链接

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

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

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

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

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

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

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

           b准备:为类的静态变量分配内存,并将其初始化为默认值              

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

    1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

    2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

   假设一个类变量的定义为:public static int value = 3;

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

 这里还需要注意如下几点:
· 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
· 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
· 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
· 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

   假设上面的类变量value被定义为: public static final int value = 3;

   编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。回忆上一篇博文中对象被动引用的第2个例子,便是这种情况。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中

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

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

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

                对解析结果进行缓存
同一符号引用进行多次解析请求是很常见的,除invokedynamic指令以外,虚拟机实现可以对第一次解析结果进行缓存,来避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证的是在同一个实体中,如果一个引用符号之前已经被成功解析过,那么后续的引用解析请求就应当一直成功;同样的,如果 第一次解析失败,那么其他指令对这个符号的解析请求也应该收到相同的异常。inDy(invokedynamic)是 java 7 引入的一条新的虚拟机指令,这是自 1.0 以来第一次引入新的虚拟机指令。到了 java 8 这条指令才第一次在 java 应用,用在 lambda 表达式中。 indy 与其他 invoke 指令不同的是它允许由应用级的代码来决定方法解析。

      (3)初始化                 

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  ①声明类变量是指定初始值

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

 JVM初始化步骤

 1、假如这个类还没有被加载和连接,则程序先加载并连接该类

 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

 3、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

– 创建类的实例,也就是new的方式

– 访问某个类或接口的静态变量,或者对该静态变量赋值

– 调用类的静态方法

– 反射(如Class.forName(“com.shengsiyuan.Test”))

– 初始化某个类的子类,则其父类也会被初始化

– Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

 

四.   jvm的运行时数据区

                A 方法区

                     方法区只有一个,线程共享的内存区域【线程不安全】,生命周期与虚拟机一样长

             B  堆

                  线程共享的内存区域【线程不安全】,生命周期与虚拟机一样长

            C  栈

                  生命周期与线程一样长,每个方法被当前线程调用时就代表一个栈帧

                   

posted @ 2019-12-28 15:51  童话述说我的结局  阅读(197)  评论(0编辑  收藏  举报