解密JVM(二)内存结构

前言:

1.程序计数器

image-20201220201952243

1.1 定义

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

1.2 作用

  • 记住下一条JVM指令的执行地址

  • 物理上,通过寄存器实现

1.3 特点

  • 线程私有
    • CPU会为每个线程分配时间片,当当前线程的时间片使用完以后,CPU就会去执行另一个线程中的代码
    • 程序计数器是每个线程私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令
    • 每个线程都有独立的线程计数器
  • 没有内存溢出

2、虚拟机栈

  • 栈:
    • 线程运行需要的内存空间
    • 一个栈由多个栈帧组成
    • 先进后出原则
  • 栈帧:
    • 每个方法运行时需要的内存

image-20201220203429200

2.1 定义

Java Virtual Machine Stacks(Java虚拟机栈)

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

演示代码

/**
 * 演示栈帧
 */
public class Demo01_1 {
    public static void main(String[] args) {
        method1();
    }

    private static void method1() {
        method2(1, 2);
    }

    private static int method2(int a, int b) {
        int c = a + b;
        return c;
    }

image-20201220205009317

在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点

问题辨析
  • 1.垃圾回收是否涉及栈内存?

    • (回答一)否。栈内存就是由一次次的方法调用产生的栈帧内存,而栈帧内存在每一次方法调用结束后都会被弹出栈,自动被回收掉。所以无需通过垃圾回收机制去回收内存。
    • (回答二)不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
  • 2.栈内存的分配越大越好吗?

    • 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
    • image-20201220205504774
  • 3.方法内的局部变量是否是线程安全的?

    • 如果方法内局部变量没有逃离方法的作用范围,则是线程安全

    • 如果如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题

    • 代码演示:

      • m1 StringBuilder对象是方法的局部变量,每个线程私有的,其他线程不可能访问到StringBuilder对象,因此线程安全
      • m2 StringBuilder对象是方法参数,不是线程安全。有可能有其他线程访问,对多个线程来说是共享的
      • m3 StringBuilder对象作为返回值,逃离了方法作用范围,能够被其他线程访问,不再线程安全
      package com.cloudguest.Demo;
      
      /**
       * 局部变量的线程安全问题
       */
      public class Demo01_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();
      
      
          }
      
          //m1    StringBuilder对象是方法的局部变量,每个线程私有的,其他线程不可能访问到StringBuilder对象,因此线程安全
          public static void m1() {
              StringBuilder sb = new StringBuilder();
              sb.append(1);
              sb.append(2);
              sb.append(3);
              System.out.println(sb.toString());
          }
      
          //m2   StringBuilder对象是方法参数,不是线程安全。有可能有其他线程访问,对多个线程来说是共享的
          //见主函数演示
          public static void m2(StringBuilder sb) {
              sb.append(1);
              sb.append(2);
              sb.append(3);
              System.out.println(sb);
      
          }
      
          //m3 StringBuilder对象作为返回值,逃离了方法作用范围,能够被其他线程访问,不再线程安全
          public static StringBuilder m3() {
              StringBuilder sb = new StringBuilder();
              sb.append(1);
              sb.append(2);
              sb.append(3);
              return sb;
          }
      }
      

2.2 栈内存溢出(stackOverflowError)

Java.lang.stackOverflowError 栈内存溢出

发生原因

  • 栈帧过多导致栈内存溢出
    • 如方法的递归调用可能会出现
  • 栈帧过大导致栈内存溢出
2.2.1 方法没有正确结束递归导致栈内存溢出

代码演示:

package com.cloudguest.Demo;

/**
 * 演示栈内存溢出  java.lang.StackOverflowError
 *  -Xss256k
 */
public class Demo01_2 {
    private static int count;

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }

    private static void method1() {
        count++;
        method1();
    }
}

image-20201220213358088

-Xss默认值未设置:

image-20201220213432347

-Xss256k: 栈的总大小变小,递归调用次数(count)变小

image-20201220213620274

image-20201220213608477

2.2.2 第三方库导致栈内存溢出
  • Json数据转换
package com.cloudguest.demo;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.List;

/**
 * json数据转换
 */
