JDK-JVM-hotspot

news2025/1/27 13:02:06

JVM

  • JDK
    • JDK内部体系结构:
    • JVM 与 跨平台
    • JVM在程序运行过程中的运行细节,内存分配 和 流转模型。
      • JVM结构体系
        • 1. 虚拟机栈
        • 2. 线程栈
          • 2.1. 栈帧
          • 2.2. 数据结构栈 与 线程栈 的关系:
          • 2.3.栈帧的内部结构:
          • 2.4 方法中的数据 在栈帧中的流转过程:
        • 3. 程序计数器
          • 3.1 字节码执行引擎与程序计数器和方法区的关系
        • 4. 方法区:
        • 5. 本地方法栈
        • 6. 堆:
          • 6.1 堆的组成
          • 6.2 GC
          • 6.3 GC过程
            • 6.3.1 GC Root
            • 6.3.2 复制算法
            • 6.3.3 可达性分析算法
            • 6.3.4 分代年龄
            • 6.3.5 对象的组成 和 对象头
            • 6.3.6 jvisualvm工具 与 GC过程验证
        • 7. JVM调优:
          • 7.1 用户线程
          • 7.2 STW机制及其设计原因
        • 8. 实践案例:
          • 8.1 故事起因:
          • 6.2 亿级流量网站 评估 与 架构
            • 6.2.1 日活用户
            • 6.2.2 付费转化率
            • 6.2.3 百万订单的系统压力推算
            • 6.2.4 JVM堆内存参数设置
            • 6.2.5 阿里面试:亿级流量 JVM调优 不发生Full GC
        • 相关代码:
  • .参考文献

JDK

JDK内部体系结构:

在这里插入图片描述
整个jdk内部体系结构,大体可分为两部分:命令工具JRE

  1. jdk自带了一些命令工具。比如java、javac、javap 和 jar命令等

javac:用于将java源代码编译成字节码文件,也就是 通过Java的编译器(Windows下的javac.exe),对源文件进行错误排查后,形成后缀名为 .class的字节码 文件。

字节码文件:是一种与任何具体的"机器环境" 和 “操作系统环境” 无关的中间代码,同时也是一个二进制文件。

java:用于让JVM执行字节码文件。

  1. JRE(Java运行时环境):包括 类库JVM
    (1) 支撑Java程序运行的核心类库。比如I/O,网络(NetWorking),并发(Concurrency),lang 和 util包等。
    (2) Java虚拟机JVM(Java Virtual Machine)

JVM 与 跨平台

Java最大的特性跨平台,就是由JVM帮我们实现的。

  • JVM 从软件层面 屏蔽了 不同操作系统 在底层 硬件 与 指令 上的区别。也就是,JVM兼容了不同的操作系统,因为我们在下载jdk的时候,下载了不同的基于这个操作系统 特定的jdk版本,它里面就有 针对 特定平台的JVM。
  • 实际上,计算机底层只认识0101的二进制机器码,但是由于不同操作系统 不同硬件平台上 指令集 的区别,同一份代码的功能实现,在不同操作系统底层 对应的二进制机器码 是不一样的,而JVM可以生成 基于这个操作系统 特定的 机器可执行的 二进制机器码。比如,一个加法功能的实现,在Windows上生成的是010101,而在 Linux上生成的 可能是110001…。
    在这里插入图片描述
  • 如果我选择了 基于Windows版本的jdk,那它对应给我们运行代码的JVM,就是基于这个Windows操作系统,做了一些特定的实现。然后,我们写了一份java源代码,比如Helloword.java。然后通过javac指令编译成Helloword.class字节码文件,再通过java指令(java Helloword.class),将字节码文件丢到JVM里去运行。需要注意的是,我们只写了一份代码,生成了一份字节码(没有平台限制),最终由 JVM 生成基于这个操作系统 特定的 可执行的二进制机器码。
  • 因为 Java是先编译成 .class字节码 文件,然后再利用JVM进行解释执行的,是一种在编译基础上进行解释运行的语言。所以,Java即可以说成编译型,也可以说成解释型语言。
  • 简而言之,跨平台就是,代码一次编写,到处运行。开发人员 只需写一份代码,最终可以放到各大平台去运行,比如Windows,Linux,Unix 和 Mac等。

JVM在程序运行过程中的运行细节,内存分配 和 流转模型。

JVM结构体系

  • 一个完整的JVM结构体系 由3部分组成:类装载子系统运行时数据区字节码执行引擎。他们的作用和联系,也就是字节码文件在JVM中的 执行过程 与 运行原理。

  • 当我们用java指令 去运行一个字节码文件时(比如java Math.class),JVM就开始运行了。字节码文件会首先通过JVM的第一块区域 类装载子系统,它会把我们的字节码文件装载(load)到JVM的第二块区域 运行时数据区(也叫内存模型/内存区域),最终,由我们JVM的第三块区域 字节码执行引擎,来执行我们内存里面的一行一行的代码。
    在这里插入图片描述

  • 在整个JVM的组成中,最为重要的部分,其实就是内存区域,JVM调优实际上调的就是这个区域的相关部分,而其中的堆内存是最为主要的调优部分。

  • 内存区域oracle官方文档的JVM规范 里面,有一些详细的定义和介绍,

  • 内存区域在规范里面的官方名称,叫做运行时数据区(Run-Time Data Areas),它的内部又包含5个部分:

  • 第一部分是程序计数器(The pc Register)

  • 第二部分是Java虚拟机栈(Java Virtual Machine Stacks)

  • 第三部分是堆(heap)

  • 第four部分是方法区(Method Area),方法区中又包括常量池(Run-Time Constant Pool)

  • 第五部分是本地方法栈(Native Method Stacks)

  • 这些部分都属于JVM的内存区域,是用来存储数据的,但它们的作用和存储的内容各不相同。
    在这里插入图片描述

1. 虚拟机栈
  • 虚拟机栈 这块区域主要是用来 存放 我们的线程运行过程中,它的那些局部变量,比如在我们java代码中,(包括main方法在内)每一个方法,都有属于自己的局部变量。这些变量就是存放在我们的"栈"内存区域。
2. 线程栈
  • 因为 Java虚拟机中的 每个线程 都有一个私有的虚拟机栈,与线程同一时间被创建出来。也就是说,在我们的java代码中,只要有一个线程开始运行代码了,JVM就会马上给这个线程,在整个一大块虚拟机栈的内存区域中,分配一小块属于这个线程 自己独立的内存空间,用来存放 我们这个线程(自己运行过程中)内部 的局部变量。所以对于线程而言,每个线程 自己私有的虚拟机栈 也可以更形象的称之为线程栈
2.1. 栈帧
  • 线程栈 的内部其实还有很复杂的结构,比如说每个线程栈的内部,都是由一系列的方法栈帧组成。那其实在 线程栈的内部,只要这个线程 开始运行了一个方法,那么,马上会给这个方法,在这个线程栈上面,又分配出一块 自己独立的 内存空间,用来存放 方法内部的局部变量,因为 局部变量的作用域 只在当前方法,所以,方法运行结束,这个方法中的局部变量 也肯定会被销毁。如果这个方法内部,又调用了另一个方法,那么,被调用的方法 也会在当前 线程栈 的内存区域中,再分配一块 内存空间,去 存放自己方法内部的局部变量。
  • 那么,线程中的每个方法,在 线程栈的 内存区域中 都会有属于自己方法 所使用的 独立的一块内存空间,用来存放自己方法内部的局部变量。对于方法而言,一个方法 在线程栈中,所对应分配出的 属于自己方法的 一小块独立的 内存空间,就被称之为 线程栈 中的一个 栈帧。也就是说,在我们整个JVM内部,一个方法开始运行,就会有对应的一块栈帧内存空间,分配给这个方法,存放方法自己内部的局部变量。
  • 概括来说,虚拟机栈,线程栈和栈帧之间的关系:虚拟机栈是属于JVM中一整块大的内存区域;线程栈属于线程,是从虚拟机栈中 为当前线程分配出的一小块内存空间;栈帧属于方法,是从方法对应的线程栈中 为当前方法分配出的 更小的一块内存空间。
    在这里插入图片描述
2.2. 数据结构栈 与 线程栈 的关系:
  • 线程栈 实际上使用的就是 数据结构中的栈存储结构。那么为什么要用数据结构中的栈,来实现线程栈。
  • 因为 栈的特点是FILO(First In Last Out),先进后出(先进去的元素反而后出来)。与 我们程序代码中的方法 嵌套调用 执行的先后顺序,非常吻合。
  • 当我们程序开始执行一个方法的时候,这个方法(暂且称之为主方法) 会在线程栈中分配出对应的一块栈帧内存空间,也就是主方法入栈;
  • 如果主方法在执行过程中,又调用了另一个方法(暂且称之为局部方法),那么JVM也会为这个局部方法 在线程栈中 对应分配出一块独立的栈帧内存空间,也就是局部方法入栈;
  • 当局部方法调用结束,那局部方法内部的局部变量 全部都会消失,也就是局部方法对应的栈帧内存空间会被释放掉,也就是局部方法出栈,回到主方法。
  • 然后,主方法继续执行未完成的代码,当主方法再执行结束,主方法对应的栈帧内存空间也会被销毁,也就是主方法出栈。
  • 也就是,先执行的方法,反而会后结束;后调用的方法,反而会先执行完。这与数据结构栈的先进后出原则,非常吻合。
