二、JVM内存模型及内存参数设置

二、JVM内存模型

1、Java语言跨平台特性

  • java程序主要通过JVM来实现跨平台的,JVM编译器将Java源代码文件编译成字节码文件(一次编译,随处运行),然后不同的操作系统生成的机器码不同,但是JVM运行是相同的,JVM解释器将字节码文件转换为机器可执行的二进制机器码

2、JVM内存模型

  • jvm包含两个子系统为类装载子系统字节码执行引擎。两个组件为运行时数据区本地接口
    • 类装载子系统:根据给定的全限定名类名(如:java.lang.Object来装载Class文件到Runtimedata area中的Metaspace。
      字节码执行引擎:执行classes中的指令。
      本地接口:与本地方法库交互,是其它编程语言交互的接口。
      运行时数据区:这就是我们常说的JVM的内存。
  • 一个Java程序的执行过程:
    1. 首先通过编译器把 Java 代码转换成字节码。
    2. 类装载子系统(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行。
    3. 字节码执行引擎(Execution Engine),将字节码翻译成底层系统指令。
    4. 由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

示例

  • image-20220327143624979

  • //上图示例中的代码:
    public class Math {
        public static int initData = 123;
        public static User user = new User();
        
        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();
            System.out.println("end");
        }
    }
    
  • 通过类加载器将类(类元信息)加载到方法区中,compute() 方法也被加载到方法区中;java代码在执行 math.compute() 的时候,通过 math 对象的对象头中的头指针找到存储在方法区中的 compute() 方法的指令码的入口地址,然后将这个入口地址放到栈中的动态链接内存区域中。

    1. 类装载子系统(ClassLoader),将class文件经过加载、验证、准备、解析、初始化后生产类元信息放入到方法区中(类装载子系统主要将类装载到方法区);常量池是在方法区中的。
    2. 栈里面的变量引用指向堆中的对象;基本类型的局部变量放到对应的栈中,对象放到堆中。
    3. 对象(在堆中)的对象头中有一个指针,指向的是这个对象所属类的类元信息(方法区中)。
    4. 静态变量是存在方法区中的,其引用指向堆中的对象。

3、JVM的内存区域

  • JVM内存模型主要包括几大模块:程序计数器(Program Counter Register)、虚拟机栈(VM stack)、堆(Heap)、元空间(Metaspace,JDK8后由方法区改为元空间)、本地方法栈(Native Method Stack)。
  • 其中堆、元空间是线程共享的,程序计数器、虚拟机栈、本地方法栈是线程私有的。

(1)程序计数器(Program Counter Register)

  • 线程私有的。
  • 存储指指向当前线程执行到的指令,由字节码执行引擎读取下一条指令。
  • 字节码执行引擎通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制。
  • 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪里。
  • 为了线程切换后能恢复到正确的执⾏位置,每条线程都需要有⼀个独⽴的程序计数器,各线程之间计数器互不影响,独⽴存储

(2)虚拟机栈(VM Stack)

  • 线程私有的
  • 虚拟机栈是由一个个栈帧组成,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
    • 局部变量表:存放局部变量。
    • 操作数栈:具体的值,做操作的时候用它来作临时空间,执行完后结果会放到局部变量表中。
    • 动态链接:动态链接会将方法的符号引用改为指向内存地址的直接引用,这里是用来存地址的值的。
    • 方法出口:内层方法返回外层方法的时候,需要知道外层方法执行到第几行了,方法出口就是存储外层方法执行的位置。
  • 存放基本类型的变量、实例方法、引用类型变量都是在函数的栈内存中分配;
  • 每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。

(3)本地方法栈(Native Method Stack)

  • 线程私有的。
  • 本地方法栈和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行Java方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native方法服务。

(4)堆(Heap)

  • 线程共享的。
  • 创建的对象和数组都保存在 Java 堆内存中。
  • Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap
  • 包含年轻代与老年代。年轻代分为Eden区与s0区,s1区。默认年轻代占堆的1/3,老年代2/3。Eden : s0 : s1=8 : 1 : 1。

(5)元空间(Program Counter Register)

  • 线程共享的。
  • 通常用来储存装载的类的元结构信息、常量、静态变量
  • 如果静态变量为对象类型,则存储一个指向堆的内存地址。

4、堆的GC回收机制

  • GC 一般分为 Minor GC 和 Full GC。Minor GC会在年轻代中触发,而Full GC是在老年代中触发。
  • GC回收的过程:
    1. 当new 一个对象时,该对象会被放入Eden区,创建的对象越来越多,Eden区快满的时候会触发一次轻量级垃圾回收(Minor GC),会从gc root开始查找,将没有对象引用的对象给回收掉。
    2. 未被回收的对象被放入S0区,Eden被清空,这些还存活的对象的分代年龄会+1。
    3. 继续 new 对象,当Eden区再次快放满的时候,又会触发一次垃圾回收机制(Minor GC),将Eden区 和 S0区从gc root开始查找,将没有对象引用的对象给回收掉。将未被回收的对象一起放入到S1区,S1区 和 Eden区 被清空,这些还存活的对象的分代年龄会+1。
    4. 之后Eden区再次快放满的时候,又会触发一次垃圾回收机制(Minor GC),将Eden区 和 S1区从gc root开始查找,将没有对象引用的对象给回收掉。将未被回收的对象一起放入到S0区,S1区 和 Eden区 被清空,这些还存活的对象的分代年龄会+1。
    5. 对象的对象头中存储了分代年龄的存储信息,当这个对象的分代年龄达到了阈值(默认15),还没有被垃圾机制回收,则会将这些对象放入到老年代。
    6. 以此类推,一再触发Minor GC,直到老年区存满,则会触发一次重量级垃圾回收机制(Full GC)。Full GC会回收整个堆和方法区。如果full gc后再存对象还是存不下,则会触发OOM异常。
  • 在GC回收的时候会触发STW(stop the work),STW会暂停当前运行的线程,这个时候对用户来说会有明显的卡顿,因此JVM调优的目的就是减少STW的次数。

5、为什么要STW

  • 因为每次gc的时候从gc root去查找对象是否存活的计算十分复杂,耗时很长。如果不暂停正在运行的线程,会出现一个对象可能在gc root检查的时候是存活对象,然后检查完后,移动到s0区 之后对象执行完毕,被释放掉了。这个时候就浪费了之前的gc root检查。会导致当前的gc root检查出来的结果不正确。

6、JVM内存参数设置

  • img

  • Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):

    • java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
      
    • -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的初始阈值(元空间无固定初始大小),如果不指定元空间的大小, 64位系统默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。
    • -XX:PermSize代表元空间(永久代)的初始容量,JDK8及以后已经没有了

      • 由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。
  • StackOverflowError示例:

    • // JVM设置  -Xss128k(默认1M)
      public class StackOverflowTest {
          
          static int count = 0;
          
          static void redo() {
              count++;
              redo();
          }
      
          public static void main(String[] args) {
              try {
                  redo();
              } catch (Throwable t) {
                  t.printStackTrace();
                  System.out.println(count);
              }
          }
      }
      
      运行结果:
      java.lang.StackOverflowError
      	at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:12)
      	at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
      	at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
         ......
      
    • 结论:-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多。

