JVM系列2:内存结构

内存结构

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈
  4. 方法区
  5. 直接内存

1. 程序计数器

在这里插入图片描述

1.1 定义

Program Counter Register 程序计数器(寄存器)

  • 作用,是记住下一条jvm指令的执行地址
  • 特点
    • 是线程私有的
    • 不会存在内存溢出

1.2 作用

 //jvm指令
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

执行顺序:jvm指令->解释器->机器码->CPU执行

2. 虚拟机栈

在这里插入图片描述

2.1 定义

Java Virtual Machine Stacks (Java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题辨析

  1. 垃圾回收是否涉及栈内存? 不需要,栈帧内存在每次方法调用结束后会被函数栈自动回收

  2. 栈内存分配越大越好吗?不是,栈内存变大只是增大递归次数,但是却减少了线程数。例如,一个栈内存大小时为1M,物理内存有500M,就是500个线程,若占内存大小变为2M,线程数变为250个

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

    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的

      /**
       * 局部变量的线程安全问题
       */
      public class Demo1_18 {
      
          // 多个线程同时执行此方法 ,x是线程安全的,因为x是私有的变量
          static void m1() {
              int x = 0;
              for (int i = 0; i < 5000; i++) {
                  x++;
              }
              System.out.println(x);
          }
      }
      
    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

      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() { //线程安全的,sb是私有的
              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;//线程不安全,存在返回值,逃离方法的作用范围
          }
      }
      