2.3.栈帧的内部结构:
  • 栈帧是用来存放方法的局部变量的内存区域,但栈帧的作用远不止于此。栈帧的内部结构也比较复杂,但最最主要的,有4块区域:

(1) 局部变量表

  • 首先栈帧内部有一个局部变量表,用来存放方法的局部变量。
  • 如果 局部变量的值 是一个对象,或者,new了一个对象,而new出来的对象,都是存放在我们的内存中的。那么 赋值为对象的局部变量,和堆里面 对应的对象,它们之间是引用关系。
  • 比如方法中有一行代码,Math math = new Math();,局部变量表中的局部变量,实际上就是一块内存空间,这块空间被JVM标识成 名为math的名称,用以知道 这块空间 是给我们math这个局部变量去使用,而这块空间中存放的是 局部变量的值对象 在堆里面的内存地址,从而 JVM 根据这个内存地址,就可以找到 堆里面 对应的对象。而地址,实际上 就是一个引用,一个指针。
  • 那么,栈里面可能有很多很多线程,每一个线程都有一些自己的方法栈帧,而每个栈帧有会很多很多自己的局部变量,这些局部变量,如果它们的值,恰好又是对象类型,那么这些对象实际上,是分配在堆中的,意味着这些局部变量里面,存放的都是堆中对应对象的地址引用,栈 和 堆 之间的关系就出来了。
  • 栈帧中的另外3部分是,操作数栈,动态链接 和 方法出口。

(2) 操作数栈

  • 我们程序在运行过程中,程序代码赋值等号右侧的数值,就是操作数。那操作数栈,就是给我们操作数,做操作运算(加减乘除运算)的时候,临时存放的一块 中转的内存空间。

(3) 动态链接

  • 动态链接 会把 符号引用,转变为 直接引用。比如,我们多态在程序运行过程中,找到对应的方法,其实就算一种动态链接。实际上,方法是加载到方法中的,方法区也是内存空间,也有内存地址。
  • 当我们的程序 在动态执行到 某个方法时,要调用这个方法,需要知道 这个方法 有哪些代码,这时 就会去解析 这个方法的符号引用,去找 这个符号 对应的 具体的JVM指令码,这些指令码 就存放在 我们的方法区 内存区域中。
  • 动态链接,可以简单的理解为 存放 方法入口的内存地址。或者说,动态链接 会记录 这个方法 指向 在方法区中 具体代码的 内存地址(就是 方法的符号引用)。从而就可以 根据 动态链接中 记录的方法符号引用(方法具体代码的内存地址) 找到 对应的直接引用(在内存里面存放具体代码的位置)。

(4) 方法出口

  • 当一个方法(主方法),动态执行到 另一个方法时,程序计数器 会返回 那个程序计数器 执行到当前 主方法 哪一行代码的 位置信息,就存放在 方法出口 里,实际上 不止这一点点,但可以 大概这么来认为。从而 根据 方法出口 里放的那些数据,可以知道 被调用方法 调用结束后,应该回到 主方法 的执行哪一行代码 继续往下去执行。

  • 那栈帧的内部这些区域,与java的字节码文件,有着密不可分的联系。一个 .class字节码 文件,实际上是一个二进制编码的文件,它包含了很多底层的程序 JVM运行的一些细节逻辑,那这些底层的逻辑实际上,就是栈帧中各组成部分之间的运行细节。如果我们通过记事本打开一个字节码文件,展现给我们的是一个 以每4位为一组的 有规则的16进制的编码集合,那么每2组编码字符串,在oracle官方网址上面的字节码手册中,都有对应的含义可以查询。但是以这样的方式 分析字节码文件,体验还是比较差的。oracle官方还提供了 另外一种更友好的字节码文件形式。可以通过jdk自带的javap命令对二进制的字节码文件,进行反汇编,得到一份可阅读的更加友好的字节码文件形式。但它们的本质上都是同一个东西,都是我们java代码更底层的一些JVM指令码。

  • 我们可以利用IDEA开发工具,在 target/classes 包下找到自己想分析的 .class字节码 文件,然后鼠标右键选择"Open in Terminal",打开一个命令窗口,输入javap回车,即可看到javap指令的相关介绍和使用。其中有一个"-c"的参数,对代码进行反汇编,可以帮我们把二进制的字节码文件,反汇编成JVM的指令码文件,或者叫JVM的汇编指令。比如我们对某个Xxx.class字节码文件进行反汇编,并把它输出打印到一个记事本文件中,如下指令:javap -c Xxx.class > Xxx.txt。那么在 .class字节码 的同级目录下,就会生成一个记事本形式的对应的JVM指令码文件。

2.4 方法中的数据 在栈帧中的流转过程:
  • 比如我们分析的java代码中有一个compute方法,方法中的数据 在栈帧中的流转过程:
public int compute() {
	int a = 1;
	int b = 2;
	int c = (a + b) * 10;
	return c;
}
  • 那么这段代码对应的JVM指令码(JVM执行的代码)如下:
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		10
	   9: imul
	  10: istore_3
	  11: iload_3
	  12: ireturn

首先我们可以发现,越底层的代码,它对应的行数越多;越高级的语言,它的代码量越少。那么对于JVM指令码中的每一行到底是什么以上,在oracle官方网址上面有一些手册,比如JVM指令手册(再复杂的java代码,要分析它底层的一些运作机制,以及它底层的一个内存流转 及分配模型,只要根据JVM指令手册,都可分析出来),可以去查询。那么这段JVM指令码,根据JVM指令手册,每一条指令对应的解释如下:

iconst_1 将int类型常量1压入 操作数栈
istore_1 将int类型值存入局部变量1
iconst_2 将int类型常量2压入栈
istore_2 将int类型值存入局部变量2
iload_1 从局部变量1中装载int类型值
iload_2 从局部变量2中装载int类型值
iadd 执行int类型的加法
bipush 将一个8位带符号整数压入栈
imul 执行int类型的乘法
istore_3 将int类型值存入局部变量3
iload_3 从局部变量3中装载int类型值
ireturn 从方法中返回int类型的数据

  • 那么,我们逐条来解释一下compute方法的JVM指令码。

(1) compute方法的第一行java代码int a = 1;它对应的JVM指令码及含义:

  • iconst_1 将int类型常量1压入操作数栈:这里的常量1,指的是操作数栈中的 第一个存储位置,而不是java代码中的1这个数值。更为精确的表达:把java代码中1这个数值,压入我们在操作数栈中 分配出的第一个常量内存空间。
  • istore_1 将int类型值存入局部变量1:这里的局部变量1,数字1指的不是第一个局部变量,它是我们局部变量表的一个索引。这个局部变量表的索引不是从1开始的,是从0开始的。局部变量表,它还有0的一个位置istore_0,放的是this这个变量,也就是,调用这个方法的对象。在JVM指令手册中,还有 istore_2 和 istore_3。我们可以把局部变量表理解为,类似一个数组,这些数字,就是数组的下标和索引。实际上,我们的一个成员方法对应的栈帧的局部变量表中,局部变量0都是定死了的,都是this,也就是调用这个方法的对象。
  • 那么局部变量1,也就是 JVM在局部变量表中 分配了一块内存空间 叫局部变量1,并把这块内存空间标识为 我们java代码中的局部变量a。实际上当istore_1这条指令执行的时候,JVM底层会干这几件事;JVM首先 会在 局部变量表,给我们的局部变量1分配出一块内存空间,作为局部变量a 的存储位置;然后,JVM会把 刚刚压入 操作栈中的 常量数值1 出栈,放到局部变量表中a对应的这一块内存空间里。这时,局部变量表中,局部变量a这块内存空间,实际上就已经有一个值了,就是我们的int类型的常量数值1。
  • 当iconst_1和istore_1这两行代码执行完后,局部变量表中标识为a内存空间,就已经被赋值为1了。也就是变量a的值就变成1了。

(2) compute方法的第二行java代码int b = 2;它对应的JVM指令码及含义:

  • iconst_2 将int类型常量2压入栈:这里的常量2,同样的,是指操作数栈中的第二个常量的存储位置,也就是,把java代码中2这个数值,压入我们在操作数栈中分配出的第二个常量内存空间。
  • istore_2 将int类型值存入局部变量2:也就是,在局部变量表中,局部变量2的存储位置,标识为我们java代码中的局部变量b,并将刚刚压入操作数栈中的数值2,放到局部变量表b对应的这一块内存空间里。
  • 与iconst_1 和 istore_1一样,当iconst_2 和 istore_2执行完之后,同样的,局部变量表中标识为b内存空间,就存入2这个数值。变量b的值就变成2了。