7、JVM内存参数大小该如何设置?

  • JVM参数大小设置并没有固定标准,需要根据实际项目情况分析,下面提供一个案例

  • 日均百万级订单交易系统设置JVM参数案例

    • image-20220329154944073

    • 我们初次给服务器的内存参数设置如下

    • ‐Xms3072M ‐Xmx3072M ‐Xss1M -XX:MetaspaceSize=512M ‐XX:MaxMetaspaceSize=512M
      
      # ‐Xms3072M 表示堆初始化内存为3G
      # -Xmx3072M 表示堆最大内存为3G
      # -Xss1M 表示每个线程内存为1M
      # -XX:MetaspaceSize=512M 表示元空间初始化内存为512M
      # -XX:MaxMetaspaceSize=512M 表示元空间最大内存为512M
      
      #因为堆的新生代:老年代比例大约是1:2, 即新生代为1G、老年代为2G。 在新生代中Eden区、S0区、S1区比例大约是8:1:1,即Eden区为800M,S0区为100M,S1区为100M。
      
    • image-20220329161924005

    • 由上图可知,生成对象首先会在Eden区,如果每秒产生60M对象,那么14秒后Eden区就会被占满,此时会触发Minor GC,第14秒产生的对象因为可能还会被引用所以没有被回收,根据垃圾回收机制存活的对象会被放到S0区,但是根据动态对象年龄判断原则,这60M对象同龄而且总和大于S0区的50%,那么这些对象都会被挪到老年代。14秒就会产生一个60M对象到老年代且1秒后就会变成垃圾,大概8分钟老年代的内存就会被沾满,就会触发Full GC。因为Full GC是重量级回收,如果每8分钟就要执行一次Full GC会导致系统性能很低。所以合理设置内存参数可以达到几乎不产生Full GC的效果。

    • 我们再次给服务器的内存参数设置如下

    • -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
      
      
      # ‐Xms3072M 表示堆初始化内存为3G
      # -Xmx3072M 表示堆最大内存为3G
      # -Xmn2048M 表示新生代大小为2G
      # -Xss1M 表示每个线程内存为1M
      # -XX:MetaspaceSize=512M 表示元空间初始化内存为512M
      # -XX:MaxMetaspaceSize=512M 表示元空间最大内存为512M
      
      #此时设置新生代为2G,所以老年代为1G。 在新生代中Eden区、S0区、S1区比例大约是8:1:1,即Eden区大约为1638M,S0区为205M,S1区为205M。
      
    • 在重新设置了服务器内存参数后,我们再次分析:如果每秒产生60M对象,则需要大约27秒Eden区会被占满,此时触发Minor GC,第14秒产生的对象因为可能还会被引用所以没有被回收,根据垃圾回收机制存活的对象会被放到S0区,此时由于这60M对象同龄总和没有达到S0区的50%,不会直接进入老年代,因此在下一次Minor GC就会被回收。

  • 合理设置内存参数可以达到几乎不产生Full GC的效果。

  • JVM优化核心思想:就是尽可能让对象都在新生代里分配和回收,尽量别 让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值