• JVM性能调优篇02-JVM内存模型深度剖析与优化


    JVM整体结构及内存模型

    整个JVM整体结构由类装载子系统字节码执行引擎运行时数据区三部分组成。我们常说的堆,栈是属于运行时数据区中的一部分。

    举例我们执行一段如下代码

    package com.tuling.jvm;
    
    public class Math {
    
        public static int initData = 666;
        public static User user = new User();
    
    
        public int compute(){ // 一个方法对应一块栈帧内存区域
            int a = 1;
            int b = 2;
            int c = (a + b) * 100;
            return c;
        }
    
        public static void main(String[] args) {
            Math math = new Math();
            math.compute();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    当执行main方法时,我们最终执行的是通过java指令运行Math.class字节码文件。那么这条指令最终执行的过程是,首先通过类装载子系统将字节码文件丢到运行时数据区(内存区域),最终通过我们的字节码执行引擎来执行内存区域里面的代码。如下图所示。

    在这里插入图片描述
    JVM调优其实就是围绕着运行时数据区进行调优。

    运行时数据区—栈(线程)内存区域

    运行时数据区里的堆、栈、方法区等等其实就是来放数据的。

    在Orcale官方网站上管栈内存区域叫Virtual Machine Stacks(虚拟机栈)。我个人更喜欢称他为线程栈
    为什么叫线程栈,我们以最上方代码举。
    当我们执行main方法时会有一个线程运行我们的方法。只要有一个线程执行我们的方法,那么Java虚拟机马上会在线程栈内的内存空间会给我们当前线程分配一块当前线程独立的内存空间,用来放我们线程执行过程中需要用到的局部变量(代码中的math变量,a变量, b变量, c变量)的内存空间。
    当有另一个线程执行别的方法时也同样Java虚拟机马上会在线程栈的内存空间开辟出该线程专属的线程区域,用来放线程内部的局部变量。
    不同的线程哪怕执行同一份代码,不同的线程都要有自己的内存空间放自己线程的局部变量。

    在这里插入图片描述

    接着往下讲,当我们线程运行上边代码中的main方法,马上会分配一块自己线程独有的线程栈,分配完独有的线程栈之后,当我们的线程开始运行方法时,他会在这块线程栈里给这个main方法分配一块自己的专属空间,用来放main方法自己的局部变量,就是把math局部变量放到自己的栈帧内存区域当中。
    然后当调用compute方法,java虚拟机马上会给compute方法分配一块自己的专属空间,用来放compute方法自己专属的局部变量,就是把a变量,b变量,c变量放到栈帧内存中。

    栈帧内存区域

    JVM内部会给每一个方法都会分配一块自己专属的内存空间,该内存空间就叫栈帧内存空间。就像我在代码中写的注释,一个方法对应一块栈帧内存空间。

    在这里插入图片描述
    接着讲栈帧内部区域,栈帧内部区域其实是比较复杂的,除了放我们的局部变量以外,还有操作数栈动态链接方法出口。当然还有一些其他的,但是不太重要。
    在这里插入图片描述
    讲解操作数栈和动态链接用字节码来分析,这两块内存区域是做什么的,通过底层的字节码文件比较好直观的理解这几块内存区域。

    反编译.class字节码文件

    我们看Math.class文件时,里头全是0000 0034 2f55等一些字节码数据,这些信息可读性特别差,我们使用javap -c Math.class命令对字节码文件进行反编译,生成一种更可读的字节码指令文件。

    查看反编译字节码文件的内容
    javap -c Math.class
    输入反编译字节码文件的内容到指定文件
    javap -c Math.class > Math.txt
    
    • 1
    • 2
    • 3
    • 4

    以下内容为反编译生成的文件

    Compiled from "Math.java"
    public class com.tuling.jvm.Math {
      public static int initData;
    
      public static com.tuling.jvm.User user;
    
      public com.tuling.jvm.Math();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public int compute();
        Code:
           0: iconst_1
           1: istore_1
           2: iconst_2
           3: istore_2
           4: iload_1
           5: iload_2
           6: iadd
           7: bipush        100
           9: imul
          10: istore_3
          11: iload_3
          12: ireturn
    
      public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class com/tuling/jvm/Math
           3: dup
           4: invokespecial #3                  // Method "<init>":()V
           7: astore_1
           8: aload_1
           9: invokevirtual #4                  // Method compute:()I
          12: pop
          13: return
    
      static {};
        Code:
           0: sipush        666
           3: putstatic     #5                  // Field initData:I
           6: new           #6                  // class com/tuling/jvm/User
           9: dup
          10: invokespecial #7                  // Method com/tuling/jvm/User."<init>":()V
          13: putstatic     #8                  // Field user:Lcom/tuling/jvm/User;
          16: return
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    反编译之后会发现Math类的常量,compute()方法,main方法。
    compute()方法里iconst_1, istore_1,iconst_2这些指令的意思可以看下边的JVM指令手册。
    比如iconst_1的意思就是const_1 将int类型常量1压入操作数栈
    istore_1的意思就是 将int类型值存入局部变量1
    这些指令查手册即可。

    JVM指令手册

    栈和局部变量操作
    将常量压入栈的指令
    aconst_null 将null对象引用压入栈
    iconst_m1 将int类型常量-1压入栈
    iconst_0 将int类型常量0压入栈
    iconst_1 将int类型常量1压入操作数栈
    iconst_2 将int类型常量2压入栈
    iconst_3 将int类型常量3压入栈
    iconst_4 将int类型常量4压入栈
    iconst_5 将int类型常量5压入栈
    lconst_0 将long类型常量0压入栈
    lconst_1 将long类型常量1压入栈
    fconst_0 将float类型常量0压入栈
    fconst_1 将float类型常量1压入栈
    dconst_0 将double类型常量0压入栈
    dconst_1 将double类型常量1压入栈
    bipush 将一个8位带符号整数压入栈
    sipush 将16位带符号整数压入栈
    ldc 把常量池中的项压入栈
    ldc_w 把常量池中的项压入栈(使用宽索引)
    ldc2_w 把常量池中long类型或者double类型的项压入栈(使用宽索引)
    从栈中的局部变量中装载值的指令
    iload 从局部变量中装载int类型值
    lload 从局部变量中装载long类型值
    fload 从局部变量中装载float类型值
    dload 从局部变量中装载double类型值
    aload 从局部变量中装载引用类型值(refernce)
    iload_0 从局部变量0中装载int类型值
    iload_1 从局部变量1中装载int类型值
    iload_2 从局部变量2中装载int类型值
    iload_3 从局部变量3中装载int类型值
    lload_0 从局部变量0中装载long类型值
    lload_1 从局部变量1中装载long类型值
    lload_2 从局部变量2中装载long类型值
    lload_3 从局部变量3中装载long类型值
    fload_0 从局部变量0中装载float类型值
    fload_1 从局部变量1中装载float类型值
    fload_2 从局部变量2中装载float类型值
    fload_3 从局部变量3中装载float类型值
    dload_0 从局部变量0中装载double类型值
    dload_1 从局部变量1中装载double类型值
    dload_2 从局部变量2中装载double类型值
    dload_3 从局部变量3中装载double类型值
    aload_0 从局部变量0中装载引用类型值
    aload_1 从局部变量1中装载引用类型值
    aload_2 从局部变量2中装载引用类型值
    aload_3 从局部变量3中装载引用类型值
    iaload 从数组中装载int类型值
    laload 从数组中装载long类型值
    faload 从数组中装载float类型值
    daload 从数组中装载double类型值
    aaload 从数组中装载引用类型值
    baload 从数组中装载byte类型或boolean类型值
    caload 从数组中装载char类型值
    saload 从数组中装载short类型值
    将栈中的值存入局部变量的指令
    istore 将int类型值存入局部变量
    lstore 将long类型值存入局部变量
    fstore 将float类型值存入局部变量
    dstore 将double类型值存入局部变量
    astore 将将引用类型或returnAddress类型值存入局部变量
    istore_0 将int类型值存入局部变量0
    istore_1 将int类型值存入局部变量1
    istore_2 将int类型值存入局部变量2
    istore_3 将int类型值存入局部变量3
    lstore_0 将long类型值存入局部变量0
    lstore_1 将long类型值存入局部变量1
    lstore_2 将long类型值存入局部变量2
    lstore_3 将long类型值存入局部变量3
    fstore_0 将float类型值存入局部变量0
    fstore_1 将float类型值存入局部变量1
    fstore_2 将float类型值存入局部变量2
    fstore_3 将float类型值存入局部变量3
    dstore_0 将double类型值存入局部变量0
    dstore_1 将double类型值存入局部变量1
    dstore_2 将double类型值存入局部变量2
    dstore_3 将double类型值存入局部变量3
    astore_0 将引用类型或returnAddress类型值存入局部变量0
    astore_1 将引用类型或returnAddress类型值存入局部变量1
    astore_2 将引用类型或returnAddress类型值存入局部变量2
    astore_3 将引用类型或returnAddress类型值存入局部变量3
    iastore 将int类型值存入数组中
    lastore 将long类型值存入数组中
    fastore 将float类型值存入数组中
    dastore 将double类型值存入数组中
    aastore 将引用类型值存入数组中
    bastore 将byte类型或者boolean类型值存入数组中
    castore 将char类型值存入数组中
    sastore 将short类型值存入数组中
    wide指令
    wide 使用附加字节扩展局部变量索引
    通用(无类型)栈操作
    nop 不做任何操作
    pop 弹出栈顶端一个字长的内容
    pop2 弹出栈顶端两个字长的内容
    dup 复制栈顶部一个字长内容
    dup_x1 复制栈顶部一个字长的内容,然后将复制内容及原来弹出的两个字长的内容压入
    栈
    dup_x2 复制栈顶部一个字长的内容,然后将复制内容及原来弹出的三个字长的内容压入
    栈
    dup2 复制栈顶部两个字长内容
    dup2_x1 复制栈顶部两个字长的内容,然后将复制内容及原来弹出的三个字长的内容压入
    栈
    dup2_x2 复制栈顶部两个字长的内容,然后将复制内容及原来弹出的四个字长的内容压入
    栈
    swap 交换栈顶部两个字长内容
    类型转换
    i2l 把int类型的数据转化为long类型
    i2f 把int类型的数据转化为float类型
    i2d 把int类型的数据转化为double类型
    l2i 把long类型的数据转化为int类型
    l2f 把long类型的数据转化为float类型
    l2d 把long类型的数据转化为double类型
    f2i 把float类型的数据转化为int类型
    f2l 把float类型的数据转化为long类型
    f2d 把float类型的数据转化为double类型
    d2i 把double类型的数据转化为int类型
    d2l 把double类型的数据转化为long类型
    d2f 把double类型的数据转化为float类型
    i2b 把int类型的数据转化为byte类型
    i2c 把int类型的数据转化为char类型
    i2s 把int类型的数据转化为short类型
    整数运算
    iadd 执行int类型的加法
    ladd 执行long类型的加法
    isub 执行int类型的减法
    lsub 执行long类型的减法
    imul 执行int类型的乘法
    lmul 执行long类型的乘法
    idiv 执行int类型的除法
    ldiv 执行long类型的除法
    irem 计算int类型除法的余数
    lrem 计算long类型除法的余数
    ineg 对一个int类型值进行取反操作
    lneg 对一个long类型值进行取反操作
    iinc 把一个常量值加到一个int类型的局部变量上
    逻辑运算
    移位操作
    ishl 执行int类型的向左移位操作
    lshl 执行long类型的向左移位操作
    ishr 执行int类型的向右移位操作
    lshr 执行long类型的向右移位操作
    iushr 执行int类型的向右逻辑移位操作
    lushr 执行long类型的向右逻辑移位操作
    按位布尔运算
    iand 对int类型值进行“逻辑与”操作
    land 对long类型值进行“逻辑与”操作
    ior 对int类型值进行“逻辑或”操作
    lor 对long类型值进行“逻辑或”操作
    ixor 对int类型值进行“逻辑异或”操作
    lxor 对long类型值进行“逻辑异或”操作
    浮点运算
    fadd 执行float类型的加法
    dadd 执行double类型的加法
    fsub 执行float类型的减法
    dsub 执行double类型的减法
    fmul 执行float类型的乘法
    dmul 执行double类型的乘法
    fdiv 执行float类型的除法
    ddiv 执行double类型的除法
    frem 计算float类型除法的余数
    drem 计算double类型除法的余数
    fneg 将一个float类型的数值取反
    dneg 将一个double类型的数值取反
    对象和数组
    对象操作指令
    new 创建一个新对象
    checkcast 确定对象为所给定的类型
    getfield 从对象中获取字段
    putfield 设置对象中字段的值
    getstatic 从类中获取静态字段
    putstatic 设置类中静态字段的值
    instanceof 判断对象是否为给定的类型
    数组操作指令
    newarray 分配数据成员类型为基本上数据类型的新数组
    anewarray 分配数据成员类型为引用类型的新数组
    arraylength 获取数组长度
    multianewarray 分配新的多维数组
    控制流
    条件分支指令
    ifeq 如果等于0,则跳转
    ifne 如果不等于0,则跳转
    iflt 如果小于0,则跳转
    ifge 如果大于等于0,则跳转
    ifgt 如果大于0,则跳转
    ifle 如果小于等于0,则跳转
    if_icmpcq 如果两个int值相等,则跳转
    if_icmpne 如果两个int类型值不相等,则跳转
    if_icmplt 如果一个int类型值小于另外一个int类型值,则跳转
    if_icmpge 如果一个int类型值大于或者等于另外一个int类型值,则跳转
    if_icmpgt 如果一个int类型值大于另外一个int类型值,则跳转
    if_icmple 如果一个int类型值小于或者等于另外一个int类型值,则跳转
    ifnull 如果等于null,则跳转
    ifnonnull 如果不等于null,则跳转
    if_acmpeq 如果两个对象引用相等,则跳转
    if_acmpnc 如果两个对象引用不相等,则跳转
    比较指令
    lcmp 比较long类型值
    fcmpl 比较float类型值(当遇到NaN时,返回-1)
    fcmpg 比较float类型值(当遇到NaN时,返回1)
    dcmpl 比较double类型值(当遇到NaN时,返回-1)
    dcmpg 比较double类型值(当遇到NaN时,返回1)
    无条件转移指令
    goto 无条件跳转
    goto_w 无条件跳转(宽索引)
    表跳转指令
    tableswitch 通过索引访问跳转表,并跳转
    lookupswitch 通过键值匹配访问跳转表,并执行跳转操作
    异常
    athrow 抛出异常或错误
    finally子句
    jsr 跳转到子例程
    jsr_w 跳转到子例程(宽索引)
    rct 从子例程返回
    方法调用与返回
    方法调用指令
    invokcvirtual 运行时按照对象的类来调用实例方法
    invokespecial 根据编译时类型来调用实例方法
    invokestatic 调用类(静态)方法
    invokcinterface 调用接口方法
    方法返回指令
    ireturn 从方法中返回int类型的数据
    lreturn 从方法中返回long类型的数据
    freturn 从方法中返回float类型的数据
    dreturn 从方法中返回double类型的数据
    areturn 从方法中返回引用类型的数据
    return 从方法中返回,返回值为void
    线程同步
    montiorenter 进入并获取对象监视器
    monitorexit 释放并退出对象监视器
    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_
    加:iadd,ladd,fadd,dadd
    减:isub,lsub,fsub,dsub
    乘: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(缩窄数值转换)
    创建类实便:new
    创建新数组:newarray,anewarray,multianwarray
    访问类的域和类实例域:getfield,putfield,getstatic,putstatic
    把数据装载到操作数栈:baload,caload,saload,iaload,laload,faload,daload,aaload
    从操作数栈存存储到数组:
    bastore,castore,sastore,iastore,lastore,fastore,dastore,aastore
    获取数组长度:arraylength
    检相类实例或数组属性:instanceof,checkcast
    操作数栈管理:pop,pop2,dup,dup2,dup_xl,dup2_xl,dup_x2,dup2_x2,swap
    有条件转移: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,fcmpl
    fcmpg,dcmpl,dcmpg
    复合条件转移:tableswitch,lookupswitch
    无条件转移:goto,goto_w,jsr,jsr_w,ret
    调度对象的实便方法:invokevirtual
    调用由接口实现的方法:invokeinterface
    调用需要特殊处理的实例方法:invokespecial
    调用命名类中的静态方法:invokestatic
    方法返回:ireturn,lreturn,freturn,dreturn,areturn,return
    异常:athrow
    finally关键字的实现使用:jsr,jsr_w,ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269

    我们详细解析一下compute方法的指令

     0: iconst_1   将int类型常量1压入操作数栈
     1: istore_1   将int类型值存入局部变量1
     2: iconst_2   将int类型常量2压入栈
     3: istore_2   将int类型值存入局部变量2
     4: iload_1    从局部变量1中装载int类型值
     5: iload_2    从局部变量2中装载int类型值
     6: iadd       执行int类型的加法
     7: bipush        100   将一个8位带符号整数压入栈 相当于把常量100压入操作数栈
     9: imul       执行int类型的乘法
     10: istore_3   将int类型值存入局部变量3
     11: iload_3    从局部变量3中装载int类型值
     12: ireturn    从方法中返回int类型的数据
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    栈帧区域之局部变量表和操作数栈

    再来看一下这些指令在栈帧区域中的操作
    iconst_1 将int类型常量1压入操作数栈
    在这里插入图片描述

    istore_1 将int类型值存入局部变量1
    istore_1 指向的的是局部变量a,但是我们看代码的话发现局部变量是a,b,c 三个局部变量,那为什么指令不是istore_a,istore_b呢?jvm的顺序中1,2,3指定的是局部变量的下标。但是下标是0开始,并且我们看JVM指令手册会发现还有一个istore_0,那么局部变量a为什么是istore_1,而不是istore_0呢?是因为istore_0指向的是this,调用这个compute方法的对象。

    在这里插入图片描述

    后边的也是一样的
    在这里插入图片描述
    这些是栈帧区域中的局部变量表和操作数栈的概念。讲动态链接和方法出口之后讲一下程序计数器

    程序计数器

    程序技术器也是每个线程独有的,和栈一样,每个线程独有的一块内存空间。
    他用来存放我们程序正在运行的或者说马上要运行的某一行的代码的位置,或者说是行号。
    比如我们来看上边反编译的compute方法,发现所有指令前边都有对应的数字,可以把这个数字当做是代码的位置的标识,我们可以理解为程序计数器就是存放这些数字的。程序计数器的值是变动的,每执行完一行代码,我们的字节码执行引擎马上就会去修改程序计数器的值

    那么为什么需要程序技计数器呢,比如我们要执行第四行代码,突然被另外一个优先级更高的线程我们的CPU的时间片抢占过去,那么这时当前的线程需要挂起,那么等再恢复该线程时,我们需要告诉CPU我们该继续执行哪一行代码。

    在这里插入图片描述

    栈帧区域之动态链接

    java源文件被编译成class字节码文件的时候,会把所有变量和方法的引用作为符号引用保存到class文件的常量池中。看上一篇博客从JDK源码级别彻底剖析JVM类加载机制。上一篇博客中我们解析了Math.class文件,获取了字节码指令。字节码指令文件中Constant pool部分就是常量池。

    看main方法的加#的数字,#加数字就是符号引用,引用Constant pool常量池。每一个栈帧中都存在一个动态链接,存的就是指向常量池的引用。所以动态链接也叫指向运行时常量池的引用。
    在这里插入图片描述

    动态链接比较复杂后续的文章中会继续讲解。

    栈帧区域之方法出口

    以我们的代码举例compute方法执行完回到main方法时,方法出口记录了当前方法执行完回到哪个方法,回到那个方法的哪一行代码。就是根据我方法出口里存的信息,接下来返回到main方法里继续执行哪一行代码。
    在这里插入图片描述

    我们再回归一下局部变量表,上边内容是以compute方法来讲解的局部变量表,但是上边代码中的main方法的局部变量表是稍微有些不同的。
    我们看一下main方法。
    在这里插入图片描述
    一般来说new出来的对象是放在堆内存空间里的。但是我们有一个局部变量math放到main方法栈帧区域中的局部变量表里的。其实就是局部变量表里存的是math对象的内存地址。
    在这里插入图片描述
    画到这个图其实栈和堆的关系已经很明确了,我们栈内部是有很多很多的局部变量,如果这些局部变量都是对象类型,那么这些值肯定是在堆上面。说明栈的局部变量表里放的都是堆的内存地址。

    在这里插入图片描述

    方法区

    方法区里主要有常量,静态变量,类元信息(就是类信息,比如代码,还有方法名称就是常量池的一些信息)组成。
    也就是说方法区存储的主要和类相关的信息。

    比如我们上边代码中的initData常量是放在方法区。
    user对象也是放在方法区,但是存放的不是new出来的User对象,创建的user对象一定是在堆里的,那么方法区里存的是user对象的引用地址。
    还有一些类信息,常量池,方法名等。这些使用javap命令解析Math.class,反解析出当前类对应的字节码指令。上一篇文章中已经讲解。
    在这里插入图片描述

    方法区比较复杂,后边的文章中会继续详解方法区。这里只要知道方法区是主要存类的信息即可。

    本地方法栈

    讲解本地方法栈之前一定要理解本地方法。

    什么是本地方法?就是native修饰的方法就是本地方法。
    比如当你运行Thread对象的start方法,start方法的内部就会调用一个本地方法

    
    public static void main(String[] args) {
            Math math = new Math();
            math.compute();
    
            new Thread().start();
    }
    
    public synchronized void start() {
            /**
             * This method is not invoked for the main method thread or "system"
             * group threads created/set up by the VM. Any new functionality added
             * to this method in the future may have to also be added to the VM.
             *
             * A zero status value corresponds to state "NEW".
             */
            if (threadStatus != 0)
                throw new IllegalThreadStateException();
    
            /* Notify the group that this thread is about to be started
             * so that it can be added to the group's list of threads
             * and the group's unstarted count can be decremented. */
            group.add(this);
    
            boolean started = false;
            try {
                start0();
                started = true;
            } finally {
                try {
                    if (!started) {
                        group.threadStartFailed(this);
                    }
                } catch (Throwable ignore) {
                    /* do nothing. If start0 threw a Throwable then
                      it will be passed up the call stack */
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    在这里插入图片描述
    这个本地方法他的底层是C或者C++语言实现的。

    总结来说本地方法栈就是不管你什么语言写的,方法运行的过程中调用的一切跨语言的方法存放到本地方法栈中。本地方法栈也是线程独有的内存区域,和程序计数器和栈是一样的,都是线程独有的。

    在这里插入图片描述
    看本篇博客的第一张图片,当时已经把栈,本地方法栈,程序计数器的背景颜色统一,意味着这三块内存区域是线程独有的内存区域,每个线程都会有自己的这三块内存区域,不会与其他线程共享。

    堆内部区域无非就是年轻代和老年代组成。年轻代里有Eden区和Surivivor区,他内部还有一定的配比。老年代占整个堆的3分之2,年轻代占整个堆的3分之1。当然他们之间的配比是可以自己去调整的。
    年轻代理边的Eden区和两个Survivor区默认的配比是8比1比1。
    在这里插入图片描述

    我们new出来的对象存放的区域他有很多种情况,绝大部分情况都是先放在Eden区。
    比如我们一个web系统,一直运行,程序一直运行那么系统会不断产生新的对象,不断产生的新的对象终究会把Eden区放满,Eden区放满之后我们的java虚拟机就会做gc,这时的gc叫做minor gc
    其实minor gc就是我们的字节码执行引擎开启一个垃圾回收线程, 做垃圾收集。
    在这里插入图片描述
    minor gc就会把Eden区里的一些无用的对象进行回收,
    他会从我们整个的方法区,栈,本地方法栈里面找很多GC Roots 。
    GC Roots简单讲讲就是,将GC Roots对象为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
    GC Roots根节点:线程栈的局部变量、方法区里的静态变量、本地方法栈的变量等等,这些都可以作为GC Roots的根节点。

    在这里插入图片描述
    那么GC的过程就是从GC Roots根节点对象出发,找他这些对象所有引用使用的对象。凡是从GC Roots根节点出发引用的所有对象,我们都会认为这些对象为非垃圾对象。我们就会把这些非垃圾对象直接复制到Survivor区,Eden区剩下的对象可能就是垃圾对象。直接干掉这些对象。
    在这里插入图片描述
    如果说一个对象经历过一次gc之后,如果还存活着他的分代年龄会+1,分代年龄是存在对象头信息里
    这是第一次触发minor gc,然后程序继续运行,运行一阵时间之后Eden区又被放满了。
    这时他minor gc不光会回收eden区域,还会回收s0区域。
    在这里插入图片描述

    回收这两块的计算方式一模一样,再把那些存活的对象直接复制到s1区域。然后这两块区域中剩余的对象直接干掉。然后在s1区域中存活的对象分代年龄又会+1.
    在这里插入图片描述
    程序继续运行,如果Eden空间又放满了,又会触发minor gc,这时候会回收Eden区域和s1区域。把存活的对象复制到s0区域中,并分代年龄+1。
    当他的分代年龄增加到15的时候,这个对象会被挪到老年代,不同的垃圾收集器的值不太一样,一般都是15。15这个值是可以自己去设置的,这个后续的文章中会继续讲解。
    在这里插入图片描述
    对象在堆中大体流转的过程就是大概就是这些。
    那么思考一下什么样的对象容易会放到老年代?
    静态变量,静态变量引用的对象,对象池,缓存对象,spring容器里的对象容易放到老年代中。

    我们用jvm工具jvisualvm看一下整个对象流转的过程。

    jvisualvm

    我们执行以下代码查看对象在堆中的流转过程

    public class HeapTest {
    
        byte[] a = new byte[1024 * 100]; // 100KB
    
        public static void main(String[] args) throws InterruptedException {
            ArrayList<HeapTest> heapTests = new ArrayList<>();
            while (true) {
                heapTests.add(new HeapTest());
                Thread.sleep(10);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们看一下上边代码
    heapTests 变量是栈中的局部变量表里的参数,也就是我们上边说的GC Roots对象,循环创建的HeapTest对象加入到heapTests 数组中,这些循环创建的对象被GC Roots对象所引用,当Eden区放满的时候就会出方minor gc。不断创建HeapTest对象,不断放满Eden区,当对象的分代年龄达到15之后,会放到老年代,老年代放满之后无法回收,最终会出现内存溢出。

    运行程序之后,运行jvisualvm工具,这是JDK提供的工具,这是启动之后的界面,这个工具会自动识别我们本地的所有JVM进程。

    在这里插入图片描述

    然后我们启动测试代码之前给jvisualvm 安装一个插件Visual GC。
    启动测试代码,查看jvisualvm的Visual GC控制台,这时候就会看到gc的时候,Eden区,Survivor区,老年代区的内存区域的变化。

    在这里插入图片描述
    当老年代放满了之后,他会触发我们的full gc, full gc他大体的gc过程和我们刚才说的gc流程类似。但是他回收的是整个堆(Eden区,Survivor区,老年代)和方法区他都会回收。
    在这里插入图片描述

    当老年代放满之后,gc无法回收老年代的对象,然后还要往老年代放对象时,这时会出现内存溢出。

    STW(Stop The World)

    Stop The World,简称STW, 指的是GC过程中,会产生引用程序的卡顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

    这种STW现象是对用户体验和网站的整个性能是有一定影响的,其实JVM调优,主要调优的目的就是减少GC的次数,不管是minor gc还是full gc尽量减少gc次数,最应该减少的是full gc的次数,因为full gc回收的是整个堆内存区域和方法区,gc的时间会比较长。
    总结来说jvm调优就是调优减少full gc次数,或者减少full gc的时间。当然如果minor gc的次数比较多也需要减少minor gc的次数,由于minor gc的回收的区域叫少,执行gc的时间非常短,暂时不用太关注minor gc,主要需要优化full gc。

    JVM内存参数设置

    对于我们运行时数据区来说,我们设置内存空间大小主要设置堆,方法区,栈以下三块区域。
    在这里插入图片描述
    -Xms: 堆空间的初始内存空间大小
    -Xmx: 堆空间的最大内存空间大小
    -XX:MetaspaceSize:设置方法区最大内存空间,默认是-1,即不限制大小,相当于服务器剩余多少内存大小就是多大。
    -XX:MaxMetaspaceSize:设置方法区的初始内存空间,默认是21MB,方法区达到该大小就会触发full gc,同时收集器会对该值进行调整,如果释放了大量的空间,就适当降低该值;如果释放了很少空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下,适当提高该值。

    由于调整方法区的大小需要full gc, 这是非常耗时的操作,如果在应用启动的时候发送大量full gc, 通常都是方法区发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置的初始值要大,对于8G物理内存的机器来说,一般我会将这两个值设置为256M。
    -Xss: 栈线程的内存空间大小,每个线程的内存空间大小是一样的。就是上边讲的栈帧区域大小。

    Spring Boot程序的JVM参数设置格式

    java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar test.jar
    
    • 1

    我们演示一下-Xss参数,以下代码为测试代码讲解,分别演示不设置-Xss和-xss参数的两种现象。

    package com.tuling.jvm;
    
    public class StackOverflowTest {
        // -Xss128k, -Xss默认1M
        static int count = 0;
    
        static void redo(){
            count++;
            redo();
        }
    
        public static void main(String[] args) {
            try {
                redo();
            } catch (Throwable e) {
                e.printStackTrace();
                System.out.println(count);
            }
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    不设置-Xss参数时count打印的值为23728
    设置-Xss128K时count打印的值为1102

    说明-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多。

  • 相关阅读:
    clickhouse、Doris、Kylin对比
    Jmeter 二次开发详解
    详解C++三大特性之继承
    2021年9月电子学会图形化二级编程题解析含答案:画正多边形
    Go 语言
    【Quarto】Markdown导出PPT
    销量上不去?跨境电商出现这5种迹象,你需要Starday了!
    Linux服务器上运行Puppeteer的Docker部署指南
    pytorch的使用:使用神经网络进行气温预测
    “淘宝拍立淘图片搜索接口:轻松找到同款商品!
  • 原文地址:https://blog.csdn.net/pjsdsg/article/details/125415811