(3) compute方法的第三行java代码int c = (a + b) * 10;,它对应的JVM指令码及含义:

  • iload_1 从局部变量1中装载int类型值:局部变量1就是变量a,那把a的值给它装载出来,那就是把1的值装载出来,压入我们的操作数栈中。
  • iload_2 从局部变量2中装载int类型值:局部变量2就是变量b,那把b的值给它装载出来,那就是把2的值装载出来,压入我们的操作数栈中。
  • iadd 执行int类型的加法:要执行加法,它会先把操作数栈里的元素弹出来,做操作运算。也就是2+1等于3,运算做完后,它又会把运算结果 重新压回操作数栈。
  • bipush 将一个8位带符号整数压入栈:那么bipush 10,就是将后面这个数字10,压入操作数栈里面。
  • imul 执行int类型的乘法:和执行iadd方法是的类似,从操作数栈顶弹出两个最新的元素,做操作运算。也就是3*10等于30,运算做完后,把结果重新压回操作数栈。
  • istore_3 将int类型值存入局部变量3:局部变量3就是compute方法中的局部变量c。首先JVM会在局部变量表中,给我们局部变量c分配一块内存空间,然后会把操作数栈中的30 出栈,放到局部变量表中局部变量对应的那块内存空间里。

(4) compute方法的第four行java代码return c;,它对应的JVM指令码及含义:

  • iload_3 从局部变量3中装载int类型值:把刚刚存入局部变量3中内存位置的局部变量c,把c的值给它装载出来。也就是把30这个值装载出来,压入到我们的操作数栈中。
  • ireturn 从方法中返回int类型的数据:把刚刚装载出来的变量c,也就是到压入我们操作数栈中的int类型的数值30,把它返回给我们的主方法(调用者方法)里面去。
3. 程序计数器
  • 程序计数器,每一个线程都会有一个自己的程序计数器,线程私有。也就是,当你一个线程在开始运行的时候,Java虚拟机除了给它分配栈内存空间之外,还会给它分配一块自己独立的程序计数器内存空间,那程序计数器的作用,就是用来存放当前这个线程,正在运行到哪一行代码的执行位置,或者说内存地址,或者说一个指示标志位。
  • 在我们java代码中,从一个方法所对应的JVM指令码可以看到,每一条指令码的前面,都有一个数字,从0开始有序增加,那么我们就把这个数字理解为标识着,我们这一行代码,在我们方法中的 一个大概的标识位置,或者说一个行号。而程序计数器,实际上存放的就是,程序运行当前方法中,马上要运行的那一行代码,所对应的一个行号位置的值。
  • 那么JVM的开发人员,为什么要设计这么一个程序计数器。因为java是多线程运行的,如果我当前线程 正准备执行某个方法的某行代码,突然它的CPU时间片 被其他的优先级更高的线程,给它抢占过去了,那是不是意味着,我们当前线程可能要挂起。直到其他线程运行完毕之后,CPU才恢复之前挂起的线程去执行。那它恢复这个线程执行,它怎么知道要继续去执行 这个线程中的 哪个方法的 哪一行代码,就是根据程序计数器里面,存放的 当前这个线程 正在运行到 哪一行代码的内存地址,去恢复这个线程当时运行的时候的 那个状态,继续往下去执行。这就是程序计数器设计的一个初衷。
3.1 字节码执行引擎与程序计数器和方法区的关系
  • 那程序计数器存放的当前线程 运行到哪一行的值是如何变化的,实际上,是通过我们的字节码执行引擎去动态修改的。那字节码执行引擎,为什么能修改程序计数器的值,因为我们把一个字节码文件,加载到我们JVM内存区域中,实际上,最主要的是加载到方法区中。那这个方法区中的字节码代码,其实就是由我们的字节码执行引擎去执行的。字节码执行引擎去执行的代码,当然知道你这个代码现在运行到哪一行代码,下一次要运行哪一行代码。所以修改我们程序计数器的决策,当然是我们的字节码执行引擎。
4. 方法区:
  • Java的字节码文件 实际就是加载到方法区 中的,方法区 也是由很多部分组成,比如在oracle官网JVM规范中提到的常量池。简单来说,方法区中存放的数据,有如下几部分:常量,静态变量 和 类元信息(类信息)。比如,在我们的java代码的一个类中,
  • 如果 有一个常量,比如 public static final int initData = 666;,那这个常量就是存放在 方法区 的。
  • 如果 还有一个静态变量,而恰好 这个静态变量的值,又是一个对象,比如 public static User user = new User,这个对象,肯定也是存放在堆中的。那么 方法区中 静态变量中user 和 堆中对应的User对象 的关系,同栈 和 堆 之间的关系是一样的。方法区中的静态变量 实际上就是一块内存空间,这块空间被JVM标识成 名为user的名称,用以知道 这块空间 是给我们user这个静态变量去使用,而 这块空间中存放的 是静态变量的值对象 在堆里面的内存地址,从而 JVM 根据这个内存地址,就可以找到 堆里面 对应的对象。那么 方法区 和 堆 之间的关系,就出来了。
  • 在我们的java类中 可能会有很多的静态变量,如果 这些对象的值,恰好又是对象类型的,而对象的值,我们一般是分配在堆上面的,那么 这些 静态变量 里面存放的 就是 堆中对应对象 的内存地址,也就是指针。
  • 元空间的概念,方法区在jdk1.8之前,对 元空间 叫做永久代 或 持久代,在jdk1.8之后,改了下名字,叫元空间。实际上,都是方法区的具体实现。这个元空间在jdk1.8及之后,是非堆内存,用的直接内存,也就是我们操作系统的内存。或者说,方法区是JVM的一个规范,JVM规范中定义了有这么一个方法区,但底层实际上是由不同的虚拟机厂商来实现的。而这里的元空间就由是oracle厂商给出的具体实现。
  • 《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于 HotSpotJVM 而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
  • Metaspace(元空间)和 PermGen(永久代)都是对 JVM规范中方法区的一种落地实现只是 Oracle 从 JDK7 就开始就逐步的将永久代移除,慢慢的用元空间代替。直到JDK8 的发布才宣告 PermGen(永久代)的彻底移除。
  • jdk7开始已经对字符串常量池等从永久代移除,所以永久代的移除过程跨越了jdk7和jdk8两个大版本。是渐进式的移除。
5. 本地方法栈
  • 本地方法栈,在我们的java中,实际上用的不多了。本地方法栈 是 属于本地方法的。
  • 本地方法,是被native所声明的方法,在我们的java中有很多很多,比如,new Thread.start();,Thread的start()方法源码内部,它就有一个本地方法。当我们调用Thread的start()方法的时候,它最终会调用start0();这行代码,而 start0()方法,就是用native 修饰的一个本地方法接口,public native void start0();,当我们java代码调用到 start0 这行代码的时候,它的底层的代码实现,不是用java实现的,是C 或 C++ 语言实现的。
  • 那为什么 会有这么一个本地方法呢,这里面有一定的历史,因为我们java语言是95年真正的开始面世,对外发布的。其实95年之前,就有一些java语言的前身了。在当年,也就是95年之前的很多年,那个时候实际是C语言,或者说汇编语言的天下,也就是当时,一个公司99%的系统,可能要么用C语言,要么就是用汇编语言去写的,或者说其他的语言。但是要知道的是,Java语言95年一面世,就一鸣惊人,成为了非常牛逼的一门语言,除了跨平台,还有它非常牛B的面向对象特性。
  • 因为 C 或 C++语言,里面有一个非常麻烦的东西,就是很多很多的内存,我们需要自己分配,自己回收,也就是自己管理内存,要是稍微不小心,就会导致内存泄漏,但是对于Java语言来说,JVM已经都帮我们把这些事情给做了,所以我们根本就不用操心,对于我们Java开发人员来说,大大的降低了程序开发者的难度。所以,95年之后,Java语言一面世,就从此,它的发展趋势,一发不可收拾。早就已经变成我们整个的,所有语言里面的霸主地位,现在互联网公司 基本上后端,都是Java,一系列的全是Java,包括开源的世界里面,都是Java。那Java的地位(2020)几乎是不可撼动的,包括未来的一二十年,依然是Java语言的天下。
  • 那为什么会有这个本地方法,95年之后,很多公司可能意识到Java语言开发这么方便啊!那我们就用Java语言开发呗!那在当年一个新系统,你用Java语言开发,而公司这么多老系统,都是用C 或 C++ 语言开发的,那么,这些系统之间 就免不了,可能会有一些接口交互。在当年Java语言 和 C语言,做跨语言交互的时候,就是通过这种native本地方法。
  • 也就是Java代码在调用到native这样的代码的时候,它会通过我们的字节码执行引擎,去到我们的操作系统的底层的库函数,比如说,在Windows操作系统下面,它会去C 或 C++语言的库函数,也就是xxx.dll文件。这个东西,就和我们Java语言里面的jar包一样,里面有很多很多公用代码的一些实现,会去到这样的dll库函数文件里面,找start0()这行代码的实现,那这就是本地方法。
  • 现在,我们早就不再用这种本地方法,做跨语言交互了,现在我们跨语言交互手段多的是,什么app接口,远程调用,各种微服务框架等等。有了本地方法的概念,那么,本地方法栈,就是指,不管用什么语言写的本地方法,只要线程有在运行到本地方法,JVM也会给 这个线程 分配一块独立专属的内存空间,用来存放本地方法,在运行过程中需要用到的数据信息。
    在这里插入图片描述
