由浅入深的JVM学习(一)

首先我们看一张图,来大概知道一下JVM的结构:

 

 

 有上图可以看到,JVM(java虚拟机)由3部分组成,类加载器子系统,JVM运行时数据区,执行引擎。

那么各个子系统有什么作用呢?我们来看下面的这个简单的代码:

public class App {

    public int add(int a,int b){
        int c = (a+b)*100;
        return c;
    }

    public static void main(String[] args) {
        App app = new App();
        int add = app.add(1, 2);
        System.out.println(add);
    }
}

  以上这段代码,调用add()方法,把1,2传入,获得结构是300。那么它是怎么被jvm加载的呢?

我们在idea写完代码之后,进行编译,运行。在这过程中,我们其实是调用了javac,和java的指令去编译和运行。

当我们调用javac时,我们会得到一个App.class的文件。我们称之为,字节码文件。

那我们看看App.class这个字节码文件长什么样子:

 

编译之后,我们可以得到一个如上图一样的十六进制字节码文件。

那么类加载器就去加载这个字节码文件,类加载器的加载机制是什么样的呢?

 

 

 

 1.加载:

主要是将.class文件中的二进制字节流加载到jvm中。

2.验证:

验证第一步加载阶段获得的二进制字节流.class文件是否符合jvm规范。

3.准备:

准备阶段是给static变量分配内存(方法区中),并设置初始值

4.解析:

虚拟机将常量池的符号引用替换成直接引用。(什么是符号引用,什么是直接引用,这里做一下说明)

解析阶段就更抽象了,稍微说一下,因为不太重要,有两个概念,符号引用,直接引用。说的通俗一点但是不太准确,比如在类A中调用了new B();大家想一想,我们编译完成.class文件后其实这种对应关系还是存在的,只是以字节码指令的形式存在,比如 "invokespecial #2" 大家可以猜到#2其实就是我们的类B了,那么在执行这一行代码的时候,JVM咋知道#2对应的指令在哪,这就是一个静态的家伙,假如类B已经加载到方法区了,地址为(#f00123),所以这个时候就要把这个#2转成这个地址(#f00123),这样JVM在执行到这时不就知道B类在哪了,就去调用了。(说的这么通俗,我都怀疑人生了).其他的,像方法的符号引用,常量的符号引用,其实都是一个意思,大家要明白,所谓的方法,常量,类,都是高级语言(Java)层面的概念,在.class文件中,它才不管你是啥,都是以指令的形式存在,所以要把那种引用关系(谁调用谁,谁引用谁)都转换为地址指令的形式。好了。说的够通俗了。大家凑合理解吧。这块其实不太重要,对于大部分coder来说,所以我就通俗的讲了讲。

5.初始化:

根据程序中的赋值语句,主动为类变量赋值。

这一块其实就是调用类的构造方法,注意是类的构造方法,不是实例构造函数,实例构造函数就是我们通常写的构造方法,类的构造方法是自动生成的,生成规则:
static变量的赋值操作+static代码块
按照出现的先后顺序来组装。
注意:1 static变量的内存分配和初始化是在准备阶段.2 一个类可以是很多个线程同时并发执行,JVM会加锁保证单一性,所以不要在static代码块中搞一些耗时操作。避免线程阻塞。

 

类加载器的种类:

1.启动类加载器(Bootstrap ClassLoader):

  最顶层的类加载器,负责加载JAVA_HOME\lib目录中的,或者通过-Xbootclasspath 参数指定加载路径,且被虚拟机认可(按文件名识别,如rt.jar)的类

2.扩展类加载器(Extension ClassLoader):

  负责加载JAVA_HOME\lib\ext 目录中的,或者通过java.ext.dirs系统变量指定路径中的类库

3.应用类加载器(Application ClassLoader):

  也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。

  如果没有自定义类加载器的话,这个类加载器就是默认的类加载器。

类加载器的加载步骤:

(1)AppClassLoader查找资源时,不是首先查看自己的地盘是否有这个字节码文件,而是委托给父类加载器ExtClassLoader。

   当然这里有一个假定,就是在AppClassLoader的缓存中,没有找到目标class。比方说第一次加载这个目标类,缓存中肯定没有这个类。

(2)ExtClassLoader查找资源时,也不是首先查看自己的地盘是否有这个字节码文件,而是直接委托给父类加载器BootstrapClassLoader。

(3)如果父类加载器BootstrapClassLoader在它的地盘上找到,并加载成功,则直接返回。反过来如果在JVM的核心地盘——%sun.boot.class.path% 中没有找到。

   则回到ExtClassLoader。

