JVM_02 内存区域(堆、方法区、直接内存)

目录

定义

特点

方法区

定义

特点

运行时常量池

定义 

StringTable(字符串常量池)

字符串变量拼接

字符串编译器优化

延迟实例化

intern()方法

直接内存

应用场景

原理分析


定义

  • 通过new关键字,创建的对象的存储区域
  • 是虚拟机所管理的内存中最大的一块区域,目的是存放对象实例。

特点

  • 是线程共享的一块区域,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制
  • 既可以被实现成固定大小,又可以是扩展的,主流通过-Xmx 及-Xms(-Xms 堆内存的初始大小,默认为物理内存的1/64,-Xmx 堆内存的最大大小,默认为物理内存的1/4
    )进行配置。

方法区

定义

  • 与Java堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。
  • 方法去逻辑上是堆的组成部分,具体的实现不同,HotSpot虚拟机1.8以前,其具体实现永久代在JVM内存中,1.8及1.8以后,其实现元空间在本地内存(操作系统中)

特点

  • 方法区在虚拟启动时被创建
  • 方法区逻辑上是堆的组成部分,但是不同版本的JVM对齐实现不同,比如永久代(jdk8之前的实现,使用的堆内存)和元空间(jdk8的实现,使用的是操作系统内存)
  • 方法区无法满足新的内存分配需求时,将抛出OOM异常(动态生成JSP文件,不断新建字符串等情况会触发以上情况)

运行时常量池

定义 

运行时常量池(Runtime Constant Pool)是方法区的一部分,Class文件中除了有类的版本、字节、方法、接口等描述信息外,还有一项是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池。并把里面的符号地址变为真实地址。

  • 1.6中运行时常量池表放在永久代中
  • 1.8中运行时常量池被移入了堆内存中

先来了解二进制字节码下的常量池

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

对以上文件执行 javap -v HelloWorld.class后得到以下字节码指令

Classfile /Users/lizhijian/IdeaProjects/Demo/out/production/Demo/jvm/HelloWorld.class
  Last modified 2021-6-16; size 541 bytes
  MD5 checksum 2857962a7a60aa95f2f6642d6b02afad
  Compiled from "HelloWorld.java"
public class jvm.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
                           // (2)获取#21 #22指向的信息
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world  (6)指向hello world
   #4 = Methodref          #24.#25        // (9) java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // jvm/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               Ljvm/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  (3)
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream; (4)
  #23 = Utf8               hello world    //(7)
  #24 = Class              #31            // (10)java/io/PrintStream
  #25 = NameAndType        #32:#33        // (11)println:(Ljava/lang/String;)V
  #26 = Utf8               jvm/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 jvm.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 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ljvm/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
         // (1)获取常量池中编号为2的常量
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // (5)将#3 指向的常量加载到操作数栈
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // (8)Method  java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

可以看到Constant pool 下面的部分是常量池,  就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。(1)~(11)即为加载常量池数据的过程。

运行时常量池:当类被加载时,类的常量池信息就会放入运行时常量池中,并且把里面的符号地址(#1,#2)换为真实地址。

StringTable(字符串常量池)

针对下面代码进行反编译

public class Demo1_22 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
    }
}

编译后字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #2                  // String a  从常量池#2号位置加载a
         2: astore_1                          // 存入到LocalVariableTable变量表1号槽位
         3: ldc           #3                  // String b  
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: return
      LineNumberTable:
        line 12: 0
        line 13: 3
        line 14: 6
        line 15: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            3       7     1    s1   Ljava/lang/String;
            6       4     2    s2   Ljava/lang/String;
            9       1     3    s3   Ljava/lang/String;

常量池中的信息,都被加载到运行时常量池中,这时a  b  ab都是符号还没有变成字符串对象,要等到具体执行到String s1 = "a"这句话的时候,才去把a符号变为“a”字符串。过程如下:

ldc #2 会把a符号变为”a“字符串(懒加载)。并在准好的的stringTable[ ] 中查找是否有相同的字符串,如果没有则放入串池。

字符串变量拼接

再来看一个demo

//StringTable["a", "b", "ab"]
public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;  //new StringBuilder().append(s1).append(s2).toString();
        System.out.println(s3 == s4);
    }