6. 堆:
  • 堆 是由 老年代 和 年轻代 两大部分组成。其中又有很多的细节,比如OOM,GC,调优等。oracle官网的JVM规范中,对堆的介绍:
  • The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
6.1 堆的组成
  • 堆 是由 年轻代 和 老年代组成。年轻代里面 分为 伊甸园(Eden) 和 Survivor区;Survivor区又分为S0 和 S1区。我们new出来一个对象,一般是放到伊甸园区的(伊甸园:亚当和夏娃造人的地方,很形象,new出来的对象,就是新生的东西)。

  • 当然,堆的主要作用,虽然是存放我们程序中的对象,但也并非所有对象,都存放在堆中,也有一些比较特殊对象,也可能会存放在 方法栈 中。
    在这里插入图片描述

  • 那我现在假设有一个7x24小时不间断运行的web应用系统,那它会不断的产生对象往伊甸园区放。假设这些对象 在伊甸园区都放满了,放不下去了;而程序还在运行,还在继续生产对象,往伊甸园区放,Java虚拟机底层就会执行 GC(garbage collector)
    在这里插入图片描述

6.2 GC
  • GC分为 minor gc(或 young gc)full gc。这里执行的gc就是young gc。那 GC实际上,是我们 字节码执行引擎,它在后台 开启的一个垃圾收集线程,专门收集我们堆区域的这些垃圾对象,那minor gc,其实 主要是收集我们的年轻代。
6.3 GC过程
  • GC底层 它收集垃圾的一个过程,或者说,GC怎么知道 堆中的哪些对象是垃圾,哪些对象该回收,以及垃圾收集大体的一个过程。
6.3.1 GC Root
  • 要想理解GC执行过程,这里有一个很重要的概念,叫GC Root。GC Roots根节点,是像一些 线程栈的本地变量、静态变量、本地方法栈的变量等,都可以叫做 GC Root。那整个GC的执行过程,实际上,它就会从那些GC Roots根节点的 那些变量 出发,找所有能引用到的对象。比如,线程栈中的本地变量的值 是一个对象,或 方法区中的静态变量的值 是一个对象,那么它们在堆中 就都有 指向的对象,所以,这些变量都可以作为GC Root。

  • 所以整个GC的过程,实际上就是从 方法区,从线程栈上面,找所有的本地变量,还有静态变量,只要它们的直接引用是对象类型的,就把它们全部找出来,然后从GC Roots这些变量出发,逐个的去找它们 引用的对象,比如说,有一个线程栈中 有一个成员变量math,Math math = new Math();,这个Math对象中的成员变量,又引用了一个对象,引用的对象 又引用了 另一个对象,依此找下去,直到找到最后那个对象,它的成员对象 不再引用任何其他对象。

6.3.2 复制算法
  • 凡是在从GC Root这个根节点出发,这个对象引用链条上的所有堆中的对象,它都会把它标记为非垃圾对象。一旦这些对象被标记为非垃圾对象,那么 这些对象就会通过 复制算法,被复制到Survivor中的 第一块区域S0,而剩下的 伊甸园里面的所有对象,都会被当做垃圾 直接清理掉。当然GC的过程很复杂,根据设置不同的垃圾收集器,也会有所区别,但大体的流程基本相似。
    在这里插入图片描述

  • 那假设第一次minor gc做完了,程序还在不间断的运行,继续生产对象,伊甸园区又被放满了,放满了之后,又会触发minor gc,但这回触发的minor gc,不仅回收伊甸园区,同时还会回收那块非空的Suirvivor区域S0,把这两块区域 同时给回收,回收结束后,GC又会把伊甸园 和 非空的Survivor区域S0,这两块区域 剩余的存活对象,都复制到Survivor的另一块空的区域S1,然后清空 另外一块Survivor区域S0 和 伊甸园区,这两块区域中的 没有被GC Root 直接或间接引用的 垃圾对象。

6.3.3 可达性分析算法
  • 这里 在从GC Roots出发,去辨别一个对象,是否为垃圾对象的过程,用到了一个算法,叫可达性分析算法。也就是 将 "GC Roots"对象 作为起点,从这些节点 开始向下搜索 引用对象,找到的对象 都被标记为 非垃圾对象,其余未标记的对象 都是垃圾对象。
    在这里插入图片描述
6.3.4 分代年龄
  • 在GC的过程中,还有一个重要的概念,叫分代年龄,也就是一个对象,每经历一次minor gc之后,如果 这个对象没有被清理掉,那么它的 分代年龄 就会加1。而这个 分代年龄,是存储在我们 对象头中的内容。
6.3.5 对象的组成 和 对象头
  • 一个类,或者说 一个对象,不仅仅是由 代码中表面的 那些成员变量 组成。一个完整的对象,除了成员变量 组成的 对象实例数据 之外,还有 对齐填充 和 对象头。对象头 是对象中 非常重要的组成部分,它结构比较复杂,但大体可以分为3部分,Mark Word,Klass Pointer 和 数组长度。而 分代年龄,就是存放在Mark Word当中,Mark Word在对象头中 是非常重要的一块信息,主要存放 对象运行时数据,其中除了存放 GC分代年龄,还有存放 对象的一些锁,线程持有锁;线程加锁的一些状态,锁状态标志;哈希值等。
    在这里插入图片描述

  • 那随着我们程序的继续运行,伊甸园区 可能又会被放满,这时又会触发minor gc,那minor gc触发之后,除了回收 伊甸园区 之外,还会回收 非空的那块Survivor区,然后 又会把存活的对象复制到 另一块空的Survivor区中,这样一个对象 如果一直存活,每进行一次minor gc,复制一次,这个对象的 分代年龄 就会加1,而当一个对象 分代年龄,如果加到了15,也就是经历了15次minor gc,还没有被干掉,那这样的对象,会被复制到老年代去。

  • 那老年代放满了之后,又会发生什么,OOM内存溢出,但它不会马上OOM,而是先 触发一次full gc,那么full gc 和 minor gc一样,也是由我们字节码执行引擎,它在后台开启的一个垃圾回收线程,但是full gc,它是回收 我们整个堆内存中的 所有垃圾,那如果 老年代中的对象 依然被gc root引用着,full gc 实际上是,回收不到 垃圾对象的,也就是 老年代中 没有垃圾对象,那老年代 回收不到垃圾,又继续有 新的对象 被挪到老年代,最终JVM会发生OOM,程序报错OutOfMemory。

  • 那么,可以知道 我一个web应用系统,最终 哪些类型的对象 可能会被挪到老年代,比如有,静态变量引用的对象,缓存,数据库连接池对象,对象池,Spring容器里面的bean对象。