(4)如果ExtClassLoader在它的地盘找到,并加载成功,则直接返回。反过来如果在ExtClassLoader的地盘——%java.ext.dirs% 中没有找到。则回到AppClassLoader自己的地盘。

(5)于是乎,逗了一大圈,终于回到了自己的地盘。还附带了两条件,就是前面的老大们没有搞定,否则也没有AppClassLoader啥事情了。

(6)AppClassLoader在自己的地盘找到,这个地盘就是%java.class.path%路径下查找。找到就返回。

(7)最终,如果没有找到,就抛出异常了。

这个过程,就是一个典型的双亲委托机制的一次执行流程。

双亲委派模型:

  双亲委派模型原理:

    如果一个类加载器收到了类加载的请求,他首先不会自己尝试去加载这个类,而是委托父类加载器去加载,每一层次的类加载器都是如此,

    因此所有的类的加载请求都会被传送到最顶层的类加载器(BootstrapClassLoader)。只有当父类加载器无法加载成功,并反馈失败的时候,

    子类加载器才会试着去加载。

问:那么为什么要使用双亲委派这个机制呢?有什么作用呢?

因为这样可以避免重复加载,父类加载器已经加载了该类的时候,子类加载器就没有必要再去加载了,双亲委派机制也就构成了类的沙箱机制。

当我们自己创建一个java.lang包,创建一个String类的时候,这个String类就不起作用,因为父类加载器已经加载了java.lang.String。

这样也避免了某些坏人想要改JVM里面的类的想法。

 

 

接下来就解析一下类的字节码文件:

这么一团东西到底代表着什么意思呢?别急,它是有模板的,具体那几个字节代表什么意思,都是有模板依据的:

 

 

 

 

 

 

 

 

 

 

 

 

 1.根据上图的我们知道第一个值是magic(魔数)占用了4个字节(u4),我们通过字节码文件可以看出前4个字节是ca fe ba be。这个东西就类似于用来标记我们

这个字节码文件的有效性。为什么用ca fe ba be来标识,据说是高斯林(java之父)喜欢喝咖啡

2.第二个是小版本号,占u2,那就是00 00 

3.第三个是大版本号,占u2,那就是00 31

 

 

 可以看出jdk为5。

4.常量池的数量,占u2,00 2a。转换成10进制就是42。那说明接下来的cp_info有42个字节。而cp_info也有自己的格式,此处就不深入了。有兴趣的同学可以自行查找学习。

 

 

那App.class字节码文件通过类加载器加载进内存之后,就会进入到JVM运行时数据区。

 

 

 运行时数据区分了很多块。有虚拟机栈,本地方法栈,程序计数器。堆,方法区。

从上图可以看出,虚拟机栈,本地方法栈,程序计数器 属于线程私有数据。所谓线程私有数据,就是只一个线程有一份数据。

堆,方法区 属于线程共享数据,即这些区域的数据是线程公用的。

本地方法栈(线程私有):登记native方法,在Execution Engine执行时加载本地方法库

 

程序计数器(线程私有):就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),   

由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

 

Java(虚拟)栈(线程私有): Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧

(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致

 

方法区(线程共享):类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。

简单说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,

虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

 

JDK版本差异

元数据区:元数据区取代了永久代(jdk1.8以前),本质和永久代类似,都是对JVM规范中方法区的实现,区别在于元数据区并不在虚拟机中,而是使用本地物理内存,永久代在虚拟机中,永久代逻辑结构上属于堆,但是物理上不属于堆,堆大小=新生代+老年代。元数据区也有可能发生OutOfMemory异常。

Jdk1.6及之前: 有永久代, 常量池在方法区

Jdk1.7:       有永久代,但已经逐步“去永久代”,常量池在堆

Jdk1.8及之后: 无永久代,常量池在元空间

元数据区的动态扩展,默认–XX:MetaspaceSize值为21MB的高水位线。一旦触及则Full GC将被触发并卸载没有用的类(类对应的类加载器不再存活),然后高水位线将会重置。新的高水位线的值取决于GC后释放的元空间。如果释放的空间少,这个高水位线则上升。如果释放空间过多,则高水位线下降。

为什么jdk1.8用元数据区取代了永久代?

官方解释:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代

 

JVM执行原理:

JVM指令集详解:
变量到操作数栈:
iload,iload_,lload,lload_,fload,fload_,dload,dload_,aload,aload_
操作数栈到变量:
istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstor_,astore,astore
常数到操作数栈
bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_
把数据装载到操作数栈
baload,caload,saload,iaload,laload,faload,daload,aaload
从操作数栈存存储到数组:
bastore, castore,sastore,iastore,lastore,fastore,dastore,aastore
操作数栈管理
pop,pop2,dup,dup2,dup_xl,dup2_xl,dup_x2,dup2_x2,swap
运算与转换:
加:iadd,ladd,fadd,dadd
• 减:is ,ls ,fs ,ds
• 乘:imul,lmul,fmul,dmul
• 除:idiv,ldiv,fdiv,ddiv
• 余数:irem,lrem,frem,drem
• 取负:ineg,lneg,fneg,dneg
• 移位:ishl,lshr,iushr,lshl,lshr,lushr
• 按位或:ior,lor
按位与:iand,land
• 按位异或:ixor,lxor
类型转换:
i2l,i2f,i2d,l2f,l2d,f2d(放宽数值转换)
i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l,d2f(缩窄数值转换)
有条件转移
ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,if_icmpeq,if_icmpene,
if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne,lcmp,fc mpl,fcmpg,dcmpl,dcmpg
复合条件转移:
tableswitch,lookupswitch
无条件转移:
goto,goto_w,jsr,jsr_w,ret

  我们可以通过javap的命令去获取执行的程序:

我们可以通过javap -v App.class > d:/app.log 把执行过程输出到d盘,内容如下:

Classfile /F:/spring/JVM01/target/classes/com/takey/App.class
  Last modified 2020-12-1; size 690 bytes
  MD5 checksum c7150fcd871c2543e218d3783c62cb52
  Compiled from "App.java"
public class com.takey.App
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#28         // java/lang/Object."<init>":()V
   #2 = Class              #29            // com/takey/App
   #3 = Methodref          #2.#28         // com/takey/App."<init>":()V
   #4 = Methodref          #2.#30         // com/takey/App.add:(II)I
   #5 = Fieldref           #31.#32        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Methodref          #33.#34        // java/io/PrintStream.println:(I)V
   #7 = Class              #35            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/takey/App;
  #15 = Utf8               add
  #16 = Utf8               (II)I
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               c
  #21 = Utf8               main
  #22 = Utf8               ([Ljava/lang/String;)V
  #23 = Utf8               args
  #24 = Utf8               [Ljava/lang/String;
  #25 = Utf8               app
  #26 = Utf8               SourceFile
  #27 = Utf8               App.java
  #28 = NameAndType        #8:#9          // "<init>":()V
  #29 = Utf8               com/takey/App
  #30 = NameAndType        #15:#16        // add:(II)I
  #31 = Class              #36            // java/lang/System
  #32 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #33 = Class              #39            // java/io/PrintStream
  #34 = NameAndType        #40:#41        // println:(I)V
  #35 = Utf8               java/lang/Object
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
  #41 = Utf8               (I)V
{
  public com.takey.App();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/takey/App;

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: bipush        100
         5: imul
         6: istore_3
         7: iload_3
         8: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 7
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/takey/App;
            0       9     1     a   I
            0       9     2     b   I
            7       2     3     c   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #2                  // class com/takey/App
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_1
        10: iconst_2
        11: invokevirtual #4                  // Method add:(II)I
        14: istore_2
        15: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_2
        19: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        22: return
      LineNumberTable:
        line 13: 0
        line 14: 8
        line 15: 15
        line 16: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1   app   Lcom/takey/App;
           15       8     2   add   I
}
SourceFile: "App.java"

  

 

 

 

字节码文件加载进内存之后,当去执行main方法的时候,会产生一个main方法的主线程,也叫main线程。

该线程里面包含着,程序计数器(用于记录程序执行的执行到那个位置了),本地方法栈(跟虚拟机栈类似,用于执行native方法),

虚拟机栈(用于存放栈帧,一个方法一个栈帧,该App类里面有2个方法,一个是main方法,一个是add方法,按照栈的FILO特性,main先执行,再执行add,所以main在栈底)

栈帧里面包含着:局部变量表(存放该方法的局部变量,例如add方法中的,a,b,c),操作数栈(进行该方法里面的一些逻辑计算,例如add方法里面的a+b),方法出口(执行完add方法之后return回到main方法,继续执行main方法),动态链接 (指向常量池中该方法的引用,例如main方法中new App(),开始是符号引用(new #2),它要去常量池中去查找对应的直接引用(#2 = Class))

 

posted @ 2020-12-01 16:51  Takey  阅读(150)  评论(0编辑  收藏  举报