2.2 栈内存溢出

  • 栈帧过多导致栈内存溢出

    常见场景:递归

  • 栈帧过大导致栈内存溢出(很少见)

    /**
     * json 数据转换
     */
    public class Demo1_19 {
    
        public static void main(String[] args) throws JsonProcessingException {
            Dept d = new Dept();
            d.setName("Market");
    
            Emp e1 = new Emp();
            e1.setName("zhang");
            e1.setDept(d);
    
            Emp e2 = new Emp();
            e2.setName("li");
            e2.setDept(d);
    
            d.setEmps(Arrays.asList(e1, e2));
    
            // { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
            ObjectMapper mapper = new ObjectMapper();
            System.out.println(mapper.writeValueAsString(d));
        }
    }
    
    class Emp {
        private String name;
        @JsonIgnore //如果不加这个注解就会导致栈溢出
        private Dept dept;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Dept getDept() {
            return dept;
        }
    
        public void setDept(Dept dept) {
            this.dept = dept;
        }
    }
    class Dept {
        private String name;
        private List<Emp> emps;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public List<Emp> getEmps() {
            return emps;
        }
    
        public void setEmps(List<Emp> emps) {
            this.emps = emps;
        }
    

2.3 线程运行诊断

案例1: cpu 占用过多

定位

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

案例2:程序运行很长时间没有结果

死锁案例:使用jstack pid 查看

class A{};
class B{};
public class Demo1_3 {
    static A a = new A();
    static B b = new B();


    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (a) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (b) {
                synchronized (a) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
    }

}

3. 本地方法栈

在这里插入图片描述

当程序在调用一些底层的方法时会用到本地方法栈。

4. 堆

在这里插入图片描述

4.1 定义

Heap 堆

  • 通过 new 关键字,创建对象都会使用堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

4.2 堆内存溢出

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * 当我们在进行程序调试的时候可以修改我们的堆空间的大小,来查看空间是否存在溢出现象
 * -Xmx8m
 */
public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

4.3 堆内存诊断

  1. jps 工具

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

      F:\视频-解密JVM\资料 解密JVM\代码\jvm\jvm>jps
      13028 Jps
      13060
      11080 Demo1_4
      2984 RemoteMavenServer
      2012 Launcher
      
      
  2. jmap 工具

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

      F:\视频-解密JVM\资料 解密JVM\代码\jvm\jvm>jmap -heap 2944
      Attaching to process ID 2944, please wait...
      Debugger attached successfully.
      Server compiler detected.
      JVM version is 25.131-b11
      
      using thread-local object allocation.
      Parallel GC with 4 thread(s)
      
      Heap Configuration:
         MinHeapFreeRatio         = 0
         MaxHeapFreeRatio         = 100
         MaxHeapSize              = 2095054848 (1998.0MB)
         NewSize                  = 44040192 (42.0MB)
         MaxNewSize               = 698351616 (666.0MB)
         OldSize                  = 88080384 (84.0MB)
         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 = 33554432 (32.0MB)
         used     = 13878224 (13.235305786132812MB)
         free     = 19676208 (18.764694213867188MB)
         41.36033058166504% used
      From Space:
         capacity = 5242880 (5.0MB)
         used     = 0 (0.0MB)
         free     = 5242880 (5.0MB)
         0.0% used
      To Space:
         capacity = 5242880 (5.0MB)
         used     = 0 (0.0MB)
         free     = 5242880 (5.0MB)
         0.0% used
      PS Old Generation
         capacity = 88080384 (84.0MB)
         used     = 0 (0.0MB)
         free     = 88080384 (84.0MB)
         0.0% used
      
      1794 interned Strings occupying 180672 bytes
      
  3. jconsole 工具

    • 图形界面的,多功能的监测工具,可以连续监测
      在这里插入图片描述

4.使用jvisualvm工具进行内存诊断

案例

  • 垃圾回收后,内存占用仍然很高

    /**
     * 演示查看对象个数 堆转储 dump,然后分析堆中对象的占用情况
     */
    public class Demo1_13 {
    
        public static void main(String[] args) throws InterruptedException {
            List<Student> students = new ArrayList<>();
            for (int i = 0; i < 200; i++) {
                students.add(new Student());
    //            Student student = new Student();
            }
            Thread.sleep(1000000000L);
        }
    }
    class Student {
        private byte[] big = new byte[1024*1024];
    }
    

在这里插入图片描述

5. 方法区

在这里插入图片描述

5.1 定义

JVM规范-方法区定义

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

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

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

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

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

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

The following exceptional condition is associated with the method area:

以下例外情况与方法区域相关:

If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

如果方法区域中的内存无法满足分配请求,Java虚拟机将抛出OutOfMemoryError。

5.2 组成

在这里插入图片描述

在这里插入图片描述

这两个图片里的常量池应该写为运行时常量池。

5.3 方法区内存溢出

  • 1.8 以前会导致永久代内存溢出

    * 演示永久代内存溢出  java.lang.OutOfMemoryError: PermGen space
    * -XX:MaxPermSize=8m
    
  • 1.8 之后会导致元空间内存溢出

    /**
     * 演示元空间内存溢出 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);
            }
        }
    }
    
    

场景

  1. spring

  2. mybatis

    在这里面会用到动态代理,其就会记载很多的类

5.4 运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

    实例,一段简单的HelloWrold程序反编译后的结果

    // 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
    public class HelloWorld {
        public static void main(String[] args) {
            System.out.println("hello world");
        }
    }
    
    F:\视频-解密JVM\资料 解密JVM\代码\jvm\jvm\src\cn\itcast\jvm\t5>javap -v HelloWorld.class
    Classfile /F:/视频-解密JVM/资料 解密JVM/代码/jvm/jvm/src/cn/itcast/jvm/t5/HelloWorld.class
      Last modified 2020-4-26; size 442 bytes
      MD5 checksum 103606e24ec918e862312533fda15bbc
      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.#15         // java/lang/Object."<init>":()V
       #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #18            // hello world
       #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #21            // cn/itcast/jvm/t5/HelloWorld
       #6 = Class              #22            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               main
      #12 = Utf8               ([Ljava/lang/String;)V
      #13 = Utf8               SourceFile
      #14 = Utf8               HelloWorld.java
      #15 = NameAndType        #7:#8          // "<init>":()V
      #16 = Class              #23            // java/lang/System
      #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
      #18 = Utf8               hello world
      #19 = Class              #26            // java/io/PrintStream
      #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
      #21 = Utf8               cn/itcast/jvm/t5/HelloWorld
      #22 = Utf8               java/lang/Object
      #23 = Utf8               java/lang/System
      #24 = Utf8               out
      #25 = Utf8               Ljava/io/PrintStream;
      #26 = Utf8               java/io/PrintStream
      #27 = Utf8               println
      #28 = 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
    
      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
             //jvm的指令
             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
    }
    SourceFile: "HelloWorld.java"
    
    
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

5.5 StringTable和常量池的关系

将下列代码进行反编译后查看结果

F:\视频-解密JVM\资料 解密JVM\代码\jvm\jvm\src\cn\itcast\jvm\t1\stringtable>javap -v Demo1_22.class
Classfile /F:/视频-解密JVM/资料 解密JVM/代码/jvm/jvm/src/cn/itcast/jvm/t1/stringtable/Demo1_22.class
  Last modified 2020-4-26; size 776 bytes
  MD5 checksum 5a4bdb3760a8a1b90340650b6651d07f
  Compiled from "Demo1_22.java"
public class cn.itcast.jvm.t1.stringtable.Demo1_22
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#25        // java/lang/Object."<init>":()V
   #2 = String             #26            // a
   #3 = String             #27            // b
   #4 = String             #28            // ab
   #5 = Class              #29            // java/lang/StringBuilder
   #6 = Methodref          #5.#25         // java/lang/StringBuilder."<init>":()V
   #7 = Methodref          #5.#30         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #5.#31         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Fieldref           #32.#33        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Methodref          #34.#35        // java/io/PrintStream.println:(Z)V
  #11 = Class              #36            // cn/itcast/jvm/t1/stringtable/Demo1_22
  #12 = Class              #37            // java/lang/Object
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               StackMapTable
  #20 = Class              #38            // "[Ljava/lang/String;"
  #21 = Class              #39            // java/lang/String
  #22 = Class              #40            // java/io/PrintStream
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo1_22.java
  #25 = NameAndType        #13:#14        // "<init>":()V
  #26 = Utf8               a
  #27 = Utf8               b
  #28 = Utf8               ab
  #29 = Utf8               java/lang/StringBuilder
  #30 = NameAndType        #41:#42        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #31 = NameAndType        #43:#44        // toString:()Ljava/lang/String;
  #32 = Class              #45            // java/lang/System
  #33 = NameAndType        #46:#47        // out:Ljava/io/PrintStream;
  #34 = Class              #40            // java/io/PrintStream
  #35 = NameAndType        #48:#49        // println:(Z)V
  #36 = Utf8               cn/itcast/jvm/t1/stringtable/Demo1_22
  #37 = Utf8               java/lang/Object
  #38 = Utf8               [Ljava/lang/String;
  #39 = Utf8               java/lang/String
  #40 = Utf8               java/io/PrintStream
  #41 = Utf8               append
  #42 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #43 = Utf8               toString
  #44 = Utf8               ()Ljava/lang/String;
  #45 = Utf8               java/lang/System
  #46 = Utf8               out
  #47 = Utf8               Ljava/io/PrintStream;
  #48 = Utf8               println
  #49 = Utf8               (Z)V
{
  public cn.itcast.jvm.t1.stringtable.Demo1_22();
    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

  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/StringBuil
der;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuil
der;
        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
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 6
        line 11: 9
        line 12: 29
        line 14: 33
        line 18: 50
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 46
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class
java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class
java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
}
SourceFile: "Demo1_22.java"

常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象,只有当执行到相应的jvm指令是才会将相应符号变为字符串对象

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象,只有当执行到相应的jvm指令是才会将相应符号变为字符串对象
    // 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);

    }
}

5.6 StringTable 特性

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

5.7 StringTable面试题

public class StringTableQuestion {
    //[a,b,,ab,c,d,cd]                               StringTable
    //堆 s4(ab) StringBuilder.append  x2(cd)
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b";
        String s4 = s1 + s2;
        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");
        String x1 = "cd";
        x2.intern();
        System.out.println(x1 == x2);//false
    }
}

答案是false,true,true,false

首先程序开始运行,s1,s2依次入池,s3会在编译期进行优化,入池,s4两个变量进行拼接,程序中会调用StringBuilder在底层进行拼接,所以S4在堆中,因为池中已经存在了“ab”,所以s5不在入池,接着执行s4.intern(),因为池中已经存在了“ab",s4还是在堆中,s6返回的是串池中的对象,锁以接下来的三个结果就是false,true,true。

程序接着往下执行,在串池中依次放入c,b。x2对象在堆中。接着x1放入串池中,因为串池中已经存在cd,所以x2没有放入串池中,依然在堆中。所以接下来回输出false。

5.8 StringTable 位置

1.6是在永久代中,1.8时在堆中。见上图。

5.9 StringTable 垃圾回收

/**
 * 演示 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);
        }

    }
}

[GC (Allocation Failure) [PSYoungGen: 2046K->488K(2560K)] 2046K->742K(9728K), 0.0024306 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2790K->782K(9728K), 0.0017411 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2830K->798K(9728K), 0.0022217 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
100000
Heap
 PSYoungGen      total 2560K, used 1965K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 72% used [0x00000000ffd00000,0x00000000ffe71628,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 310K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 4% used [0x00000000ff600000,0x00000000ff64d8c0,0x00000000ffd00000)
 Metaspace       used 3522K, capacity 4504K, committed 4864K, reserved 1056768K
  class space    used 390K, capacity 392K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     14205 =    340920 bytes, avg  24.000
Number of literals      :     14205 =    627672 bytes, avg  44.187
Total footprint         :           =   1128680 bytes
Average bucket size     :     0.710
Variance of bucket size :     0.714
Std. dev. of bucket size:     0.845
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :     28122 =    674928 bytes, avg  24.000
Number of literals      :     28122 =   1655160 bytes, avg  58.856
Total footprint         :           =   2810192 bytes
Average bucket size     :     0.469
Variance of bucket size :     0.441
Std. dev. of bucket size:     0.664
Maximum bucket size     :         4

Process finished with exit code 0

从打印的信息中我们可以看到了进行垃圾回收

5.10 StringTable 性能调优

  • 调整 -XX:StringTableSize=桶个数
/**
 * 演示串池大小对性能的影响
 * -Xms 为jvm启动时分配的内存,比如-Xms200m,表示分配200M

 -Xmx 为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存

 -Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Demo1_24 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }


    }
}
  • 考虑将字符串对象是否入池
/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());//入池可以减少占用时间
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

6. 直接内存

6.1 定义

Direct Memory

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理
public class Demo1_9 {
    static final String FROM = "F:\\12.mp4";
    static final String TO = "F:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); 
        directBuffer();
    }

    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);
    }
}

使用io读写时的原理:

在这里插入图片描述

使用NIO读写时原理:

在这里插入图片描述

6.2 分配和回收原理

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来

结尾语:首先我写的jvm系列是在一个本人认为一个很好的教jvm的视频教程的笔记的基础上改的,加入了自己的一些修改和理解,由于是初学JVM,可能某些方面写的不对,请积极指教。如果觉得我改的烂,可以路过,不喜勿喷,写博客只是为了记录自己学习中的一些问题。附上个人认为不错的教程地址。
https://www.bilibili.com/video/BV1yE411Z7AP?from=search&seid=2688930742873346466

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值