6.3.6 jvisualvm工具 与 GC过程验证
  • 这就是一个对象,怎么从伊甸园区,然后通过很多次gc,最终挪到老年代 的完整过程。这个过程是可以通过一段代码去验证的。
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);
		}
	}
}
  • 首先,我在main方法中初始化了一个ArrayList,然后 又加了一个 while(true)死循环,往我们这个ArrayList中 来放新new出来的HeapTest对象。这个代码 最终执行 肯定会内存溢出,那 这里面为什么会内存溢出,就是因为 在while(true)死循环里 生成出来的对象,都被ArrayList引用着,放在这个List集合里面,也就是List内部肯定有一些指针 引用着这些对象。而这个ArrayList,又是被heapTests,这个main方法的局部变量引用着,heapTests这个变量,就是gc root根节点。
  • 我们这些被gc root引用的 对象,最开始会放到伊甸园区,伊甸园区放满之后,会触发minor gc,那么,gc会从gc root根节点出发,找到它的 所有引用对象,而在 while(true) 死循环中 又不断new出 新的对象,都会被gc root引用着,所以 gc不会对 这些对象 进行回收,这样的对象 最终都会被挪到 老年代。
  • 那现在 我们把这个代码运行起来,证明一下 这个gc垃圾回收的完整流程。当代码运行起来后,就会执行到while(true)死循环里,它会执行一会,还没有那么早的内存溢出。然后,我们通过cmd打开Windows命令窗口,再通过命令jvisualvm,打开一个jdk安装好后,自带的jvm调优诊断工具。这个工具启动之后,它会自动在本机 找所有的jvm进程。
  • 比如说,我们正在运行的HeapTest程序 运行的进程,找到后,双击进去看一下里面有什么东西,进去之后,会有我们当前进程的JVM参数,系统参数,包括这个进程使用的CPU情况,类的加载情况,堆的使用情况,元空间的使用情况,还有一些线程的使用情况,等等。那这些东西,其实就可以帮我们去调优诊断 整个JVM的运行情况。
  • 那我们这里主要看的是 Visual GC这个视图:
    在这里插入图片描述
  • 这个图像还有一点点不对,这个图像可能还不是特别清晰。可能能执行的比较慢,可以它执行的快点,或者,我们这边可能设置了一个参数,打开Run/Debug Configurations,在VM options: -XX:-UseAdaptiveSizePolicy,把这个VM options输入框中的参数去掉,然后重新运行一下代码。
  • 标准的没有任何参数 执行这个代码的时候,它是这样的一个效果,重新打开jvisualvm,它的Visual GC视图:
    在这里插入图片描述
  • 左侧是我们的伊甸园(Eden)区,两块Survivor区S0 和 S1,以及 老年代的内存增长过程。我们新new出来的对象,会往伊甸园区放,所以伊甸园区,它会慢慢的增长,增长到一定程度,突然没有了,但是两块Survivor区域中一块,会突然增长了一块,两块Survivor区域S0和S1,一会S0有对象被存放,一会S1有对象被存放,但是老年代old区域,是一直往上增加的。
  • 然后 右侧的视图是 每个区域,详细的一个使用的 曲线图,这个曲线图实际上就是我们 当前程序 正在运行的 内存分配模型 的流转。一开始Eden区的曲线图 就是慢慢增长,增长到放满了,触发一次GC,而我们当前程序的对象 都是被gc root引用着,是非垃圾对象,回收不了,就会挪到我们Survivor区域,所以Survivor区域一开始没有,然后 突然增长了一大块区域,当然是从伊甸园区域挪过来的,随之 伊甸园就被清空了。
  • 那下一次 又会往伊甸园区放,伊甸园区被放满,又会触发一次gc,而这回触发的gc,不仅回收 伊甸园区域,还会回收 上一次被占用的Survivor区域,然后把 回收到的非垃圾对象 挪到另一块 上次没有被占用的Survivor区域,上次被占用的Survivor区域和伊甸园区 就被清空了。
    在这里插入图片描述
  • 那如果Suvivor区域放不下,另一块Suvivor区域 和 伊甸园区 复制过来的 非垃圾对象时,就会把多余的 非垃圾对象 挪到老年代,所以 老年代有时也会 同时增长一块区域,当然 老年代的增长相对比较平稳,但是突然会增长,这个增长 除了Survivor区域 放不下之外,可能还会 积累了一大批 分代年龄达到15的对象,这批对象也会被挪到老年代,同时Survivor区域的曲线图表现为,突然增长过程中,又马上减少,出一个峰形状态,就是分代年龄达到15的对象被移到老年代了。这个程序执行过程中,对象流转的模型 就是visual GC所展示的曲线图。
  • 那随着程序的不断运行,老年代的非垃圾对象不断增长,总有一天会被放满,当老年代区域被放满时,会触发full gc,full gc和minor gc一样,也是由字节码执行引擎开启的后台线程 做垃圾收集这件事,但是full gc,它实际上是回收我们整个堆里面的所有垃圾,但是,在我们当前HeapTest程序,即便挪到老年代,这些老年代中的对象依然被gc root引用着,full gc其实是回收不了什么垃圾对象的,也就是我们这个HeapTest当前程序没有什么垃圾。那回收不了垃圾,又继续往老年代复制 新的对象 进去,最终jvm就会发生OOM,报错OutOfMemory异常。
    在这里插入图片描述
7. JVM调优:
  • JVM调优的目的,主要 就是 减少full gc的次数 和 执行时间,因为只要JVM执行垃圾收集gc,就会发生STW(Stop The Word),停止整个世界,它是在我们JVM 只要开始执行gc了( gc实际是由后台垃圾收集线程去执行的),JVM就会停掉所有用户线程。
7.1 用户线程
  • 比如说一个电商网站,用户正在点击了一个加购物车按钮,点击了一个下单按钮,在点击了这些按钮之后,后端都有一些程序的线程要执行,而凡是由用户操作发起,得到的这些线程的执行,那么,这些线程就叫做用户线程。
  • 那如果把这些用户线程停掉,对用户来说 就是,比如 用户刚点了一个加入购物车按钮,突然网站卡顿了一下,隔一会又好了。STW实际上就是停掉用户的操作,对用户的感知就是突然卡了。也就是后台的老年代满了,发生了full gc,如果这个full gc执行的时间比较长,那么用户在前端点击按钮,就可能会突然卡一下,底层就是做了STW,把用户线程给停掉了。
7.2 STW机制及其设计原因
  • 那么为什么JVM的开发人员要设计这么一个STW机制,难道就不能去掉这个机制么?也就是full gc的时候,不暂停用户线程,用户线程与我们垃圾线程一起去执行,这样程序性能会不会提高一点。
  • 那我们用反证法来推演一下,假设JVM在执行full gc的时候,不进行STW,也就是说用户线程不暂停,那gc就会不断的去从gc root寻找引用的对象,当一个线程执行结束后,那这个线程的gc root变量也会被销毁,内存空间被释放,这样的话,gc之前通过gc root在堆中 找到的那些 被标记为 非垃圾的对象,就动态变成了 垃圾对象,而我们堆内部可能有 成千上百万个对象,gc执行过程中,刚刚被标记为 非垃圾的对象,又马上变成了 垃圾对象,此时gc还没做完垃圾回收,难道还要从头再开始标记一次么。
  • 如果不从头再进行gc,那么gc实际上 回收到的对象 就是 垃圾对象 和 非垃圾对象 的混合体,就乱套了。从而 会导致一系列更为的复制系统问题,比如分代年龄标记混乱,对内存空间 需要更大的存储要求,等等。也就是 如果不做STW,就会导致堆中的对象状态 时刻发生改变,这时,gc就根本不知道如何去标记和回收,所以JVM的设计人员索性 就在执行gc的时候,让关键的地方进行STW,让JVM专心的 一次性把所有非垃圾对象 都标记出来。
  • 那这样设计后,gc的执行时间 还比较短,系统性能 还更高一点。所以 对用户的感知而言,虽然有一点STW,但执行时间很短,大多数的用户是感知不到的。
8. 实践案例:
8.1 故事起因:
  • 小猪在一家头部电商的一线互联网公司,担任架构师。当时带了一个小兄弟,小兄弟 做了一个系统。这个系统上线之后,平时没什么太大的问题,但是一旦到了,稍微有一点大促的时候,有一点点流量,系统并发量升高了的时候,就会收到用户的一些反馈,说怎么网站这么卡。同时系统后台的 开发人员 其实也收到了一些报警,线上系统频繁地full gc,因为 一线互联网公司的后台,后端系统都有大规模的监控系统,比方说,系统频繁gc了,系统压力比较大了,都能收到报警的提醒。
  • 小猪当时带的那小兄弟,做的那系统 就有这问题,就是刚上线,平时系统运行,也没什么问题,挺正常的,一旦到了大促的时候,哇,各种报警,各种gc,频繁full gc的报警。那 full gc,一般正常情况下,几个小时,几天,甚至几周才执行一次。但小兄弟做的系统,一旦到的大促,full gc 就几分钟 就会做一次,甚至一两分钟 就会做一次。实际上,full gc 肯定不能太频繁,如果太频繁,SWT的时间太长了,对整个网站的性能来说,会有非常大的影响,对于我们整个用户的感知是非常差的,他会感到你这个网站怎么这么卡。
  • 那小兄弟当时做的系统,现在就以 电商网站 来还原一下,到底什么问题导致的。用电商网站 来还原 当时的场景,到底为什么,系统稍微有点压力,就频繁会发生full gc,以及它的调优方法。当然,当然这个与拼多多,淘宝,京东,那些在春晚,618,双11等的秒杀大促,不是一个量级的。其实电商网站平时,就是电商网站三天两头就会搞一次大促活动。如果是 双十一,618出现点full gc,这很正常。小兄弟做的系统,平时这种大促就会导致full gc,那肯定是由问题的,三天两头就会给你报警。那这样的情况怎么去调优呢。
    在这里插入图片描述
6.2 亿级流量网站 评估 与 架构
  • 比方说,我们这有一个亿级流量电商网站,那亿级流量,你不要把它想的特别牛B,其实亿级流量 说白了就是,每日你的网站有用户点击量上亿次,就可以叫做亿级流量。其实现在很多网站,都是亿级流量,比方说京东,像拼多多淘宝,美团, 凡是叫得上名字的,顶尖的互联网公司,基本上 肯定是亿级流量,甚至百亿千亿级都有可能。
