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();
    }
}

当执行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

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

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
}

反编译之后会发现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

我们详细解析一下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类型的数据

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

再来看一下这些指令在栈帧区域中的操作
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 */
            }
        }
    }

在这里插入图片描述
这个本地方法他的底层是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);
        }
    }
}

我们看一下上边代码
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

我们演示一下-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);
        }

    }
}

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值