JVM
JVM虚拟机模型图
- 一个Java文件运行流程:Java文件编译成class字节码文件,然后通过 类装载子系统 加载到 运行时数据区(内存模型/内存区域),字节码执行引擎 去执行代码。
- JVM虚拟机核心:JVM内存区域。
- 图中紫色部分是 线程私有 的,橙色部分是 线程共享 的。
JVM内存区
栈(线程)
- 主要存放局部变量。
- 程序中每个线程运行时,JVM虚拟机都会在栈中给运行的线程分配一小块专属的栈内存空间,线程里运行的方法内部会有局部变量,方法里面的局部变量存则存放在对应线程专属的栈内存空间中。
栈帧
- 栈内部由一块一块的栈帧组成。
一个方法对应一块栈帧内存区域。
一个线程有多个方法:一个线程专属的栈内存空间中,有多个栈帧内存区域,每个方法的局部变量都在自己的栈帧内存区域中。 - 特点:先进后出,后进先出(FINO)。下图先执行
main()
方法,main()
调用compute()
方法。 - 栈帧内部核心的4块(不止4块但其它不重要):
- 局部变量:存放方法内的局部变量。
- 操作数栈:
操作数栈 可理解为java虚拟机栈中存放临时操作数的一块内存区域。
javap-c可反汇编字节码文件为字节码指令文件,字节码指令指定了每个步骤做什么。
如:int a = 1; int b = 2; int c = a + b;
- 变量初始化时,先把赋给变量的值压入 操作数栈,局部变量表 给变量分配内存空间后,则把对应的值从 操作数栈 中弹栈赋值给变量。
(引用类型时,变量中存储的是对象在 堆 内存中的地址) - 方法内变量做运算时,先把 局部变量表 中的变量对应的值复制一份压入 操作数栈,然后从 操作数栈 弹栈出去做运算,将结果在压入 操作数栈,然后重复1的分配内存空间并弹栈赋值。
- 变量初始化时,先把赋给变量的值压入 操作数栈,局部变量表 给变量分配内存空间后,则把对应的值从 操作数栈 中弹栈赋值给变量。
- 动态连接:Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
- 方法出口:记录了当前方法运行完后要回到主方法的哪一行位置。
程序计数器
- 记录当前线程正在运行的字节码指令的行号/位置。
- 每个线程都有一个独有的程序计数器,线程一开始运行就会有一个程序计数器来记录线程运行到哪一行代码的位置。
- JVM虚拟机需要程序计数器的原因:
当CPU被别的线程抢走时记录当前线程要挂起,其它线程执行完后,当前线程需要恢复,程序计数器 会告知从哪一步恢复执行。
字节码执行引擎(执行引擎)
- 字节码执行引擎 会执行字节码文件,且每执行一步操作, 就会去 程序计数器 中修改线程执行到的位置。
方法区(元空间)
- jdk1.8之前也叫 永久代,jdk1.8开始改为 元空间。
- 存放 常量、静态变量、类信息。
- 类装载子系统 把字节码文件(类信息)加载到 运行时数据区(内存模型/内存区域) 的 方法区 里面。
- 当 静态变量 是引用类型时,该静态变量中存储的是对象在 堆 内存中的地址。
本地方法栈
- 本地方法:
private native void start0();
这是一个本地方法,有native
修饰。不是Java接口,是本地方法接口。早期Java出世时与旧项目跨语言对接要用。
本地方法接口是由C实现的,当Java执行到本地方法时,会去到操作系统本地的库函数寻找C语言实现的后缀.dll
的文件。类似于Java的jar包,里面有很多的函数的实现。 - 本地方法运行时也需要内存空间,对应的内存区域就是 本地方法栈。
堆
- 堆 里面分为:年轻代 和 老年代。
年轻代 里面分:Eden(伊甸园区)、survivor(幸存者区)。
幸存者区 里面分:from区、to区。 - 堆内存 分配:
老年代 默认占 堆内存 2/3的空间,年轻代 默认占1/3。
伊甸园区 默认占 年轻代内存 中的8/10,from区、to区 各1/10。 - 可达性分析算法:找 垃圾对象 的算法。
垃圾对象:没有被变量引用的对象。 - GC:垃圾收集器
- Minor GC(新生代GC):回收 年轻代 的垃圾收集器。收集 垃圾对象 并将 非垃圾对象 通过 复制算法 挪到 幸存者区 里面,同时对象的分代年龄+1。
- 对象动态年龄判断机制:如挪到 幸存者区 的那一批对象大于 from区 或 to区 50%的内存大小,会被直接挪到 老年代。
- Major GC():
- Full GC:清理整个堆空间(包括 年轻代 和 永久代 )。执行时会触发STW(stop the world),并专心收集垃圾。
- stop the world/STW:停止用户的应用线程。Java虚拟机调优目的是为了减少STW的触发、执行时间(减少Full GC的触发次数、减少Full GC执行时间)。
- Minor GC(新生代GC):回收 年轻代 的垃圾收集器。收集 垃圾对象 并将 非垃圾对象 通过 复制算法 挪到 幸存者区 里面,同时对象的分代年龄+1。
- GC Roots根:线程 栈 的本地变量(栈帧 中的引用类型变量)、静态变量、本地方法栈的变量等等。以这些变量为起点向下搜索引用的对象,找到的对象都被标记为 非垃圾对象,其余未标记的对象都是 垃圾对象。
- 刚new出来的对象放在 伊甸区 分代年龄为0,当 伊甸园区 内存满时,执行引擎 专门开启一个线程执行 Minor GC 收集 垃圾对象,并将 非垃圾对象 通过 复制算法 挪到 幸存者区 里面 ( 幸存者区里面每次只有一个区存放对象,轮流存放。对象动态年龄判断机制:如挪到 幸存者区 的那一批对象大于 from区 或 to区 50%的内存大小,会被直接挪到 老年代 ) ,同时对象的分代年龄+1。
当对象的分代年龄到达15时,该对象会被直接挪到 老年代。
() - 当 老年代 放满后,执行引擎 专门开启一个线程执行 Full GC 收集 垃圾对象,如果老年代没有 垃圾对象 回收,且没有空间放其它对象,就会内存溢出(OOM)。
线上系统JVM调优案例模拟
举例模型图(数据量不是百分百真实)
举例模型分析
- 有固定的方法/一定的依据。
上图是以后端成百上千个系统中的订单系统来举例。
根据核心业务场景的内存使用容量来分配。 - 订单系统每秒钟产生300个 单/order对象。
假设一个order对象占用内存1KB,对象大小根据对象头的大小和对象实例数据的大小估算,对象的每个成员变量占多少个字节。如对象内部成员变量的类型(如:int类型4个字节)。 - 真正的订单系统下单时,不可能只产生一个order对象,还会有其它对象如:子order、库存、优惠券、积分等,这些对象都需要有内存空间存放。这里把JVM堆内存使用情况放大20倍来模拟。
(300KB*20)/秒 = 6M - 订单系统不可能只有一个下单操作,如:还有订单查询操作、订单取消操作、退货操作等,在该业务系统大概情况下再放大10倍来模拟。
(300KB*20*10) = 60M,这个意味着核心业务系统每秒钟要往堆里面放60M对象,且1秒后这些对象就可能都变成 垃圾对象。
内存使用模型图
- 订单系统服务内存分析:
一个4核8G的物理机,一般2-3G给到操作系统或其它程序使用,给到JVM虚拟机只有4-5个G。且还不能全给 堆内存。 - 堆内存使用分析:
- 线程每秒产生60M对象,运行13秒占满 Eden,触发 Minor GC 回收对象。
- 此时回收的对象只有前12秒的,第13秒产生的可能不是 垃圾对象,因方法还未执行完,GC Roots根 还存活着,会被挪到幸存者区。
- 但因为对象超过 from区 50%内存大小,所以直接被挪到 老年代。导致每过13秒就会有60M对象进入 老年代,大概每过7分钟左右 老年代 就会放满。
- 此时再往 老年代 放对象会触发 Full GC,老年代 里面基本99%的对象都是 垃圾对象,且每过7分钟触发一次 Full GC,因此这些对象没必要放入 老年代。
- 让其几乎不发生 Full GC,优化方案分析:
正在存放在 老年代 的对象其实只有一丢丢,垃圾对象没必要挪到老年代,因此老年代不需2G内存。把年轻代内存扩大,尽量让 垃圾对象 在 年轻代 就被销毁。
整个 堆 的大小不变,年轻代 设为2G:
java -Xms3072M -Xmx3072m -Xmx2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
优化后 内存使用模型图
- 优化后:
- 每过25秒才占满 Eden,触发 Minor GC 后,第25秒对象挪到幸存者区。
- 对象动态年龄判断机制 发现对象并未达到 from区 50%内存大小,不需被挪到 老年代。
- Eden 再次占满触发 Minor GC ,回收对象时之前进入 幸存者区 的垃圾对象也被回收,没有机会进入 老年代,老年代 放不满没有机会触发 Full GC。