6.2.1 日活用户
  • 那对于一个亿级流量的电商网站,假设它刚刚上亿,这样一个亿级流量的网站。它的日活用户,可以推算一下,可能也就五六百万,到1千万的样子。我们推算,正常情况下,你到一个网站上面,买一个东西,平均点击 操作这个网站,也就几十次,那我们假设20次的话。一亿的流量除以20次的点击,可以大概推算一下,日活用户应该大概是在500w左右。那么 日活用户 说白了就是,这网站 每天有多少用户来访问。
6.2.2 付费转化率
  • 当然 它实际上的用户 肯定不止500w,可能是5000w,甚至几个亿 都有可能,但是它的日活用户,亿级流量这个量级的日活用户,按推算也就五六百万的样子。那么 对于电商网站来说,一般它会根据日活用户,会有一个转化率,叫付费转化率。特别是在大促的时候,这个转化率不到10%,可能会接近10%。比方说500w用户来访问你的网站,每个用户,假设它就买一个东西,那日均的成单量,可能是在50w到100w之间,也就是,差不多是百万级别交易的这样的一个电商网站。那10%的转化率,实际上就是指 500w日活用户 有50w个人下单。
6.2.3 百万订单的系统压力推算
  • 实际上,正常情况下,日常不搞大促活动的时候,对于京东,淘宝,唯品会这样的网站,一天的80%的订单,应该都是集中在几个小时之内产生的,比方说,早上一个小时高峰,下午一个小时高峰,晚上2个小时高峰。如果百万级别的订单,几个小时产生,其实算一下,几个小时产生 百来万订单,每秒钟 也就几十几百个订单,而一线大厂的后端的订单系统,肯定有很多台机器集群,每秒几十几百个订单,分到每台机器 可能就几个单,几十个单撑死了,平时这样,对系统没有什么压力。所以,像京东,唯品会,淘宝这样的网站,不搞大促的时候,平时的压力其实并不是特别的大。
  • 那对于 这样百万级别交易规模的网站,小兄弟做的那个系统 上线之后,平时没什么太大问题,一旦到了 大促抢购的 时候,系统就会频繁的系统压力告紧,频繁的发生full gc。实际上,大促的时候,有一个非常大的特点,就是可能网站当天的整个成单交易量,可能80%的订单 都是在大促的 前面几分钟产生。比如说,一天五六十万的订单,就集中在大促的前五六分钟,全部给它下完,那这个量级还是比较大的,平均下来,每秒可能有一两千单。
  • 当然,这里说的大促 不是像 双十一 或 618的那样的大促,其实电商网站 平时,三天两头就会搞 日常的大促秒杀等活动,那么小兄弟做的系统,在这样的大促下场景下,三天两头就会报警。平时的这种大促就会导致full gc。
  • 那对于这样的一个 每秒一两千单 的系统压力。小兄弟做的系统,假设当时是 用的3台机器部署的,比方说,4核8G的 这样的物理机。当然,实际可能会有更多的机器,这里假设就3台机器,那么算下来,其实每台机器 每秒钟大概也就 三四百个单。也就是,每秒钟这个订单系统,一台机器 要抗住 三四百个订单。那小兄弟老背锅是吧!我告诉你,我这小兄弟还挺不错的啊。待会给你看看啊。
  • 这样一个订单系统 每秒钟抗住三四百个订单。那对于这样一个系统,亿级流量;部署到3台 4核8G这样的机器上,大概知道它的压力,每秒三四百个订单。评估做完了之后,这样的业务场景下,一般都要给它的堆内存 设置一些参数。比方说,堆多大,堆中的 各个区域大概是多大。那 这个系统是 怎么来设置它的 JVM的内存大小。
  • 我当时带那兄弟,他这么来设置,他当时 事后跟我说,他说,这个一台物理机是8个G的内存,那一般来说,操作系统 可能要占个 两到三个G,那分配给我们java程序的,可能有4到5个G,那我完全可以给堆 分配个 3个G,然后 剩下的可能1-2个G,要给我们 元空间,很多线程栈 去用,也就是,根据这个 系统机器 的物理内存 来设置的堆的大小,其实我当时带那小兄弟,他就是 出于这样的一个考虑,这样来设置的。
  • 小兄弟 他其实,为什么他会这么来设置,其实我们当时 并不是完全做成 这种订单交易系统,这种交易系统 上线的时候,肯定有严格的压测,所以 这里只是 拿这种系统来给举例,当时带的那小兄弟,他就是凭经验,来这么来设置,但往往就是 经验可能就会误导你,其实 这小兄弟基础还挺不错的,我告诉你 是国内前4排名的大学毕业,整个的基础底子很不错的,但是可能经验不多,是我当时 在上海的一所大学 校招过来的,但是可能由于经验不足,这一块就出了点问题。
  • 那我事后就带他分析了,看问题到底出在了 哪个点,为什么这么来设置,会导致我们系统频繁full gc,其实是有一些固定的分析方法,比方说,按照这个压力每秒300单,是可以大概推算一下,我们整个堆内存,这个地方 大概它应该 要怎么去分配 比较合理,每秒钟300个订单,推算一下,一个订单大概可以占多少内存空间,大概也就1KB。
  • 你下单的话,有订单的对象要产生。比方说,每秒钟300个订单产生,那说白了 在我后端的线程来说,它肯定会调用一个方法,比如叫 createOrder 这样的方法,我不管这个方法具体的实现怎么样,反正它至少肯定会有 这样的方法,而且这样的方法,它肯定会生成订单对象Order,这个毋庸置疑。也就是说,它肯定会有 300个线程,它都会调用这个createOrder这个方法,在后台运行,就会生成300个Order的订单对象。
  • 那我假设这一个订单对象Order,大概1KB大小。一般的对象,都不会超过1KB,可能就还不到1KB。那对象怎么计算大小,对象的大小,无非就是 对象中的 这些成员变量之和,加上 对象头,还有个对齐填充。那成员变量,比如一个对象,有一个整型,整型默认占4个字节;有一个布尔类型,大概是占1个字节;然后 还有String等等。根据你具体程度来看,一个订单对象,可能 有几十个字段,每个字段 有几个字节,到几十个字节不等,算下来 可能几百个字节撑死了,一般来说 不会超过1kb。
  • 对于当前的系统,我当时估算了下,差不多1kb大小,那说白了,每秒钟这个压力,差不多每秒钟有300kb订单对象会生成,放到堆里面,那当然 下单的时候,不止有 这个下单操作,肯定不可能只有一个订单对象。可能还会涉及到 库存扣减,那库存可能也有对象;优惠券的使用,积分的加减,等等 其他的一些对象的生成。也就是 一个业务操作里面,应该根据业务模型去分析,这些对象大概要占用多大空间,最终可能要评估一下。那当时根据我们系统的 核心业务,大概评估了下,每秒钟 可能要抗住300单的压力的话,再给它放大了20倍。也就是,每秒钟可能会有300KB*20,这样规模的一个对象 生成,放到我们堆里面去。
  • 那当然,一个订单系统,不可能说,只满足下单这个接口, 它可能还有 很多其他接口,比方说,订单查询接口,订单退货接口,订单什么什么,一大堆操作接口。实际上 应该根据 系统的一个业务情况,核心业务情况,可以估算一下 ,我当时就估算了之后,跟我的小兄弟一起估算了下,大概给它 又放大了10倍,也就是,按照我们当时那个业务模型,每秒300单的压力情况下 这样一个业务,差不多有 接近300KB3010,每秒钟会有这么多对象产生,算下来 差不多会有60M对象,往我们的堆里去丢。
6.2.4 JVM堆内存参数设置
  • 那既然知道这样的一个系统压力的情况,和 使用对象的一个模型的情况,就可以给JVM,它的堆内存 设置参数了。按照我们当时小兄弟,做的JVM的参数设置,java -Xms3072M -Xmx3072M,设置为3个G:
    在这里插入图片描述