编译后字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: ldc           #2                  // String a
         // 将a存入局部变量表
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         // 创建StringBuilder对象
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
         // 调用StringBuilder的构造方法
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
         // 加载局部变量表1号位置元素
        16: aload_1
         // 调用局部变量表中的append方法
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: aload_2
         // 调用局部变量表中的append方法
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
         // 调用局部变量表中的toString方法 
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
         // 将toString结果存入4号局部变量表中
        27: astore        4
        29: return
      LineNumberTable:
        line 12: 0
        line 13: 3
        line 14: 6
        line 15: 9
        line 16: 29
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      30     0  args   [Ljava/lang/String;
            3      27     1    s1   Ljava/lang/String;
            6      24     2    s2   Ljava/lang/String;
            9      21     3    s3   Ljava/lang/String;
           29       1     4    s4   Ljava/lang/String;

可以看到, String s4 = s1 + s2;<==> new StringBuilder().append(s1).append(s2).toString();

而toString()的源码是

public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

在堆中新建一个字符串,综上所述System.out.println(s3 == s4); 输出false.

字符串编译器优化

String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" +"b";

因String s3 = “ab” 已经在串池中创建(s3 先去常量池加载,加载完以后将“ab”添加到StringTable[ ]),jvm认为“a”和“b”都是常量,不会改变,因此在编译期做出优化,使String s5 = "a" + "b",直接在串池中找到“ab”字符串,并没有重新创建对象。即System.out.println(s3 == s5)   <==>   true

延迟实例化

public static void main(String[] args) {
        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("1");
        System.out.println("2");
        System.out.println("3");
    }

从上图中可以看到,随着代码的执行,StringTable中对应的字符串个数一直在增加,直到System.out.println("9")执行完。当第二次执行System.out.println("1");时数量没有再增加,因为“1”~“9”已经在串池中生成过。

intern()方法

概念

intern()作用是将这个字符串对象放入串池,如果有则不会放入,如果没有则放入串池,同时会把串池中的对象返回(这里需要注意,不管串池中之前存不存在,都会把串池对象返回)

  • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串 池中的对象返回
  • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回

1.8下下看几个🌰:

  • demo1
public static void main(String[] args) {
        // new String("a")中 "a"是常量被放入串池     "b"是常量被放入串池
        // new String("a")又在堆中生成了新的对象
        String s1 = new String("a") + new String("b"); // 这里因为是动态拼接的,”ab“并不放入串池中
        String s2 = new String("a") + new String("b");
        System.out.println(s1 == "ab");   //false
        System.out.println(s2.intern() == "ab");  // true
    }

执行到new String("a")时,会在串池中放入”a“,同时在堆中生成新对象,new String("b")同理。但是当执行到String s1 = new String("a") + new String("b");时,因为是动态拼接的,所以”ab“并不会放到串池中,只会在堆中创建对象。因此s1==="ab"输出false,而s2.intern()返回的是串池对象,因此s2.intern()==”ab“输出true。

  • demo2
public static void main(String[] args) {
        String s = new String("a") + new String("b");// 堆对象
        String s2 = s.intern();  // "ab"在串池中不存在,把s对象放入串池,同时把结果赋值给s2
        String x = "ab";
        System.out.println(s2 == "ab"); // true
        System.out.println(s == x); // true
    }

--输出
true
true

执行到String s2 = s.intern();时,发现串池中并没有"ab",则在串池中创建”ab“,注意:这里需要理解为把s对象放入到了串池!!!!!!!!可以看到,s2拿到的是"ab"在串池中的地址,因此输出true,而串池中的"ab"本身就是用的s的地址,因此s==x(ab)输出true。

  • demo3

如果是“ab”先创建,则输出如下。

public static void main(String[] args) {
        String x = "ab"; // 串池对象
        String s = new String("a") + new String("b");// 堆对象
        String s2 = s.intern();  // "ab"在串池中已经存在,s.intern()返回串池对象
        System.out.println(s2 == x); // true
        System.out.println(s == x); // false
    }

--输出
true
fasle

可以看到变量s返回的是堆中地址,而s.intern()返回的是串池地址,即s==x 输出false(因为串池中的”ab“最初生成时并没有依赖变量s,所以s和"ab"没有直接联系),s2==x 输出true。

1.6下🌰:

  • demo1

字符串先创建

public static void main(String[] args) {
        String x = "ab";
        String s = new String("a") + new String("b");// 堆对象
        String s2 = s.intern();
        System.out.println(s2 == x);
        System.out.println(s == x);
    }

--输出
true
false

”ab“先在串池中创建,s2 = s.intern(),为s2返回的是串池地址,s本身指向堆中(串池已经有"ab"了,即不用依赖s进行再次创建),所以,s2==x 输出true,s==x 输出false。

  • demo2

字符串后创建

public static void main(String[] args) {   
        String s = new String("a") + new String("b");// 堆对象
        String s2 = s.intern(); // 1.6中s.intern()拷贝一份放入串池,理解为"ab"的创建不直接依赖s
        String x = "ab";
        System.out.println(s2 == x);
        System.out.println(s == x);
    }

-- 输出
true
false

从代码中可以分析出,s2=s.intern(),返回的是串池对象,因此s2==x输出true,s.intern()是拷贝一份“ab”到串池中,即“ab”并不直接依赖s,因此s==x,输出false。

总结:

  • 1.6/1.8中s.intern()返回的都是串池对象
  • 字符串变量s本身是否指向串池中的“ab”,需要看“ab”的创建是否依赖于s。

直接内存

应用场景

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

操作文件代码如下:

static final String FROM = "E:\\xx.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }

--输出
io 用时:2921.12
directBuffer 用时:892.55

可以看到,利用直接内存的方式性能上要大幅度高于传统IO方式

原理分析

  • 传统文件操作

传统文件操作会先从磁盘中将数据加载到系统内存,再将数据从系统内存加载到堆内存的缓冲区中,经历两步操作。

  • 利用直接内存

如上图所示,利用直接内存,在磁盘中开辟了一段空间使系统内存和Java堆内存都能直接访问,进而大幅提高了文件的读写性能。

参考资料:《深入理解Java虚拟机》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值