JVM(一)内存结构

一、前言

1.1、什么是 JVM ?

1)定义

Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。

2)好处

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

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

1.2、学习 JVM 有什么用?

  • 解决Java虚拟机的内存问题
  • 看懂GC日志

1.3、常见的 JVM

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

0_引言

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

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

1.4、JVM体系结构

20201214144213658

JVM体系结构分为:Class Loader SubSystem(类加载器子系统)、Runtime Data Areas(运行时数据区)、Execution Engine(执行引擎)

类加载器子系统(Class Loader SubSystem)

java的动态类加载功能由类加载器子系统来处理,它是在运行时首次引用类的时候加载连接并初始化类文件。

  • 加载(Loading)
    在加载(Loading)这个地方,它是通过引导类加载器(Bootstrap Class Loader)、扩展类加载器(Extention Class Loader)、应用程序类加载器(Application Class Loader)这三个类加载器帮助完成加载。
  • 连接(Linking)
    加载完毕之后要做连接(Linking)操作,而在连接操作里,它又分为这三个连接步骤验证(Vertify)、准备(Prepare)、解析(Resolve)。
    验证(Vertify):验证做的事情是字节码验证程序将验证生成的字节码文件是否正确,如果验证失败,将收到验证错误。
  • 准备(Prepare):准备阶段是为所有静态变量分配内存,并分配默认值。
  • 解析(Resolve):解析阶段所要做的事情是用方法区的原始引用代替所有符号内存引用。
  • 初始化(Initialization):这是类加载的最后阶段,此处所有的静态变量将被分配原始值,并且将执行静态代码块。

以上便是类加载器子系统。

运行时数据区(Runtime Data Areas)

  • 方法区域(Method Area):
    存储类级别的数据都将放在这里,每个java虚拟机只有一个方法区,所以它是一个共享的资源。
  • 堆区(Heap Area):
    所有的对象及其对应的实例变量和数组,都将存储在这里,而每个虚拟机堆区也只有一个。
    由于方法区和堆区是由多个线程共享的,所以它们不是线程安全的。
  • 虚拟机栈(Stack Area):
    对于每个线程创建一个单独的运行时栈,而对于每个方法调用我们在栈内存会创建一个条目,是为栈帧(Stack Frame),所有的局部变量都是在栈内存创建,由于它不是共享的资源,所以它是线程安全的。
  • pc寄存器(PC Registers):
    每个线程都有它自己的pc寄存器,所以它也是线程安全的,它的作用是用来保存当前正在执行的指令,一旦指令执行,pc寄存器将会更新到下一条指令。
  • 本地方法栈(Native Method Stack):
    主要是用来保存本机的方法信息,而对于每个线程,它将单独创建本地方法栈,所以它也是线程安全的。

执行引擎(Execution Engine)

  • 解释器(Interpreter):
    主要作用是读取字节码,对其进行解释并逐一执行,解释器解释字节码的速度较快,但它的执行速度较慢。
    致命缺点:当一个方法被多次调用,它每次都需要解释,所以这个时候就出现了JIT编译器(JIT Compiler)。
  • JIT编译器(JIT Compiler):
    被称为即时编译器,它的作用就是用来解决解释器的缺点的,当它发现重复代码时,它将采用即时编译器,进行编译整个字节码并将其更改为本地代码,使本地代码直接用于重复的方法调用,从而提高系统性能。JIT Compiler从右向左,从上至下,依次是中间代码生成器(用来生成中间代码)、代码优化器(负责优化上面生成的中间代码)、目标代码生成器(主要用来负责生成机器码);最后分析器(Profiler):它是个特殊的组件,负责查找热点,也就是来看这个方法是否被多次调用。
  • 垃圾回收器(Garbage Collection):
    它也是执行引擎的一部分,它收集或删除未引用的对象,我们可以通过调用System.gc来触发垃圾回收,但是我们不能保证执行,java的垃圾收集器它只收集你用关键字创建的对象,因此那些你不是通过new的形式创建的对象,就可以直接使用finalize方法来直接清理操作。

本地方法接口(Native Method Interface,JNI)

本地方法接口将与本地方法库(Native Method Library)进行交互,并提供这个执行引擎所需要的本地库。

本地方法库(Native Method Library)

它是执行引擎所需要的本机库的集合

二、内存结构

20201214144213658

2.1、程序计数器

2.1.1 定义

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

特点:

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

2)作用

img

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

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

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

2.2、虚拟机栈

2.2.1 定义

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

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

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

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

img

问题辨析:

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

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

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

img

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

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

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.2.2 栈内存溢出

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

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

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

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

2.2.3 线程运行诊断

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

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

img

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

2.3、本地方法栈

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

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

2.4、堆

2.4.1 定义

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

特点

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

2.4.2 堆内存溢出

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

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

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

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

2.4.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

img

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

img

2.5、方法区

2.5.1 定义

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

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

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

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

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

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

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

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

img

2.5.2 组成

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

1_内存结构2

ClassLoader用来加载类的字节码。

2.5.3 方法区内存溢出

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

演示内存溢出

img

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

img

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指令,指令地址 操作方式 常量池对应地址

img

常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
运行时常量池:常量池是 *.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)
img

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方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象拷贝添加到字符串常量池中,并且返回该字符串对象的引用。

img

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

img

1.8 x2==x1 false

1.6 x2==x1 false

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

img

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常量对内存的占用
img

/**
 * 演示 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位置

img

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

img

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

    }
}

堆空间

img

imgimg

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

新生代的垃圾回收快

2.5.8 StringTable 性能调优

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

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

设置桶的长度:

img

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

2.6、直接内存

2.6.1 定义

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

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

2.6.2 使用直接内存的好处

文件读写流程:

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

缓存,分次读取
img

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

使用了 DirectBuffer 文件读取流程

img

直接内存是操作系统和 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 的方式释放内存。

posted @ 2021-09-22 14:57  王陸  阅读(250)  评论(0编辑  收藏  举报