java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize512M -XX:MaxMetaspaceSize512M -jar microservice-eureka-server.jar

  • 那大概算下来,默认的情况,老年代会占2/3,年轻代会占1/3吧,年轻代里面,伊甸园区和Survivor两个区域的比例值是8:1:1 ,也就是 剩下的年轻代1个G,伊甸园区 大概会分800M,Survivor两个区 分别分100M,大概就这么一个模型。
    在这里插入图片描述

  • 基于我们当时 这个系统的压力情况,会发现每秒钟会有60M的对象往 我们的伊甸园区放,现在 伊甸园区只有800M,差不多也就13 或14秒,伊甸园区就会被放满,放满之后会 触发minor gc。其实,这里还有一个因素要考虑,就是对象存活的时间。我们生成的这些60M对象,大概1秒钟之后 都会变成垃圾。因为下单的时候,下单的线程开始运行,执行createOrder方法的时候,这个对象它存活着,但是每秒钟下了300个订单,1秒钟之后,这300个订单,生成了大概60M对象。那实际上 在1秒之后,这个createOrder这个方法结束了,那线程结束了,对应的方法也结束了。
    在这里插入图片描述

  • 那方法结束了,方法中创建的订单对象Order order = new Order();,也要被回收。因为 对应的order局部变量没有了,就是gc root会被释放掉,那这个Order对象还存活在堆里面,之前这个方法没结束的时候,这个对象 可以把它叫做有效对象,但是这个方法结束之后,这些对象 没有gc root 引用着了,就变成是垃圾对象,下一次做gc 的时候,就会把这些 垃圾对象 给它回收掉。

  • 当这个方法在执行过程中,局部变量order,就是gc root,它引用着这个Order对象,这个对象是非垃圾,但是当 这个方法结束的时候,它会把这些局部变量,内存给它清理掉,那么这些对象,变成游离状态,没有任何的引用指向它,就会被回收掉。也就是 这些产生的60M对象,大概过1秒钟之后,线程结束之后,它们都会变成垃圾对象。

  • 那基于这样的一个结论,这里大概 做一次minor gc,当运行到14秒的时候,我们伊甸园放满了,触发了minor gc,那前面13秒产生的那些对象,都会被回收掉,第14秒产生的那60M对象,可能会被复制到Suvivor区。因为第14秒,可能300个线程,都执行到createOrder方法中的Order order = new Order();,刚好线程执行完创建订单这一步之后,伊甸园区放满了,这时候触发了minor gc,触发minor gc就会STW,这个线程被暂停了。

  • 但在第14秒,这个线程还没结束,产生的这些Order对象,还被线程内部的局部变量,也就是gc root 引用着。第14秒产生的那60M对象,可能不是垃圾,会放到我们的Survivor区域里面去,但是这时候,Survivor区域100M,看上去是放的下的,但实际上,它放不下,这60M对象会直接挪到老年代。

  • 其实我们一个对象,会被挪到老年代。除了 分代年龄达到15 会被挪到老年代。
    在这里插入图片描述

  • 还有很多种情况,都会触发 对象被挪到老年代,还有 对于那种比较大的对象,其实也会被直接挪到老年代。
    在这里插入图片描述

  • 还有,比方说 动态年龄判断机制,也会触发我们对象,被挪到老年代。
    在这里插入图片描述

  • 还有 老年代空间分配担保机制。
    在这里插入图片描述

  • 我们当时那小兄弟忽略的一种情况,就是 对象动态年龄判断机制,导致我们对象,直接被挪到老年代,才最终 导致了我们系统,频繁full gc,几分钟就做一次full gc。按照JVM中的这个 动态年龄判断机制 的规则,如果有一批对象,要被挪到Survivor区域,这批对象总大小 如果超过了Survivor区域的50%,那这一批对象,就很有可能 会被直接挪到老年代。
    在这里插入图片描述

  • 我们当时那个小兄弟,那个小兄弟干嘛,把锅都丢到他身上对不对,就是因为小兄弟,没注意到这一个机制,对象挪到老年代的其中一个机制,他可能对这一个机制,对象动态年龄判断机制 了解不是特别透彻,可能也忘记了,所以就导致了 他凭经验设计的这么一个JVM的参数,也就导致了这个问题,那基于这样的问题啊,那就问题大条了,这意味着大概每过14秒,可能就会有60M的对象往老年代放,老年代总共两个G,大概算一下,差不多也就那么五六分钟 就放满了,放满之后 就会触发full gc,触发full gc,是非常影响性能的。

  • 我们jvm调优的主要目的,最最主要的,就是要减少full gc,减少full gc的次数 和 一次full gc的执行时间,当然 如果minor gc,太频繁,也是需要做些调优。

  • 另外的话,每过那么几分钟,就有一大批对象,被挪到老年代,但是 这些个对象,挪到老年代之后,其实根本就没有任何意义,因为 就这些对象,刚挪到老年代,再过1秒钟,可能就会 变成垃圾了,就会被full gc 全部清理掉了。因为我们老年代的主要目的,是存放那些老不死对象,而这些个 朝生夕死的这种对象,挪到老年代 就浪费了 我们程序的性能,所以 这个地方肯定是有问题的。

6.2.5 阿里面试:亿级流量 JVM调优 不发生Full GC
  • 那基于这个案例,阿里有道面试题:能否对JVM调优,让其几乎不发生Full GC。以当前这个系统来举例的话,也就是 把当前的系统做一些性能调优,让它几乎不发生Full GC。在当前这个压力不变化的情况下,其实只要把 那个堆的参数稍微调整下,就可以解决问题了。
    在这里插入图片描述

  • 因为老年代根本就不需要2G这么大的空间,应该把Survivor这块空间给它调大一点,或者说 把这个年轻代的比例调大一点,我把年轻代给它调到了2个G,那年轻代调成2个G之后,根据伊甸园区和Survivor两个区域的默认比例:8:1:1,那么 伊甸园区实际上,大概现在就是 默认1.6G,Suvivor的两个区域 差不多就是 分别200M,大概就这么个情况。
    在这里插入图片描述

java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize256M -XX:MaxMetaspaceSize256M -jar microservice-eureka-server.jar

  • 那基于这样的场景,这样的这样参数设置,再有60M对象,从伊甸园区,minor gc放过来,Suvivor区200M 就放得下,没有触发 对象动态年龄判断机制。然后 下一次,伊甸园区又放满了,又触发minor gc不光回收伊甸园区域,还会回收非空的Survivor区,这次回收的时候,这个非空的Survivor区域S0里的60M对象,早就已经变成垃圾了,因为已经过了14秒,它早就变成垃圾了,那肯定会给它清掉。只有第14秒,伊甸园区新增的那60M对象,会被复制到空着的Survivor区域S1,那剩下的伊甸园区和非空的Survivor区S0的这些对象,全被清掉。

  • 那程序 继续运行,伊甸园区又被放满了,放满了又触发了minor gc,那这回,又会回收 伊甸园区和非空的Survivor区域S1,这块非空的Survivor区域S1的60M对象,又变成垃圾了,又会给它干掉,那又有60M对象,从伊甸园区 挪到另一块空着的Survivor区域S0这边来,那非空的那块Survivor区域S1又清空了,下一次再放满,整个过程一样的。

  • 那么,整个我们的对象,要么在Survivor区的其中一块被干掉,要么在伊甸园区被干掉,也就是,我们这些朝生夕死的对象,永远都只会在年轻代被干掉,几乎就不会去到老年代。那老年代,只要不改变程序的情况下,压力不是说 增大到很大程度,那老年代几乎不会发生Full GC。

  • 以上就是 调优的一个思路。我们要尽量的,把那些朝生夕死的对象,尽量在年轻代,就给它干掉,然后 还要基于系统的压力,或者说压测的各种情况,你估算一下系统压力,然后评估出合适的JVM的 一个内存模型的 内存分配。当然评估这个模型,它没那么简单,要考虑到各种情况,比方说,还有 对象老年代空间分配担保机制,把这些所有的情况都考虑进去,才可能真正的应对 这种大规模 并发量的 线上系统的 一些JVM调优。

  • 其实,如果 真的要把JVM的参数,调的非常牛B,还要掌握很多技术,还要掌握很多JVM内部的一些机制,比方说,对象的 内部创建的 一些机制,类结构,类加载 这块关系 还不是特别大,关键的其实还要清楚 像内部的 一些垃圾收集算法。
    在这里插入图片描述

  • 比如,还有 分代收集算法。
    在这里插入图片描述

  • 比如这里说的 复制算法。
    在这里插入图片描述

  • 了解这些算法之后,还要了解 各种垃圾收集器,比如 CMS,G1,ZGC等。每一种垃圾收集器,都必须把它内部的机制 彻底理解。比方说,每一种垃圾收集器,它底层 都有大量的一些参数,比方说CMS,
    在这里插入图片描述

  • 当然 这里只是标一些核心参数,每一种垃圾收集器,只有把它里面 每一个参数,包括它的每一种情况,都彻底搞明白之后,这样 才可能 针对于自己的 这个业务场景,设计出来一个非常合理的内存模型。

  • 虚拟机栈,程序计数器,本地方法栈 为线程独享;堆 和 方法区 为所有线程共享。

相关代码:
public class Math{
	public static final int initData = 666;
	public static User user = new User();
	public int initData1 = 666;
	public int compute() {	//一个方法对应一块栈帧内存区域
		int a = 1;
		int b = 2;
		int c = (a + b) * 10;
		return c;
	}
	public static void main(String[] args) {
		Math math = new Math();
		math.compute();
	}
}

垃圾回收是Java中一项重要的语言特性,它负责自动管理内存并释放不再使用的对象。Java虚拟机(JVM)的垃圾回收机制使用了一种称为"Stop-The-World"(STW)的技术。本篇文章将详细解析Java垃圾回收和JVM中的STW。

  1. Java垃圾回收(Garbage Collection)

在Java中,垃圾回收是自动进行的,无需开发人员手动管理内存。垃圾回收器会周期性地扫描堆内存,标记并释放不再使用的对象所占用的内存空间。这样,开发人员可以专注于业务逻辑而不必过多关注内存管理。

  1. 垃圾回收算法

