• JVM学习之 内存结构


    一、引言

    1.什么是JVM?

    1. 定义:Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)
    2. 好处:
      • 一次编写,到处运行
      • 自动内存管理, 垃圾回收功能
      • 数组下标越界检查
      • 多态
    3. 比较jvm、jre、jdk
      在这里插入图片描述

    2.学习JVM有什么用

    • 理解底层的实现原理
    • 中高级程序员的必备技能

    3.常见的JVM

    在这里插入图片描述

    4.学习路线

    在这里插入图片描述

    二、内存结构

    在这里插入图片描述

    1. 程序计数器

    1.1 定义

    Program Counter Register 程序计数器(寄存器)
    在物理上:位于寄存器
    作用:是记住下一条jvm指令的执行地址
    特点:

    • 是线程私有的
    • 不会存在内存溢出

    1.2作用

    0: getstatic #20 // PrintStream out = System.out;
    3: astore_1 // --
    4: aload_1 // out.println(1);
    5: iconst_1 // --
    6: invokevirtual #26 // --
    9: aload_1 // out.println(2);
    10: iconst_2 // --
    11: invokevirtual #26 // --
    14: aload_1 // out.println(3);
    15: iconst_3 // --
    16: invokevirtual #26 // --
    19: aload_1 // out.println(4);
    20: iconst_4 // --
    21: invokevirtual #26 // --
    24: aload_1 // out.println(5);
    25: iconst_5 // --
    26: invokevirtual #26 // --
    29: return
    
    • 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
    • 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

    2. 虚拟机栈

    在这里插入图片描述

    2.1定义

    Java Virtual Machine Stacks (Java 虚拟机栈)

    • 每个线程运行时所需要的内存,称为虚拟机栈
    • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
    • 问题辨析
    1. 垃圾回收是否涉及栈内存?
      栈内存并不涉及垃圾回收,栈内存的产生就是方法一次一次调用产生的栈帧内存,而栈帧内存在每次方法被调用后都会被弹出栈,自动就被回收掉,不需要垃圾回收。来管理

    2. 栈内存分配越大越好吗?
      不是,在线程不多的情况下,栈内存分配大在递归时能提高运行速度,但他会影响线程的数目,从而影响到整个系统的运行速度

    3. 方法内的局部变量是否线程安全?
      如果方法内局部变量没有逃离方法的作用访问,它是线程安全的 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全,

      如果是共享的需要考虑线程安全,如果是私有的不用考虑线程安全

    2.2栈内存溢出

    • 栈帧过多导致栈内存溢出 ---->一般递归的时候容易出现
    • 栈帧内存过大导致栈内存溢出 --->不易出现
    • StackOverflowError 栈内存溢出异常
    • @JsonIgnore

    2.3线程运行诊断

    案例1:cpu占用过多
    定位

    • 用top定位哪个进程对cpu的占用过高
    • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
    • jstack 进程id
      可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号

    3. 本地方法栈

    一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。
    在这里插入图片描述

    为本地的方法提供一个运行的空间

    4. 堆

    在这里插入图片描述

    4.1定义

    Heap 堆

    • 通过 new 关键字,创建对象都会使用堆内存
      特点
    • 它是线程共享的,堆中对象都需要考虑线程安全的问题
    • 有垃圾回收机制

    4.2堆内存溢出

    1. jps 工具
      查看当前系统中有哪些 java 进程
    2. jmap 工具
      查看堆内存占用情况 jmap - heap 进程id
    3. jconsole 工具
      图形界面的,多功能的监测工具,可以连续监测
    4. jvisualvm 工具
      在这里插入图片描述

    5. 方法区

    5.1方法区

    在这里插入图片描述
    Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区。方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用 的特殊方法。

    方法区是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾收集或压缩它。本规范不要求方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小,也可以根据计算需要扩大,如果不需要更大的方法区域,可以缩小。方法区的内存不需要是连续的。

    Java 虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,以及在方法区域大小可变的情况下,对最大和最小方法区域大小的控制。

    以下异常情况与方法区相关:

    如果方法区域中的内存无法满足分配请求,Java 虚拟机将抛出一个OutOfMemoryError.
    JVM规范-方法区定义

    5.2组成

    在这里插入图片描述

    5.3方法区内存溢出

    • 1.8 以前会导致永久代内存溢出
      演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
      -XX:MaxPermSize=8m

    • 1.8 之后会导致元空间内存溢出
      演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
      -XX:MaxMetaspaceSize=8m
      场景:

          spring
          mybatis
    

    5.4 运行时常量池

    // 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
    public class Test {
        public static void main(String[] args) {
            System.out.println("hello world");
        }
    }
    

    然后使用 javap -v Test.class 命令反编译查看结果:
    在这里插入图片描述
    每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。
    在这里插入图片描述

    • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
      等信息
    • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量 池,并把里面的符号地址变为真实地址

    5.5 StringTable

    面试题:

            String s1 = "a";
            String s2 = "b";
            String s3 = "a" + "b"; // ab
            String s4 = s1 + s2;   // new String("ab")
            String s5 = "ab";
            String s6 = s4.intern();
    
    // 问
            System.out.println(s3 == s4); // false
            System.out.println(s3 == s5); // true
            System.out.println(s3 == s6); // true
    
            String x2 = new String("c") + new String("d"); // new String("cd")
            x2.intern();
            String x1 = "cd";
    
    // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
            System.out.println(x1 == x2);
                    // jdk1.6:
            // String x1 = "cd";            x2.intern();
            // x2.intern();  false          String x1 = "cd"; ture
    
            // jdk1.8:
            // String x1 = "cd";            x2.intern();
            // x2.intern();  false          String x1 = "cd"; ture
    

    练习:

    // StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
    public class Demo1_22 {
        // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
        // ldc #2 会把 a 符号变为 "a" 字符串对象
        // ldc #3 会把 b 符号变为 "b" 字符串对象
        // ldc #4 会把 ab 符号变为 "ab" 字符串对象
    
        public static void main(String[] args) {
            String s1 = "a"; // 懒惰的
            String s2 = "b";
            String s3 = "ab";
            String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
            String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
            System.out.println(s3 == s4);//s3是在串池中的,而s4则是在堆中,所有不相等
    
            System.out.println(s3 == s5);// true
        }
    }
    

    使用javap -v Demo1_22.class命令

    在这里插入图片描述

    5.6 StringTable的特性

    • 常量池中的字符串仅是符号,第一次用到时才变为对象
    • 利用串池的机制,来避免重复创建字符串对象
    • 字符串变量拼接的原理是 StringBuilder (1.8)
    • 字符串常量拼接的原理是编译期优化
    • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
      1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串 池中的对象的引用返回
      1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回

    5.7 StringTable 位置

    jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。
    在这里插入图片描述

    5.8 StringTable 垃圾回收

    -Xmx10m 指定堆内存大小
    -XX:+PrintStringTableStatistics 打印字符串常量池信息
    -XX:+PrintGCDetails
    -verbose:gc 打印 gc 的次数,耗费时间等信息

    演示StingTable垃圾回收:

    public static void main(String[] args) throws InterruptedException {
            int i = 0;
            try {
                for (int j = 0; j < 100000; j++) { // j=100, j=10000
                    String.valueOf(j).intern();
                    i++;
                }
            } catch (Throwable e) {
                e.printStackTrace();
            } finally {
                System.out.println(i);
            }
    
        }
    

    在这里插入图片描述

    5.9 StringTable 性能调优

    调整 -XX:StringTableSize=桶个数

    考虑将字符串对象是否入池

    6. 直接内存

    6.1 定义

    Direct Memory

    • 常见于 NIO 操作时,用于数据缓冲区

    (Java NIO 简介:
    Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。 NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作)

    • 分配回收成本较高,但读写性能高

    直接内存读取文件用的是byteBuffer进行读取文件,调用allocateDirect方法,建立一个直接内存,系统可以用,java代码也可以用

    • 不受 JVM 内存回收管理

    垃圾回收只能回收java内存
    回收直接内存需要通过主动调用Unsafe类中的freeMory()方法来释放内存的

    文件读写过程(IO):
    在这里插入图片描述

    6.2 使用直接内存的好处

    因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。

    使用了 DirectBuffer 文件读取流程:
    在这里插入图片描述
    直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

    6.2 直接内存回收的原理

    public class Demo1_9 {
        static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
        static final String TO = "E:\\a.mp4";
        static final int _1Mb = 1024 * 1024;
    
        public static void main(String[] args) {
            io(); // io 用时:1535.586957 1766.963399 1359.240226
            directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
        }
    
        private static void directBuffer() {
            long start = System.nanoTime();
            try (FileChannel from = new FileInputStream(FROM).getChannel();
                 FileChannel to = new FileOutputStream(TO).getChannel();
            ) {
                ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
                while (true) {
                    int len = from.read(bb);
                    if (len == -1) {
                        break;
                    }
                    bb.flip();
                    to.write(bb);
                    bb.clear();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            long end = System.nanoTime();
            System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
        }
    
        private static void io() {
            long start = System.nanoTime();
            try (FileInputStream from = new FileInputStream(FROM);
                 FileOutputStream to = new FileOutputStream(TO);
            ) {
                byte[] buf = new byte[_1Mb];
                while (true) {
                    int len = from.read(buf);
                    if (len == -1) {
                        break;
                    }
                    to.write(buf, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            long end = System.nanoTime();
            System.out.println("io 用时:" + (end - start) / 1000_000.0);
        }
    }
    
    

    直接内存的回收不是通过JVM的垃圾回收来实现的,而是手动通过Unsafe的freeMemory方法来释放的

    第一步 :allocateDirect的实现

        public static ByteBuffer allocateDirect(int capacity) {
            return new DirectByteBuffer(capacity);
        }
    

    观看代码可知,是创建了一个DirectByteBuffer的对象

    第二步,DircetByteBuffer类

        DirectByteBuffer(int cap) {                   // package-private
    
            super(-1, 0, cap, cap);
            boolean pa = VM.isDirectMemoryPageAligned();
            int ps = Bits.pageSize();
            long size = Math.max(1L, (long)cap + (pa ? ps : 0));
            Bits.reserveMemory(size, cap);
    
            long base = 0;
            try {
                base = unsafe.allocateMemory(size);
            } catch (OutOfMemoryError x) {
                Bits.unreserveMemory(size, cap);
                throw x;
            }
            unsafe.setMemory(base, size, (byte) 0);
            if (pa && (base % ps != 0)) {
                // Round up to page boundary
                address = base + ps - (base & (ps - 1));
            } else {
                address = base;
            }
            **cleaner = Cleaner.create(this, new Deallocator(base, size, cap));**
            att = null;
    
        }
    
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    

    这句代码是通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。
    这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。

        public void clean() {
            if (remove(this)) {
                try {
                    this.thunk.run();
                } catch (final Throwable var2) {
                    AccessController.doPrivileged(new PrivilegedAction() {
                        public Void run() {
                            if (System.err != null) {
                                (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                            }
    
                            System.exit(1);
                            return null;
                        }
                    });
                }
    
            }
        }
    

    可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法

    		public void run() {
                if (address == 0) {
                    // Paranoia
                    return;
                }
                // 释放内存
                unsafe.freeMemory(address);
                address = 0;
                Bits.unreserveMemory(size, capacity);
            }
    
    

    直接内存的回收机制总结

    • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
    • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一但ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory方法 来释放直接内存。
    /**
         * -XX:+DisableExplicitGC 显示的
         */
        private static void method() throws IOException {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
            System.out.println("分配完毕");
            System.in.read();
            System.out.println("开始释放");
            byteBuffer = null;
            System.gc(); // 手动 gc 失效
            System.in.read();
        }
    

    一般用 jvm 调优时,会加上下面的参数:

    -XX:+DisableExplicitGC  // 静止显示的 GC
    

    意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。

  • 相关阅读:
    说说你对js作用域的理解 相关题
    基于JavaWeb+MySQL的学院党费缴费系统
    SpringCloud & SpringCloud Alibaba基本介绍
    Vue 项目中用户登录及 token 验证的思路
    【TS04——接口的多态——泛型接口】
    网络安全-抓包和IP包头分析
    使用Tomcat搭建一个Servlet项目
    SpringBoot 整合 websocket (二)—— 部署Nginx\Tomcat
    一文读懂Embedding
    java设计模式---策略模式
  • 原文地址:https://www.cnblogs.com/beibei725/p/16557655.html