public class Demo01_19 {
    public static void main(String[] args) throws JsonProcessingException {
        Dept d = new Dept();
        d.setName("Dylan");

        Emp e1 = new Emp();
        e1.setName("Li");
        e1.setDept(d);

        Emp e2 = new Emp();
        e2.setName("Zhang");
        e2.setDept(d);

        d.setEmps(Arrays.asList(e1, e2));

        // {name : 'Dylan', emps: [{name:'Li', dept: '', emps: ...无限递归... }]}
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(d));

    }

}

class Emp {
    private String name;
    //@JsonIgnore  // 添加注解JsonIgnore转换时忽略dept属性的转换,双向关联--》单项关联,只通过部门关联员工
    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;
    }
}
  • 注意:
    • 1.运行错误
      • image-20201223173901044
    • 2.stackOverflowError原因:
      • image-20201223173708263
    • 3.解决思路:
      • 添加@JsonIgnore注解
        • image-20201223174212054
        • 添加注解JsonIgnore转换时忽略dept属性的转换,双向关联–》单项关联,只通过部门关联员工

2.3 线程运行诊断

案例1:CPU占用过多
  • 在Linux上使用nohup命令后台运行java代码

image-20201223184032609

  • 使用top命令实时监测(查看进程)

从下图可以看出,java代码在后台不断使用cpu在跑,占用cpu达到99.4%左右:

image-20201223184301547

  • 使用ps H -eo pid,tid,%cpu命令查看Linux所有线程的pid,tid,cpu占用

image-20201223184559463

  • 使用ps H -eo pid,tid,%cpu | grep 32655查看32655进程

    image-20201223184859074

  • 使用jstack 32655查看进程中的线程的nid

image-20201223185823912

  • 分析源代码

    image-20201223190220658

  • 定位方法总结:

    • 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: 程序运行很长时间没有结果
  • 在Linux上使用nohup命令后台运行java代码

    ​ 等待很久没有得到结果,可能是线程发现死锁

image-20201223190310120

  • 使用jstack 32752查看进程中的线程的nid

image-20201223190811786

  • 分析源代码

    ​ 开始时候,Thread0(第一个线程)锁住对象a,然后休眠2s;在Thread0休眠的时间段里,其他代码继续运行。

    ​ 1s之后,Thread1开始运行,锁住对象b,然后尝试去锁住对象a,由于Thread0已经将对象a锁住,所以Thread1想锁住对象a,需要等待Thread0释放对象a的锁。

    ​ 2s之后,Thread0从睡眠状态醒过来,尝试去获得对象b上的锁,但是b对象已经在1s时刻,被Thread1锁住,所以Thread0也陷入等待。

    ​ 死锁原因:Thread1等待Thread0释放对象a的锁,Thread0等待Thread1释放对象b的锁。

image-20201223191005916

image-20201223192334173


3、本地方法栈(Native Method Stacks)

image-20201223192904957

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

​ 如java.lang.Object中:

  • wait()方法,hashCode()方法…

image-20201223193335421

image-20201223193407576


4、堆

4.1 定义

Heap堆:

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

特点:

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

4.2 堆内存溢出

  • 代码:
package com.cloudguest.demo;

import java.util.ArrayList;
import java.util.List;

/**
 * 演示堆内存溢出
 * -Xmx8m
 */
public class Demo01_5 {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();  //list集合声明开始
            String a = "hello";
            while (true) {
                list.add(a);  // hello,hellohello,hellohellohello, ...
                a = a + a;  // 与上一次得到的字符串拼接
                i++;
            }
        }catch (Throwable e){
            e.printStackTrace();
            System.out.println(i);  //输出拼接多少次造成堆内存溢出
        }
    }
}
  • 堆内存溢出 (java.lang.OutofMemoryError :java heap space):

image-20201223194456736

  • 分析:

​ list集合从声明开始,到catch块前面,都是有效范围,因此不能被垃圾回收掉。直到将堆空间占满,造成OutofMemoryError

  • 修改堆空间值: Xmx8m

image-20201223195307908

  • 最终循环17次,造成堆空间不足

image-20201223195345628

4.3 堆内存诊断

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

演示代码:

package com.cloudguest.demo;

/**
 * 演示堆内存
 */
public class Demo01_4 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);
        byte[] array = new byte[1024 * 11024 * 100];  // 10Mb
        System.out.println("2...");
        Thread.sleep(30000);
        array = null;  //byte[]数组对象可以被垃圾回收
        System.gc();  //显示调用垃圾回收
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

堆内存诊断:

  • jmap演示:
E:\programming\java\itcast\jvm>jps
2656
13160 Demo01_4
14632 Jps
4188 Launcher
E:\programming\java\itcast\jvm>jmap -heap 13160  //不同时间点(打印1,2,3)堆内存占用不同
Attaching to process ID 13160, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.271-b09

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4240441344 (4044.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1413480448 (1348.0MB)
   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     = 6658608 (6.3501434326171875MB) //堆内存占用
   free     = 59925968 (57.14985656738281MB)
   10.000225878137304% 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 = 1307049984 (1246.5MB)
   used     = 1128857616 (1076.562515258789MB)
   free     = 178192368 (169.93748474121094MB)
   86.36682834005528% used

3186 interned Strings occupying 261168 bytes.

  • jconsole演示

image-20201223201512603

image-20201223201524262

案例
package com.cloudguest.demo;

import java.util.ArrayList;
import java.util.List;

/**
 * 演示查看的对象个数  演示查看的对象个数  堆转储  dump
 */
public class Demo01_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(10000000000L);
    }
}

class Student {
    private byte[] big = new byte[1024 * 1024];  //1Mb
}
  • 垃圾回收后,内存占用仍然很高

  • jconsole分析:

image-20201223201951141

  • jvisualvm分析

image-20201223203148199

image-20201223202420055


5、方法区

image-20201223203630527

5.1 定义

image-20201223203736847

5.2 组成

image-20201223204321639

5.3 方法区内存溢出

  • 1.8以前会导致永久代内存溢出
  • 1.8以后会导致元空间内存溢出
