• JVM 内存结构


    一、前言

    1.1、什么是 JVM ?

    1)定义
    Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。
    2)好处

    •     一次编译,处处执行
    •     自动的内存管理,垃圾回收机制
    •     数组下标越界检查

    3)比较
    JVM、JRE、JDK 的关系如下图所示

    1.2、学习 JVM 有什么用?

        面试必备
        中高级程序员必备
        想走的长远,就需要懂原理,比如:自动装箱、自动拆箱是怎么实现的,反射是怎么实现的,垃圾回收机制是怎么回事等待,JVM 是必须掌握的。

    1.3、常见的 JVM

    一套规范,可以自己实现jmv的

    我们主要学习的是 HotSpot 版本的虚拟机。

    HotSpot VM是Sun JDK和OpenJDK中所带的虚拟机。

    1.4、学习路线

    ClassLoader:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
    Method Area:类是放在方法区中。
    Heap:类的实例对象。
    当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。

    方法执行时的每行代码是有执行引擎中的解释器逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口(调用操作系统方法)。

    二、内存结构

    2.1、程序计数器

    1)定义
    Program Counter Register 程序计数器(寄存器)
    作用:是记录下一条 jvm 指令的执行地址行号。
    特点:

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

    2)作用

    计数器是java对物理硬件(寄存器)的屏蔽和抽象

    解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。

    多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

    2.2、虚拟机栈

    1)定义

    每个线程运行需要的内存空间,称为虚拟机栈

    每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存

    每个线程只能有一个活动栈帧,对应着当前正在执行的方法

    栈顶的那个栈帧,调用一次方法,把方法的栈帧放入栈,方法执行完,弹出栈帧;方法调用方法,在放入另一个栈帧。

    问题辨析:

    垃圾回收是否涉及栈内存?

    不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。

    栈内存分配越大越好吗?
    不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。因为一个线程对应一个栈,即栈是线程私有的,所以栈大,那么栈数目少,线程数就少。

    方法的局部变量是否线程安全?

    如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
    如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题(函数参数、 函数返回值等的情况)。

    public class Demo1_17 {
        public static void main(String[] args) {
            StringBuilder sb = new StringBuilder();
            sb.append(4);
            sb.append(5);
            sb.append(6);
            new Thread(()->{
                m2(sb);
            }).start();
        }
    
        public static void m1() {
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb.toString());
        }
    
        public static void m2(StringBuilder sb) {
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb.toString());
        }
    
        public static StringBuilder m3() {
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            return sb;
        }
    }

    m1线程安全,私有的引用局部变量

    m2线程不安全,sb是方法参数传递的,说明与其他线程共享

    m3线程不安全,作为返回值,也共享了


    2)栈内存溢出

    栈帧过大(局部变量一般占用内存比较少,不太容易出现)、

    过多(方法调用太多,且没有返回,递归没有终止)、

    或者第三方类库操作(两个类的循环引用,json循环依赖),

    都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!

    3)线程运行诊断
    案例一:cpu 占用过多
    解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

    top 命令,查看是哪个进程占用 CPU 过高
    ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高

    jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

    2.3、本地方法栈

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

    Object中有很多本地方法,clone/wait/..

    2.4、堆

    1)定义

    Heap 堆

        通过new关键字创建的对象都会被放在堆内存

    特点

    •     它是线程共享,堆内存中的对象都需要考虑线程安全问题
    •     有垃圾回收机制,堆中不再引用的对象会被释放内存


    2)堆内存溢出

    java.lang.OutofMemoryError :java heap space. 堆内存溢出,堆中的对象太多,也没被回收
    可以使用 -Xmx8m 来指定堆内存大小(大小指定8M)。

    先是将hello对象创建堆,将对象引用加入list集合

    然后不断做字符串拼接,将hello*****对象创建堆,将对象引用加入list集合

    。。。。。死循环,对象也无法回收,爆了。

    3)堆内存诊断

    •     jps 工具

    查看当前系统中有哪些 java 进程

    •     jmap 工具

        查看堆内存占用情况 jmap - heap 进程id

    •     jconsole 工具

        图形界面的,多功能的监测工具,可以连续监测

    •     jvisualvm 工具

    先运行演示堆内存的程序

    /**
     * 演示堆内存
     */
    public class Demo1_4 {
    
        public static void main(String[] args) throws InterruptedException {
            System.out.println("1...");
            Thread.sleep(30000);
            byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
            System.out.println("2...");
            Thread.sleep(20000);
            array = null;
            System.gc();
            System.out.println("3...");
            Thread.sleep(1000000L);
        }
    }

     idea Terminal中运行jps

    查看当前系统中有哪些 java 进程

    I:网课资料资料-解密JVM代码jvm>jps
    22080 Jps
    21556
    23380 Demo1_4
    5812 RemoteMavenServer36
    8460 Launcher

    查看堆内存占用情况

    内存快照信息

    I:网课资料资料-解密JVM代码jvm>jmap -heap 23380
    Attaching to process ID 23380, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.231-b11
    
    using thread-local object allocation.
    Parallel GC with 8 thread(s)
    
    Heap Configuration:
       MinHeapFreeRatio         = 0
       MaxHeapFreeRatio         = 100
       MaxHeapSize              = 4261412864 (4064.0MB)
       NewSize                  = 88604672 (84.5MB)
       MaxNewSize               = 1420296192 (1354.5MB)
       OldSize                  = 177733632 (169.5MB)
       NewRatio                 = 2
       SurvivorRatio            = 8
       MetaspaceSize            = 21807104 (20.796875MB)
       CompressedClassSpaceSize = 1073741824 (1024.0MB)
       MaxMetaspaceSize         = 17592186044415 MB
       G1HeapRegionSize         = 0 (0.0MB)
    
    Heap Usage:
    PS Young Generation
    Eden Space:
       capacity = 66584576 (63.5MB)
       used     = 17145656 (16.35137176513672MB)
       free     = 49438920 (47.14862823486328MB)
       25.750191756120817% used
    From Space:
       capacity = 11010048 (10.5MB)
       used     = 0 (0.0MB)
       free     = 11010048 (10.5MB)
       0.0% used
    To Space:
       capacity = 11010048 (10.5MB)
       used     = 0 (0.0MB)
       free     = 11010048 (10.5MB)
       0.0% used
    PS Old Generation
       capacity = 177733632 (169.5MB)
       used     = 0 (0.0MB)
       free     = 177733632 (169.5MB)
       0.0% used
    
    3170 interned Strings occupying 280952 bytes.

    jconsole 工具

    idea Terminal中运行jconsole

    可以看到堆内存空间先增后降 符合代码

    2.5、方法区

    2.5.1 定义

    Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域

    方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。

    它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和接口的实例初始化。

    方法区域是在虚拟机启动时创建的。

    尽管方法区在逻辑上是堆的一部分(不同厂商实现不一样,HotSpots 1.8前是永久代,堆的一部分,1.8时把永久代移除了,元空间,本地系统内存),但简单的实现可能不会选择垃圾收集或压缩它。

    方法区是规范,什么永久代、元空间是实现。

    此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。

    方法区域的内存不需要是连续的!


    2.5.2 组成

    Hotspot 虚拟机 jdk1.6 1.7 1.8 内存结构图

    ClassLoader用来加载类的字节码。

    2.5.3 方法区内存溢出

        1.8 之前会导致永久代内存溢出
            使用 -XX:MaxPermSize=8m 指定永久代内存大小
        1.8 之后会导致元空间内存溢出
            使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

    演示内存溢出

    import jdk.internal.org.objectweb.asm.ClassWriter;
    import jdk.internal.org.objectweb.asm.Opcodes;
    
    /**
     * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
     * -XX:MaxMetaspaceSize=8m
     */
    public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
        public static void main(String[] args) {
            int j = 0;
            try {
                Demo1_8 test = new Demo1_8();
                for (int i = 0; i < 10000; i++, j++) {
                    // ClassWriter 作用是生成类的二进制字节码
                    ClassWriter cw = new ClassWriter(0);
                    // 定义类
                    // 版本号, public, 类名, 包名, 父类, 接口
                    cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                    // 返回 byte[]
                    byte[] code = cw.toByteArray();
                    // 执行了类的加载
                    test.defineClass("Class" + i, code, 0, code.length); // Class 对象
                }
            } finally {
                System.out.println(j);
            }
        }
    }

    2.5.4 运行时常量池

    运行一段程序,将程序编译为二进制字节码:

    二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令
    首先看看常量池是什么,编译如下代码:

    public class HelloWorld {
        public HelloWorld() {
        }
    
        public static void main(String[] args) {
            System.out.println("hello world");
        }
    }

    然后使用 javap -v Test.class 命令反编译查看结果。

    Classfile /I:/网课资料/资料-解密JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
      Last modified 2021-9-24; size 567 bytes
      MD5 checksum 8efebdac91aa496515fa1c161184e354
      Compiled from "HelloWorld.java"
    public class cn.itcast.jvm.t5.HelloWorld
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
       #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #23            // hello world
       #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #26            // cn/itcast/jvm/t5/HelloWorld
       #6 = Class              #27            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lcn/itcast/jvm/t5/HelloWorld;
      #14 = Utf8               main
      #15 = Utf8               ([Ljava/lang/String;)V
      #16 = Utf8               args
      #17 = Utf8               [Ljava/lang/String;
      #18 = Utf8               SourceFile
      #19 = Utf8               HelloWorld.java
      #20 = NameAndType        #7:#8          // "<init>":()V
      #21 = Class              #28            // java/lang/System
      #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
      #23 = Utf8               hello world
      #24 = Class              #31            // java/io/PrintStream
      #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
      #26 = Utf8               cn/itcast/jvm/t5/HelloWorld
      #27 = Utf8               java/lang/Object
      #28 = Utf8               java/lang/System
      #29 = Utf8               out
      #30 = Utf8               Ljava/io/PrintStream;
      #31 = Utf8               java/io/PrintStream
      #32 = Utf8               println
      #33 = Utf8               (Ljava/lang/String;)V
    {
      public cn.itcast.jvm.t5.HelloWorld();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 4: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcn/itcast/jvm/t5/HelloWorld;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3                  // String hello world
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 6: 0
            line 7: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
    }
    SourceFile: "HelloWorld.java"

    其中Constant pool那部分是常量池表

    Constant pool:
       #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
       #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #23            // hello world
       #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #26            // cn/itcast/jvm/t5/HelloWorld
       #6 = Class              #27            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lcn/itcast/jvm/t5/HelloWorld;
      #14 = Utf8               main
      #15 = Utf8               ([Ljava/lang/String;)V
      #16 = Utf8               args
      #17 = Utf8               [Ljava/lang/String;
      #18 = Utf8               SourceFile
      #19 = Utf8               HelloWorld.java
      #20 = NameAndType        #7:#8          // "<init>":()V
      #21 = Class              #28            // java/lang/System
      #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
      #23 = Utf8               hello world
      #24 = Class              #31            // java/io/PrintStream
      #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
      #26 = Utf8               cn/itcast/jvm/t5/HelloWorld
      #27 = Utf8               java/lang/Object
      #28 = Utf8               java/lang/System
      #29 = Utf8               out
      #30 = Utf8               Ljava/io/PrintStream;
      #31 = Utf8               java/io/PrintStream
      #32 = Utf8               println
      #33 = Utf8               (Ljava/lang/String;)V

    每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。

    Code后是jvm指令,指令地址   操作方式  常量池对应地址

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

    2.5.5 StringTable

    String table又称为String pool,字符串常量池,其存在于堆中(jdk1.7之后改的)。最重要的一点,String table中存储的并不是String类型的对象,存储的而是指向String对象的索引,真实对象还是存储在堆中。

    此外String table还存在一个hash表的特性,里面不存在相同的两个字符串。

    此外String对象调用intern()方法时,会先在String table中查找是否存在于该对象相同的字符串,若存在直接返回String table中字符串的引用,若不存在则在String table中创建一个与该对象相同的字符串。

    // 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 == s5);
            
        }
    }

    反编译

    public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=6, args_size=1
             0: ldc           #2                  // String a
             2: astore_1
             3: ldc           #3                  // String b
             5: astore_2
             6: ldc           #4                  // String ab
             8: astore_3
             9: new           #5                  // class java/lang/StringBuilder
            12: dup
            13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
            16: aload_1
            17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            20: aload_2
            21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            27: astore        4
            29: ldc           #4                  // String ab
            31: astore        5
            33: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
            36: aload_3
            37: aload         5
            39: if_acmpne     46
            42: iconst_1
            43: goto          47
            46: iconst_0
            47: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
            50: return

    常量池中的字符串仅是符号,只有在被用到时才会将符号转化为对象(懒汉),放入StringTable,放入时会先在StringTable中查找,如果对象存在就无法放入,不存在放入,最后返回串池中对象。
        利用串池的机制,来避免重复创建字符串对象
        字符串变量拼接的原理是StringBuilder(线程安全,效率低)

    String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")

    s5==s3 true
        字符串常量拼接的原理是编译器优化,s5是常量,去常量池中查找,还特么找到了, 常量是确定,可以在编译期间确定为ab,而引用相加不确定,只能运行时确定
        可以使用方法,主动将串池中还没有的字符串对象放入串池中
    (懒汉行为,延迟实例化,遇到一个常量,用时将常量池符号变对象,再放入StringTable)

    intern方法 1.8
    调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池StringPooling中

    •     如果串池中没有该字符串对象,则放入成功
    •     如果有该字符串对象,则放入失败
    •     无论放入是否成功,都会返回串池中的字符串对象


    注意:此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

    例1:

    public class Main {
        public static void main(String[] args) {
            // "a" "b" 被放入串池中,str 则存在于堆内存之中
            String str = new String("a") + new String("b");
            // 调用 str 的 intern 方法,这时串池中没有 "ab" ,则会将该字符串对象放入到串池中,此时堆内存与串池中的 "ab" 是同一个对象
            String st2 = str.intern();
            // 给 str3 赋值,因为此时串池中已有 "ab" ,则直接将串池中的内容返回
            String str3 = "ab";
            // 因为堆内存与串池中的 "ab" 是同一个对象,所以以下两条语句打印的都为 true
            System.out.println(str == st2);//true
            System.out.println(str == str3);//true
        }
    }

    例2:

    public class Demo1_23 {
    
        //  ["ab", "a", "b"]
        public static void main(String[] args) {
    
            String x = "ab";
            //此处创建字符串对象 "ab" ,因为串池中还没有 "ab" ,所以将其放入串池中
            String s = new String("a") + new String("b");
            // "a" "b" 被放入串池中,s则存在于堆内存之中
            String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
            // 此时因为在创建x时,"ab" 已存在与串池中,所以放入失败,但是会返回串池中的 "ab"
            System.out.println( s2 == x); // true
            System.out.println( s == x ); // false
        }
    
    }

    当java1.6时  当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象拷贝添加到字符串常量池中,并且返回该字符串对象的引用。

    1.8不拷贝,1.6要拷贝,当常量池无对象时,1.8返回的引用和堆引用一样,因为放入的是引用不是拷贝,而1.6则是常量池引用,放入的是拷贝

    面试题

    /**
     * 演示字符串相关面试题
     */
    public class Demo1_21 {
    
        public static void main(String[] args) {
            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);
        }
    }

    1.8 x2==x1  false

    1.6 x2==x1  false

    串池中已经存在“cd”了,x2不会再放入串池 x2的“cd”存在于堆中

    1.8  x2==x1  true   x2一开始堆,然后将其应用放入StringPooling,x1放入后得到返回引用和x2引用一样

    1.6  x2==x1  false  x2一开始堆,然后拷贝对象放入StringPooling,x1放入后得到返回引用和x2(此时的x2还是之前的 没有更新)不一样

    2.5.6 StringTable 的位置

    jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

    因为永久代的回收效率很低,永久代只有fullGC的时候才会垃圾回收
    堆中只需要minGC就可以垃圾回收,大大减少String常量对内存的占用

    /**
     * 演示 StringTable 位置
     * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
     * 在jdk6下设置 -XX:MaxPermSize=10m
     */
    public class Demo1_6 {
    
        public static void main(String[] args) throws InterruptedException {
            List<String> list = new ArrayList<String>();
            int i = 0;
            try {
                for (int j = 0; j < 260000; j++) {
                    list.add(String.valueOf(j).intern());
                    i++;
                }
            } catch (Throwable e) {
                e.printStackTrace();
            } finally {
                System.out.println(i);
            }
        }
    }

    实验 对比1.6和1.8StringPool位置

    设置永久代参数,内存大小

    花了98%的时间进行垃圾回收,但是垃圾回收不足2%,说明救不活了!哈哈哈 ,直接报堆溢出

    2.5.7 StringTable 垃圾回收

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

    /**
     * 演示 StringTable 垃圾回收
     * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
     */
    public class Demo1_7 {
        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);
            }
    
        }
    }

    堆空间

     

    内存不足,触发一次垃圾回收,垃圾回收速度很快,

    新生代的垃圾回收快

    2.5.8 StringTable 性能调优

        * 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶(对象数组长度)的个数,减少hash碰撞的可能性,链的长度较短,来减少字符串放入串池所需要的时间,哈希桶的长度太小的话,如果String常量对象很多,哈希碰撞更严重,链表插入、扩容、红黑树费时

       *  考虑是否需要将字符串对象入池
       *  可以通过 intern 方法减少重复入池,不同对象(相同)指向池中同一String

    设置桶的长度:

     -XX:StringTableSize=桶个数(最少设置为 1009 以上)

    2.6、直接内存

    2.6.1 定义

    Direct Memory -----是操作系统的内存  ---java和系统都可以访问,避免了内存重复

    •     常见于 NIO 操作时,用于数据缓冲区
    •     分配回收成本较高,但读写性能高
    •     不受 JVM 内存回收管理

    2.6.2 使用直接内存的好处

    文件读写流程:

    java本身不具备磁盘读写的能力,需要调用操作系统的方法,本地方法--CPU状态由用户态(java)切换到内核态(System);

    缓存,分次读取

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

    使用了 DirectBuffer 文件读取流程

    直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。磁盘文件读取到直接内存后,可以让java直接访问,少了缓冲区的copy操作,所以高效,内存不浪费。

    2.6.3 直接内存回收原理


    1.直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。

    2.ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过通过Clean方法调用unsafe.freeMemory 是(守护线程)来释放内存

    直接内存的分配:ByteBuffer.allocateDirect();

    /**
     * 禁用显式回收对直接内存的影响
     */
    public class Demo1_26 {
        static int _1Gb = 1024 * 1024 * 1024;
    
        /*
         * -XX:+DisableExplicitGC 显式的
         */
        public static void main(String[] args) throws IOException {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
            System.out.println("分配完毕...");
            System.in.read();
            System.out.println("开始释放...");
            byteBuffer = null;
            System.gc(); // 显式的垃圾回收,Full GC
            System.in.read();
        }
    }

    这里的直接内存被释放,不是因为GC,因为JVM管不了

    但是,有虚引用

    public class Code_06_DirectMemoryTest {
    
        public static int _1GB = 1024 * 1024 * 1024;
    
        public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
    //        method();
            method1();
        }
    
        // 演示 直接内存 是被 unsafe 创建与回收
        private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {
    
            Field field = Unsafe.class.getDeclaredField("theUnsafe");//用反射拿到unsafe对象
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
    
    //分配内存,,用unsafe分配的内存,由unsafe对象方法释放掉
    
            long base = unsafe.allocateMemory(_1GB);
            unsafe.setMemory(base,_1GB, (byte)0);
            System.in.read();
    
    //释放内存
    
            unsafe.freeMemory(base);
            System.in.read();
        }
    
        // 演示 直接内存被 释放
        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 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
    第一步:allocateDirect 的实现

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

    底层是创建了一个 DirectByteBuffer 对象。
    第二步:DirectByteBuffer 类

    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)); 
    // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。 att = null; }

    这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。

     public void clean() {
            if (remove(this)) {
                try {
                // 都用函数的 run 方法, 释放内存
                    this.thunk.run();
                } catch (final Throwable var2) {
                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
                        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);
            }

    注意:

    /**
         * -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 的方式释放内存。
     

    作者:王陸

    -------------------------------------------

    个性签名:罔谈彼短,靡持己长。做一个谦逊爱学的人!

    本站使用「署名 4.0 国际」创作共享协议,转载请在文章明显位置注明作者及出处。鉴于博主处于考研复习期间,有什么问题请在评论区中提出,博主尽可能当天回复,加微信好友请注明原因

  • 相关阅读:
    改变字段的值
    创建新的对象
    根据方法的名称来执行方法
    获取类的字段
    获取构造器的信息
    找出类的方法
    开始使用Reflection
    反射简介
    leetcode501
    leetcode235
  • 原文地址:https://www.cnblogs.com/wkfvawl/p/15319916.html
Copyright © 2020-2023  润新知