Java虚拟机中的垃圾回收器使用了不同的垃圾回收算法,例如标记-清除算法、复制算法、标记-整理算法等。这些算法的选择取决于应用程序的特点和需求。

  1. Stop-The-World(STW)

STW 是Java垃圾回收机制中的一种策略。当垃圾回收器开始执行时,它会暂停应用程序的所有线程,使得整个应用程序进入停顿状态。在这个过程中,垃圾回收器可以安全地执行垃圾回收操作,而不会受到应用程序线程的干扰。

STW对于应用程序的性能和响应时间有一定的影响,特别是在大型内存的应用中。因此,优化垃圾回收和减少STW的时间是提高应用程序性能的重要考虑因素。

.参考文献

图灵课堂-诸葛老师

1.Java垃圾回收和JVM中的STW解析.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1454185.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《Go 简易速速上手小册》第7章:包管理与模块(2024 最新版)

文章目录 7.1 使用 Go Modules 管理依赖 - 掌舵向未来7.1.1 基础知识讲解7.1.2 重点案例&#xff1a;Web 服务功能描述实现步骤扩展功能 7.1.3 拓展案例 1&#xff1a;使用数据库功能描述实现步骤扩展功能 7.1.4 拓展案例 2&#xff1a;集成 Redis 缓存功能描述实现步骤扩展功能…

代码随想录刷题笔记 DAY 28 | 复原 IP 地址 No.93 | 子集 No.78 | 子集 II No.90

文章目录 Day 2801. 复原 IP 地址&#xff08;No. 93&#xff09;1.1 题目1.2 笔记1.3 代码 02. 子集&#xff08;No. 78&#xff09;2.1 题目2.2 笔记2.3 代码 03. 子集 II&#xff08;No. 90&#xff09;3.1 题目3.2 笔记3.3 代码 Day 28 01. 复原 IP 地址&#xff08;No. 9…

SAP BC Partner XXXX:3299 not reached

带SAProuter 出现如下问题 不带SAProuer 正常登录 原因&#xff1a;SAProuter 服务未开启。 开启过程如下&#xff1a; 进入对应的SAProuter 目录 一般是SAProuter 服务器上 cmd进入目录 执行 saprouter.exe -r -V 2 -G saprouter.og -K "p:CNsap-router, OU0000725…

手把手一起开发SV4E-I3C设备(二)

JEDEC DDR5 SPD Hub Devices例程 DDR5生态系统的核心是SidebandBus Protocol 参考下图&#xff0c;可以将SV4E-I3C的端口1声明为主服务器(模拟主机控制器)&#xff0c;并且它可以属于SV4E-I3C上的一个总线。端口2可以作为SPD Hub DUT的Local Bus侧的从站连接。这个从站可以被…

JWT和base64

1.1 jwt和token 1.1.1 token介绍 令牌&#xff08;Token&#xff09;&#xff1a;在计算机领域&#xff0c;令牌是一种代表某种访问权限或身份认证信息的令牌。它可以是一串随机生成的字符或数字&#xff0c;用于验证用户的身份或授权用户对特定资源的访问。 简单理解 : 每个…

【解决(几乎)任何机器学习问题】:处理分类变量篇(上篇)

这篇文章相当长&#xff0c;您可以添加至收藏夹&#xff0c;以便在后续有空时候悠闲地阅读。 本章因太长所以分为上下篇来上传&#xff0c;请敬请期待 很多⼈在处理分类变量时都会遇到很多困难&#xff0c;因此这值得⽤整整⼀章的篇幅来讨论。在本章中&#xff0c;我将 讲述不同…

H5028B 车灯舞台灯 PWM调光 可温控 48V 60V 72V 80V 100V降压芯片

带温控功能的降压恒流芯片的工作原理如下&#xff1a; 降压功能&#xff1a;首先&#xff0c;芯片会监测输入电压&#xff0c;并通过内部的电路将输入电压降低到所需的输出电压水平。这可以通过开关电源转换技术实现&#xff0c;例如脉冲宽度调制&#xff08;PWM&#xff09;或…

2024年【熔化焊接与热切割】考试题库及熔化焊接与热切割考试报名

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 熔化焊接与热切割考试题库考前必练&#xff01;安全生产模拟考试一点通每个月更新熔化焊接与热切割考试报名题目及答案&#xff01;多做几遍&#xff0c;其实通过熔化焊接与热切割模拟考试题库很简单。 1、【单选题】…

硬错误-STM32

需要修改栈大小 还得是野火的文档比较讲得深一点。

图解linux零拷贝技术

转发自&#xff1a;https://zhuanlan.zhihu.com/p/442771856 1、数据拷贝基础过程 在Linux系统内部缓存和内存容量都是有限的&#xff0c;更多的数据都是存储在磁盘中。对于Web服务器来说&#xff0c;经常需要从磁盘中读取数据到内存&#xff0c;然后再通过网卡传输给用户&am…

第13章 网络 Page741~744 asio核心类 ip::tcp::socket

1. ip::tcp::socket liburl库使用"curl*" 代表socket 句柄 asio库使用ip::tcp::socket类代表TCP协议下的socket对象。 将“句柄”换成“对象”,因为asio库是不打折扣的C库 ip::tcp::socket提供一下常用异步操作都以async开头 表13-3 tcp::socket提供的异步操作 …

ElementUI Form:Cascader 级联选择器

ElementUI安装与使用指南 Cascader 级联选择器 点击下载learnelementuispringboot项目源码 效果图 el-cascader.vue&#xff08;Cascader 级联选择器&#xff09;页面效果图 项目里el-cascader.vue代码 <script> let id 0; export default {name: el_cascader,dat…

根据Ruoyi做二开

Ruoyi二开 前言菜单代码生成新建微服务网关添加微服务的路由 vue页面和对应的js文件js中方法的url和controller中方法的url总结 前言 之前写过一篇文章&#xff0c;若依微服务版本搭建&#xff0c;超详细&#xff0c;就介绍了怎么搭建若依微服务版本&#xff0c;我们使用若依就…

【大厂AI课学习笔记】【2.1 人工智能项目开发规划与目标】(2)项目开发周期

我们来学习项目开发的周期。 再次声明&#xff0c;本文来自腾讯AI课的学习笔记&#xff0c;图片和文字&#xff0c;仅用于大家学习&#xff0c;想了解更多知识&#xff0c;请访问腾讯云相关章节。如果争议&#xff0c;请联系作者。 今天&#xff0c;我们来学习AI项目的周期。 主…

基于python的企业编码管理系统源码【附下载】

《企业编码生成系统》程序使用说明 在PyCharm中运行《企业编码生成系统》即可进入如图1所示的系统主界面。在该界面中可以选择要使用功能对应的菜单进行不同的操作。在选择功能菜单时&#xff0c;只需要输入对应的数字即可。 图1 系统主界面 具体的操作步骤如下&#xff1a;…

如何选择阿里云服务器配置?阿里云服务器CPU内存带宽攻略

阿里云服务器配置怎么选择&#xff1f;根据实际使用场景选择&#xff0c;个人搭建网站可选2核2G配置&#xff0c;访问量大的话可以选择2核4G配置&#xff0c;企业部署Java、Python等开发环境可以选择2核8G配置&#xff0c;企业数据库、Web应用或APP可以选择4核8G配置或4核16G配…

QML | 属性特性(property)

一、属性特性 属性是对象的一个特性,可以分配一个静态的值,也可以绑定一个动态表达式。属性的值可以被其他对象读取。一般而言,属性的值也可以被其他对象修改,除非显式声明不允许这么做,也就是声明为只读属性。 1.定义属性特性 属性可以在C++中通过注册一个类的Q_PROPERT…

推荐几个漏洞扫描工具

渗透测试收集信息完成后&#xff0c;就要根据所收集的信息&#xff0c;扫描目标站点可能存在的漏洞了&#xff0c;包括我们之前提到过的如&#xff1a;SQL注入漏洞、跨站脚本漏洞、文件上传漏洞、文件包含漏洞及命令执行漏洞等&#xff0c;通过这些已知的漏洞&#xff0c;来寻找…

matlab代码--汉明码通过不同信道的误码率分析

1 前言 汉明码是在原数据中的一些固定位置&#xff0c;插入一个0&#xff08;或1&#xff09;&#xff0c;以进行奇&#xff08;或偶&#xff09;校验位&#xff0c;虽然使原数据变长&#xff0c;但可使其拥有纠错能力。能侦测并更正一个比特的错误&#xff1b;若有两个比特出…

京东平台的行业数据(数据分析报告)在哪里看?如何获取?

京东行业分析数据获取可通过以下途径&#xff1a; 京东官方不定期会发布行业白皮书或市场研究报告&#xff0c;商家可以关注京东官网、官方公众号、官方论坛等渠道获取最新发布的研究报告。 对于有技术开发能力的企业&#xff0c;可以通过申请接入京东开放平台API&#xff…