• 02-JVM内存模型深度剖析与优化


    一、JDK体系结构

    在这里插入图片描述


    JDK: JDK提供了编译、运行Java程序所需的各种资源和工具;包括Java编译器,Java运行时环境【JRE】;开发工具包括编译工具(javac.exe) 打包工具(jar.exe)等。
    JRE: 即JAVA运行时环境,JVM就是包括在JRE中,以及常用的JAVA类库等;
    SDK: SDK是基于JDK进行扩展的,是解决企业级开发的工具包。如JSP、JDBC、EJB等就是由SDK提供的 ;
    JVM(Java Virtual Machine),Java虚拟机,可以看做是一台抽象化的计算机,它有一套完整的体系架构,包括处理器、堆栈 、寄存器等。
    在运行时环境,JVM会将Java字节码解释成机器码。机器码和平台相关的(不同硬件环境、不同操作系统,产生的机器码不同),所以JVM在不同平台有不同的实现。
    目前JDK默认使用的实现是Hotspot VM。

    二、Java语言的跨平台特性

    在这里插入图片描述


    一次编译,到处执行(Write Once ,Run Anywhere)。
    用Java创建的可执行二进制程序,能够不加改变的运行于多个平台。从软件层面屏蔽不同操作系统底层硬件与指令上的区别

    三、JVM整体结构及内存模型

    3.1 内存模型

    官方文档参考:[https://doc](https://doc)s.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5

    Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为个不同的数据区。这些区域有各自的用途,以及创建和销毁事件。
    JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据。

    在这里插入图片描述

    PC寄存器(线程私有)

    PC寄存器,也叫程序计数器。JVM支持多个线程同时运行,每个线程都有自己的程序计数器。倘若当前执行的是JVM方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native方法,则PC寄存器为空。
    这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。

    虚拟机栈(线程私有)

    每个线程有一个私有的栈,随着线程的创建而创建。栈里面存放着一种叫做“栈帧”的东西,每个方法在执行的时候会创建一个栈帧,存储了局部变量表(基本数据类型和对象引用),操作数栈,动态连接,方法出口等信息。
    每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。
    (方法中的局部变量的空间可以进行释放)
    通常所说的栈,一般是指虚拟机栈中的局部变量表部分。局部变量表所需的内存在编译期间完成分配。
    栈的大小可以固定也可以动态扩展,当扩展到无法申请足够的内存,则OutOfMemoryError。
    当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误
    演示栈帧:

    public class StackTest {
    
        public int method2(){
            int a=1;
            int b=2;
            int c=a+b;
            return c;
        }
    
        public int method1(){
           return method2();
        }
    
    
        public static void main(String[] args) {
            StackTest stackTest = new StackTest();
            int i = stackTest.method1();
            System.out.println(i);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    本地方法栈(线程私有)

    和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。 也会抛出StackOverflowError和OutOfMemoryError。

    堆(线程共享)

    堆内存是JVM所有线程共享的部分,在虚拟机启动的时候就已经创建。
    和程序开发密切相关,应用系统对象都保存在Java堆中。所有的对象和数组都在堆上进行分配。这部分空间可通过GC进行回收。对分代GC来说,堆也是分代的,是GC的主要工作区间。当申请不到空间时,会抛出OutOfMemoryError。
    演示内存溢出:

    public class HeapOutOfMemoryErrorTest {
        byte[] arr = new byte[1024 * 1000];//1M
    
        public static void main(String[] args) throws InterruptedException {
            ArrayList<HeapTest> list = new ArrayList<>();
            while (true) {
                list.add(new HeapTest());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    结果:

    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    	at com.zengqingfa.exercise.jvm.HeapTest.<init>(HeapTest.java:13)
    	at com.zengqingfa.exercise.jvm.HeapOutOfMemoryErrorTest.main(HeapOutOfMemoryErrorTest.java:17)
    
    • 1
    • 2
    • 3

    方法区(线程共享)

    方法区也是所有线程共享的。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
    这个区域的内存回收目标主要针对常量池的回收和对类型的卸载。
    当方法区无法满足内存分配需求时,则抛出OutOfMemoryError异常。
    在HotSpot虚拟机中,用永久代来实现方法区,将GC分代收集扩展至方法区,但是这样容易遇到内存溢出的问题。
    JDK1.7中,已经把放在永久代的字符串常量池移到堆中。
    JDK1.8撤销永久代,引入元空间。

    运行时常量池

    运行时常量池就是将编译后的类信息放入方法区中,也就是说它是方法区的一部分。** 运行时常量池用来动态获取类信息**,包括:class文件元信息描述、编译后的代码数据、引用类型数据、类文件常量池等。 运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中。

    在这里插入图片描述

    3.2 演示

    堆内存回收

    堆内存的回收过程,使用jvisualvm工具

    public class HeapTest {
    
        byte[] arr = new byte[1024 * 100];//100kb
    
        public static void main(String[] args) throws InterruptedException {
            ArrayList<HeapTest> list = new ArrayList<>();
            while (true) {
                list.add(new HeapTest());
                Thread.sleep(10);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    使用命令行:

    jvisualvm
    
    • 1

    图形界面:

    在这里插入图片描述


    没有VisualGC界面,可以使用插件,下载即可,重启

    在这里插入图片描述

    启动程序,查看程序的运行过程:

    在这里插入图片描述

    四、jvm内存参数设值

    在这里插入图片描述


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

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

    关于元空间的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。

    linux中jdk8 栈内存的默认大小:1M

    [root@k8s-node02 ~]#  java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
         intx CompilerThreadStackSize                  = 1024                                   {pd product} {default}
         intx ThreadStackSize                          = 1024                                   {pd product} {default}
         intx VMThreadStackSize                        = 1024                                   {pd product} {default}
    openjdk version "11.0.10" 2021-01-19 LTS
    OpenJDK Runtime Environment 18.9 (build 11.0.10+9-LTS)
    OpenJDK 64-Bit Server VM 18.9 (build 11.0.10+9-LTS, mixed mode, sharing)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    演示栈内存溢出:

    public class StackOverflowTest {
    
        //jvm设置 -Xss128k, -Xss默认1M     8倍   12548/8=1588
        static int count = 0;
    
        static void method() {
            count++;
            method();
        }
    
        public static void main(String[] args) {
            try {
                method();
            } catch (Throwable e) {
                e.printStackTrace();
                System.out.println(count);
    
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    运行结果:

    	java.lang.StackOverflowError
    	at com.zengqingfa.exercise.jvm.StackOverflowTest.method(StackOverflowTest.java:15)
    	at com.zengqingfa.exercise.jvm.StackOverflowTest.method(StackOverflowTest.java:16)
    	at com.zengqingfa.exercise.jvm.StackOverflowTest.method(StackOverflowTest.java:16)
      at com.zengqingfa.exercise.jvm.StackOverflowTest.method(StackOverflowTest.java:16)
    1098
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    **结论: -Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多 **
    JVM内存参数大小该如何设置? JVM参数大小设置并没有固定标准,需要根据实际项目情况分析

  • 相关阅读:
    Golang context.Context
    uniapp - 微信小程序 - 自定义底部tabbar
    Required field ‘client_protocol‘ is unset 原因探究
    微信小程序学习day01
    【状态机模型】大盗阿福 买卖股票IV V
    〖全域运营实战白宝书 - 运营角色认知篇④〗- 与运营打交道的小伙伴
    企业邮箱选择指南:最适合跨境贸易的解决方案推荐
    第二证券:机构策略:大盘有望继续走出震荡攀升走势
    最近公共祖先算法详解 + 模板题 建议新手收藏 信息学奥赛一本通 祖孙询问
    ESP32(VSCode+PlatformIO)开发环境搭建教程(2024版)
  • 原文地址:https://blog.csdn.net/zengqingfa123/article/details/127398739