zoukankan      html  css  js  c++  java
  • 由浅入深的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_HOMElib目录中的,或者通过-Xbootclasspath 参数指定加载路径,且被虚拟机认可(按文件名识别,如rt.jar)的类

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

      负责加载JAVA_HOMElibext 目录中的,或者通过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))

  • 相关阅读:
    必备单词
    Vim
    Linux基础
    python链表操作详解
    冒泡和快速排序
    学员练车选课系统
    面试题
    获取resp:heads:content-disposition的filename
    记录springBoot启动报错(无脑型)
    springBoot+Vue搭建新项目(1)
  • 原文地址:https://www.cnblogs.com/takeyblogs/p/14069264.html
Copyright © 2011-2022 走看看