1、JDK回顾

JDK包含Java程序语言、(java、javac、javadoc、jar、javap等)工具程序与JRE,我们今天讲解的主角:JVM,属于JRE部分。详见下图:
二、JVM内存模型 - 图1

2、JVM整体结构

二、JVM内存模型 - 图2
JVM结构整体分为三大部分:
1、类加载系统
用于加载类。
2、运行时数据区
运行时数据区又可以分为以下板块:
堆:我们新建的类对象都放在这个区域
栈:说白了,每开启一个方法(线程)都会开启一个栈内存,栈内存里面存放一个个栈帧,栈帧存放局部变量表,操作数栈,动态链接,方法出口。
本地方法区:存放C、C++语言编写的本地方法
程序计数器:存放每个方法执行的位置
方法区(元空间):存放常量、静态变量、类信息等。
3、字节码执行引擎
它可以用来执行GC。

3、垃圾回收机制初步讲解

在这里我们只介绍一下大体步骤,至于里面的细节,我们在后面的章节再讲。

  • 堆分为年轻代和老年代,默认内存比例1:2。年轻代又有伊甸园区、s0幸存者区、s1幸存者区,默认内存比例为8:1:1;
  • 当伊甸园区内存满了,就会发生minor gc,将可以回收的垃圾对象清理,没有被回收的,先放到s0幸存者区。当发生下一次minor gc时,会把仍然没有被清理的对象放入s1幸存者区,如此往复;
  • 当发生15次minor gc时,会发生一次full gc,将依然存活的对象放入老年代,比如spring bean。当老年区也满了时,就会报内存溢出;
  • 注意,当发生gc时,会触发STW(stop the world)机制,停止用户新的请求,minor gc 时,STW时间很短,可忽略不计,但是full gc 时,STW时间较长,所以我们优化的一个重要方向就是,尽量避免full gc。

文字太枯燥,有没有实时查看垃圾回收过程的工具?
这里介绍一个JDK自带工具:jvisualvm。
先写一段死循环代码:

  1. package com.example.jvmstudy.class2;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. /**
  5. * @author liwq
  6. * @description
  7. * @date 2022-01-19 17:39
  8. */
  9. public class TestGC {
  10. public static void main(String[] args) {
  11. List<TestGC> list = new ArrayList<>();
  12. while (true) {
  13. TestGC testGC = new TestGC();
  14. list.add(testGC);
  15. }
  16. }
  17. }

运行程序后,打开cmd窗口,运行:jvisualvm,会打开这个工具,我们就可以动态查看GC过程了:
image.png

4、JVM内存参数设置

我们先看一个参数设置例子,spring boot项目一般这么设置:

  1. java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar

二、JVM内存模型 - 图4
这些参数都是啥意思?
-Xss:每个线程的栈大小
-Xms:设置堆的初始可用大小,默认物理内存的1/64
-Xmx:设置堆的最大可用大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
注意:由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC(具体表现为启动项目巨慢!这是我们经常遇到的),通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。

5、栈溢出的原因

我们都知道,当full gc 都回收不了对象时,会发生内存溢出。但是栈溢出是怎么导致的?
前面我们讲了,JVM每执行一个方法(线程),都会开辟一块栈内存,如果有子方法,会在这块栈内存分配栈帧,注意这个栈内存是有大小限制的(默认1M),如果里面有循环调用的话,不断开辟栈帧,当循环次数太大,栈内存不够,就会发生栈溢出。
我们拿下面的例子来讲解,本案例,有一个main方法,执行到这里时会开辟栈内存,main方法调用redo子方法,在这里会发生循环调用,最终栈内存不够,报错。来看代码:

  1. package com.example.jvmstudy.class2;
  2. /**
  3. * @author liwq
  4. * @description
  5. * @date 2022-01-19 17:26
  6. */
  7. public class StackOverflowTest {
  8. static int count = 0;
  9. static void redo() {
  10. count++;
  11. redo();
  12. }
  13. public static void main(String[] args) {
  14. try {
  15. redo();
  16. } catch (Throwable t) {
  17. t.printStackTrace();
  18. System.out.println(count);
  19. }
  20. }
  21. }

运行结果:

  1. java.lang.StackOverflowError
  2. at com.example.jvmstudy.class2.StackOverflowTest.redo(StackOverflowTest.java:13)

如何避免?
很简单,将-Xss参数调大,注意,我们工作时,一般不会调这个参数,为什么?因为默认的1M足够用了,当我们在项目中发生栈溢出时候,很可能我们某个方法递归调用次数太多,我们再来调整这个参数即可。