案例:演示元空间内存溢出(jdk1.8~)
package com.cloudguest.demo;

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 演示元空间内存溢出
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo01_8 extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo01_8 test = new Demo01_8();
            for (int i = 0; i < 10000; i++, j++) {
                ClassWriter cw = new ClassWriter(0);
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                byte[] code = cw.toByteArray();
                test.defineClass("Class" + i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}
  • 默认情况,利用系统内存(16g),未发生元空间内存溢出

image-20201223213216834

  • 更改元空间虚拟机参数 -XX:MaxMetaspaceSize=8m
    • 报错:java.lang.OutOfMemoryError: Metaspace,元空间内存溢出

image-20201223213443234

image-20201223213827850

案例:演示永久代内存溢出 (jdk1.6测试)
  • 代码同上
  • 报错:java.lang.OutOfMemoryError: PermGen space,永久代内存溢出

image-20201223214231354

总结
  • 1.8以前会导致永久代内存溢出
* 演示永久代内存溢出		java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
  • 1.8以后会导致元空间内存溢出
* 演示元空间内存溢出		java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
场景
  • spring
  • mybatis

5.4 运行时常量池

  • 通过HelloWorld.java初识常量池

image-20201223215833869

通过反编译来查看类的信息

  • 获得对应类的.class文件

    • 在JDK对应的bin目录下运行cmd,也可以在IDEA控制台输入

    image-20201223225328344

    • 输入 javac 对应类的绝对路径
    javac HelloWorld.java
    

    ​ 输入完成后,对应的目录下就会出现类的.class文件

  • 在控制台输入javap -v 类的绝对路径

    javap -v HelloWorld.class
    
  • 然后能在控制台看到反编译以后类的信息了

    • 类的基本信息
E:\programming\java\itcast\jvm\src\main\java\com\cloudguest\jvm\t5>dir
2020/12/23  21:54    <DIR>          .
2020/12/23  21:54    <DIR>          ..
2020/12/23  21:54               448 HelloWorld.class
2020/12/23  21:52               230 HelloWorld.java
               2 个文件            678 字节
               2 个目录 282,418,880,512 可用字节


    
E:\programming\java\itcast\jvm\src\main\java\com\cloudguest\jvm\t5>javap -v HelloWorld.class
Classfile /E:/programming/java/itcast/jvm/src/main/java/com/cloudguest/jvm/t5/HelloWorld.class  //类的文件
  Last modified 2020-12-23; size 448 bytes  //最后修改时间
  MD5 checksum 0c6f4ef9e2ca5eddb6b38e6ca713c2f3 //签名
  Compiled from "HelloWorld.java"  
public class com.cloudguest.jvm.t5.HelloWorld 
  minor version: 0 
  major version: 52  //jdk内部版本 - 1.8
  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            // com/cloudguest/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               com/cloudguest/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 com.cloudguest.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:内是真正编译执行的内容,#号的内容需要在常量池`Constant pool`中查找)
    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!  加载`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"

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

  • 运行时常量池,常量池是*.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

5.5 StringTable(串池)

面试题:

        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);
        System.out.println(s3 == s5);
        System.out.println(s3 == s6);

        String x2 = new String("c") + new String("d");
        String x1 = "cd";
        x2.intern();

        // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
        System.out.println(x1 == x2);

常量池与串池的关系

1.用来放字符串对象且里面的元素不重复

//StringTable[]
public class Demo01_22 {
    // 常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有变为java字符串对象
    // 当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)  -->  StringTable[“a”]
    // 当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中  -->  StringTable[“a”,“b”]
    // 当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中 -->  StringTable[“a”,“b”,“ab”]
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
    }
}

常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串

    Code:
      stack=1, locals=4, 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: return

当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)

当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中

当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中

最终StringTable [“a”, “b”, “ab”]

注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

2.使用拼接字符串变量对象创建字符串的过程

public class Demo01_22 {
    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")
    }
}

反编译后的结果

    Code:
      stack=2, locals=5, 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: return
}

通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()

最后的toString方法的返回值是一个新的字符串,但字符串的和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中

String s3 = "ab";
String s4 = s1 + s2;
//结果为false,因为s3是存在于串池之中,s4是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
System.out.println(s3 == s4);  

3.使用拼接字符串常量对象的方法创建字符串

public class Demo01_22 {
    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()
        
        //使用拼接字符串常量对象的方法创建字符串
        String s5 = "a" + "b";  //javac 在编译期间的优化,结果已经在编译器确定为ab
    }
}

反编译后的结果:

    Code:
      stack=2, 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: return

  • 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以s5直接从串池中获取值,所以进行的操作s3与s5一致。
  • 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuffer来创建

4.字符串字面量也是【延迟】成为对象的

/**
 * 演示字符串字面量也是【延迟】成为对象的
 */
public class TestString {
    public static void main(String[] args) {
        int x = args.length;
        System.out.println();  //字符串个数 = 2163

        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("0");

        System.out.println("1");  //字符串个数 = 2173
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("0");

    }
}

image-20201223235012873

  • 从字符串数量变化可知,执行TestString的时候,不是直接将所有字符串对象全部放入串池,而是执行一行代码,遇见一个没见过的字符串对象,才放入串池。
  • 遇到串池中已有的字符串,会直接从串池中获取,不会创建新的字符串对象。

5.5 StringTable特性

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
    • jdk1.8 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
    • jdk1.6 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
  • 注意:无论是串池还是堆里面的字符串,都是对象
intern方法演示
  1. intern方法(jdk 1.8)

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

  • 如果串池中没有该字符串对象,则放入成功
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

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

例1

public class Demo23 {
    //StringTable["a","b","ab"]
    public static void main(String[] args) {
        String s = new String("a") + new String("b");  //new  String("ab") ,仅仅存在于堆中,动态拼接的得到的结果

        //堆 new String("a")  new String("b")  new String("ab")
        String s2 = s.intern();//将s这个字符串对象尝试放入串池,如果有,则不会放入,如果没有则放入串池,会把串池中的对象返回

        //true,这里的"ab"用的就是s2中放入串池的对象,因此为同一个对象
        System.out.println(s2 == "ab");
        //true
        System.out.println(s == "ab");
    }
}

例2

public class Demo23 {
    //StringTable["ab","a","b"]
    public static void main(String[] args) {
        String x = "ab";
        String s = new String("a") + new String("b");  //new  String("ab") ,仅仅存在于堆中,动态拼接的得到的结果

        //堆 new String("a")  new String("b")  new String("ab")
        String s2 = s.intern();//将s这个字符串对象尝试放入串池。串池中存在,不会放入,会把串池中的对象返回

        //true,s2是把x在串池中的对象"ab"返回,因此二者一致
        System.out.println(s2 == x);
        //false,s的"ab"仅存在于堆中
        System.out.println(s == x);
    }
}
  1. intern方法(jdk 1.6)

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

  • 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象

面试题解答
        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

  • jdk1.8中
        String x2 = new String("c") + new String("d");  //StringTable["c","d"],堆中new String("cd")
        String x1 = "cd";  //StringTable["c","d","cd"]
        x2.intern();  //将x2这个对象尝试放入串池,有,不会放入,会将串池中"cd"对象返回

        // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢 
        System.out.println(x1 == x2);  //false
		//如果调换了【最后两行代码】的位置 -- true
		//调换了【最后两行代码】的位置 + 是jdk1.6  -- false

  • jdk1.8中
        String x2 = new String("c") + new String("d");  //StringTable["c","d"],堆中new String("cd")
        x2.intern();  //将x2这个对象尝试放入串池,没有,放入成功,会将串池中对象返回,StringTable["c","d","cd"]
        String x1 = "cd";  //将"cd"这个对象尝试放入串池,有,不会放入,会将串池中"cd"对象返回,即与x2一致
		
        // 如果是jdk1.6呢 
        System.out.println(x1 == x2);  //true
  • jdk1.6中
        String x2 = new String("c") + new String("d");  //new String("cd")
        x2.intern();  //将x2这个对象尝试放入串池,没有,将该字符串对象复制一份,再将副本放入到串池中,会将串池中对象返回
		//常量池中的"cd"是一个副本,同堆中的"cd"是不同的对象
        String x1 = "cd";  //x1引用自变量"cd",得到的是堆中常量池的副本,但x2还是堆中的对象

        System.out.println(x1 == x2);  //false

5.6 StringTable 位置

  • 与版本有关。
    • jvm1.6时候,StringTable是常量池的一部分,存在于永久代中;
    • jvm1.7开始,StringTable从永久代转移到了堆中,
      • 原因: 永久代的回收效率太低。
      • 优点:大大减轻了内存的字符串对内存的占用。

image-20201223204321639

5.7 StringTable 垃圾回收

StringTable在内存紧张时,会发生垃圾回收

演示StringTable垃圾回收
/**
 * 演示StringTable垃圾回收
 *  -Xmx10m   虚拟机堆内存最大值
 *  -XX:+PrintStringTableStatistics  打印字符串表的统计信息,可以观察到串池中字符串的相关信息
 *  -XX:+PrintGCDetails  -verbose:gc  打印垃圾回收的详细信息
 */
public class Demo1_7 {
    public static void main(String[] args) {
        int i = 0;
        try {
        }catch (Throwable e){
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }
}

打印信息:

0
Heap  //打印堆内存占用情况
 PSYoungGen      total 2560K, used 1791K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 87% used [0x00000000ffd00000,0x00000000ffebfdd8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
 Metaspace       used 3193K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:  
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13359 =    320616 bytes, avg  24.000
Number of literals      :     13359 =    571832 bytes, avg  42.805
Total footprint         :           =   1052536 bytes
Average bucket size     :     0.668
Variance of bucket size :     0.669
Std. dev. of bucket size:     0.818
Maximum bucket size     :         6
StringTable statistics:  //常量池统计信息,底层类似于HashTbale实现,结构为数组 + 链表
Number of buckets       :     60013 =    480104 bytes, avg   8.000  //数组个数,桶(buckets)
Number of entries       :      1764 =     42336 bytes, avg  24.000  //键值对个数
Number of literals      :      1764 =    158120 bytes, avg  89.637  //串池中字符串对象个数
Total footprint         :           =    680560 bytes
Average bucket size     :     0.029
Variance of bucket size :     0.030
Std. dev. of bucket size:     0.172
Maximum bucket size     :         3

Process finished with exit code 0

修改代码;

        int i = 0;
        try {
            for (int j = 0; j < 100; j++) {  //j=100,j=10000
                String.valueOf(j).intern();  //将产生的字符串对象加入到StringTable中
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

j=100打印信息:

image-20201224160316972

j=10000打印信息(buckets的个数没有增加10000个,只有6149):

image-20201224160547039

5.8 StringTable 性能调优

StringTable 性能调优,就是调整StringTable桶的个数

  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间

    -XX:StringTableSize=xxxx
    
  • 考虑是否需要将字符串对象入池

    可以通过intern方法减少重复入池

6.直接内存

6.1 定义

Direct Memory

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

文件读写流程

img

使用了DirectBuffer(用于数据缓冲区)

img

直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率。

6.2 分配和回收原理

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

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

通过

//通过ByteBuffer申请1M的直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);

申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?

allocateDirect的实现

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

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为虚引用的实际对象
    att = null;
}

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

public void clean() {
       if (remove(this)) {
           try {
               this.thunk.run(); //调用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;
                   }
               });
           }

对应对象的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来释放内存
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值