JVM学习笔记 02、JVM的内存结构(下)

举报
长路 发表于 2022/11/27 22:47:33 2022/11/27
【摘要】 文章目录前言JVM整体视角一、程序计数器(私有)1.1、介绍1.2、作用1.3、特点二、虚拟机栈(私有)2.1、初识2.2、定义2.3、问题辨析(3个)2.4、栈内存溢出(案例演示)2.5、线程运行诊断2.5.1、案例1: cpu 占用过多2.5.2、案例2:程序运行很长时间没有结果三、本地方法栈(私有)四、堆(公共)4.1、定义4.2、堆内存溢出(案例演示)4.3、堆内存诊断(三个工具)4.4、

5.4、StringTable特性

5.4.1、总结

1、常量池中的字符串仅是符号,第一次用到时才变为对象

2、利用串池的机制,来避免重复创建字符串对象

3、字符串变量拼接的原理是 StringBuilder (1.8)

4、字符串常量拼接的原理是编译期优化

5、可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池

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


5.4.2、案例演示

5.4.2.1、常量(字节码)

public static void main(String[] args) {
    //这三个常量在javac一开始编译时就存储了常量池
    String s = "a";
    String s1 = "b";
    String s2 = "ab";
}

//反编译字节码
Constant pool:
  #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
  #2 = String             #25            // a
  #3 = String             #26            // b
  #4 = String             #27            // ab
  #5 = Class              #28            // com/changlu/JVM/Test
  #6 = Class              #29            // java/lang/Object

stack=1, locals=4, args_size=1
        0: ldc           #2                  // String a,根据#2直接从常量池中取出a
        2: astore_1     //存储到变量表中的slot1
        3: ldc           #3                  // String b  根据#3直接从常量池中取出b
        5: astore_2     //存储到变量表中的slot2
        6: ldc           #4                  // String ab  根据#4直接从常量池中取出ab
        8: astore_3     //存储到变量表中的slot3
        9: return
            
LocalVariableTable:   //本地变量表
        Start  Length  Slot  Name   Signature
        0      10     0  args   [Ljava/lang/String;
        3       7     1     s   Ljava/lang/String;
        6       4     2    s1   Ljava/lang/String;
        9       1     3    s2   Ljava/lang/String;


5.4.2.2、常量拼接、常量与字符串对象拼接

+拼接操作

①常量+常量

public static void main(String[] args) {
	String s2 = "a" + "b";//对常量直接进行拼接的在javac编译时会直接将拼接好的放入常量池
}

// 反编译
Constant pool:
    #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
    #2 = String             #21            // ab
    #3 = Class              #22            // com/changlu/JVM/Test
    #4 = Class              #23            // java/lang/Object
      
Code:
        stack=1, locals=2, args_size=1
        0: ldc           #2                  // String ab
        2: astore_1
        3: return

②先常量,后拼接

public static void main(String[] args) {
	String s = "a";
    String s1 = "b";
    String s2 = "a" + "b";//javac在编译期间进行优化,直接会将拼接好的字符串放入常量池
}

// 反编译
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

③常量+对象

public static void main(String[] args) {
    //1、javac编译期间,会将"a","b"存入到常量池中
    //2、java执行,也就是jvm进行执行时首先会进行stringbuilder实例化,接着append("a"),接着从常量池中取到"b",进行String实例化(new String("b")),接着传入到append()方法中,最后toString()返回新的对象
    //2完整过程:new StringBuilder().append("a").append(new String("b")).toString()
    String s = "a" + new String("b");
}

//反编译
Constant pool:
    #1 = Methodref          #11.#27        // java/lang/Object."<init>":()V
    #2 = Class              #28            // java/lang/StringBuilder
    #3 = Methodref          #2.#27         // java/lang/StringBuilder."<init>":()V
    #4 = String             #29            // a
    #5 = Methodref          #2.#30         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    #6 = Class              #31            // java/lang/String
    #7 = String             #32            // b
        
Code:
        stack=4, locals=2, args_size=1
        //字节码过程与我上面叙述的大致相同
        0: new           #2                  // class java/lang/StringBuilder
        3: dup
        4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
        7: ldc           #4                  // String a
        9: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        12: new           #6                  // class java/lang/String
        15: dup
        16: ldc           #7                  // String b
        18: invokespecial #8                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        21: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore_1
        28: return

总结

  1. 仅仅对于"xxx"常量,无论是单独声明还是拼接,在javac编译期间就会进行优化直接将拼接好的放入到常量池中。
  2. 对于new String(“xxx”),在javac编译时就会先将对应的字符串作为常量放入到常量池中,真正在java执行过程中会从常量池里取到"xxx",再进行String的实例化。


5.4.2.3、intern 方法详解

①JDK8

情况1:若是当前字符串在常量池中已经存在,那么本身字符串就不会放入常量池,其返回值是常量池的。

public class Test {
    public static void main(String[] args) {
        //*****情况1:先将ab放置到常量池******
        String s = "ab";
        //**********************************
        String str = new String("a") + new String("b");//对象
        String intern = str.intern();//在本题中"ab"原本就在常量池里,所以这里不需要把str再放入常量池(其本身还是对象),其返回值是常量池中的
        System.out.println(s == str);
        System.out.println(s == intern);
    }
}

image-20211115230517124

情况2:若是当前字符串在常量池中不存在,那么就会将本身字符串放入到常量值(这里指str),其返回值同样也是常量池的。

public class Test {
    public static void main(String[] args) {
        String str = new String("a") + new String("b");//对象
        String intern = str.intern();//由于常量池中没有"ab",此时就会将str放置到常量池,str指向常量池而不再指向对象,返回值也是常量池中的
        //*****情况2:后使用常量"ab",此时就是引用常量的了******
        String s = "ab";
        //**********************************
        System.out.println(s == str);
        System.out.println(s == intern);
    }
}

image-20211115230846175


JDK1.6

②JDK1.6:与1.8实现不同的是,当调用intern()方法时会先对自己本身进行复制一份,若是原本常量池中有则将拷贝那份放入,若是没有就不放入,自身不变,返回值为常量池那份。

所以上面两种情况代码,在jdk1.6中运行都是一样的结果,str本身在调用intern()无论常量池中是否存在其自身都不会改变

image-20211115231339071



5.4.3、面试题

JDK1.8

public class Test {
    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(); //"ab"   由于调用该方法前常量池已经存在"ab",所以s4本身不会改变,返回值依旧是常量池中的
        // 问
        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(); //由于"cd"在常量池中并不存在,所以x2会将自己放置到常量池中,x2="cd"
        String x1 = "cd";
        System.out.println(x1 == x2);//true
    }
}

image-20211115232845705

若是14、15行互换

String x1 = "cd";        
x2.intern(); //由于"cd"在常量池中存在,所以x2不会将自己放置到常量池中

image-20211115233013586


JDK1.6

由于执行intern()会进行拷贝一份,无论常量池中是否存在都会对拷贝的那份进行操作,而不会改变本身,所以无论上下位置是哪里最终都是false

image-20211115233013586



5.5、StringTable的位置(含案例)

示例

JDK1.8常量池在堆中。

JDK1.6在永久代里。

image-20211116093848453

对于JDK8与JDK1.6的位置改变原因:永久代的内存回收效率很低,需要父GC的时候才会触发永久代的垃圾回收,对于父GC需要等待老年代的GC后才会进行GC,触发的时机比较晚,间接的导致StringTable的回收效率并不高。

更改位置后效果:StringTable存储的是十分频繁的常量,若是它的回收效率不高就会占用大量的内存,进而导致永久代的内存不足,因为这个原因自JDK1.7将StringTable转义到堆中,只需要每秒GC都会触发垃圾回收,能够大大减轻了内存的占用。


程序代码

在JDK6和8中进行运行。

/**
 * 演示 StringTable 位置
 * 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
 * 在jdk6下设置 -XX:MaxPermSize=10m
 */
public class Main {

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

①JDK1.6:添加jvm参数选项-XX:MaxPermSize=10m,设置最大永久代容量为10MB

image-20211116100355418

②JDK1.8:-Xmx10m -XX:-UseGCOverheadLimit,两个参数第一个是设置堆内存空间为10MB,第二个参数是针对于将GCOverheadLimit开关关闭(-表示关闭开关,默认开启),若是不关闭jvm会自动进行检测垃圾回收时间大于98%,并且只回收了2%时会直接先抛出GC overhead limit exceeded,而不是堆空间溢出,如下图:

只设置参数:-Xmx10m

image-20211116101159726

若是我们想要看到堆空间溢出的报错信息需要将检测开关关闭,那么就需要添加上后一条指令:-Xmx10m -XX:-UseGCOverheadLimit

image-20211116101604936



5.6、StringTable的垃圾回收

JDK1.8下演示

使用jvm参数:-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

首先不创建常量查看信息:

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 * 10MB堆内存 打印常量表信息  打印GC细节
 */
public class Main {

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
			//暂为空
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}


//进行了一次垃圾回收
[GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->933K(9728K), 0.0011077 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0
//堆内存的信息
Heap
 //年轻代、老年代、元空间(方法区)
 PSYoungGen      total 2560K, used 1053K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 26% used [0x00000000ffd00000,0x00000000ffd89668,0x00000000fff00000)
  from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e010,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 429K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 5% used [0x00000000ff600000,0x00000000ff66b4f0,0x00000000ffd00000)
 Metaspace       used 3132K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 337K, capacity 388K, committed 512K, reserved 1048576K
//符号表:类名、方法名、变量名等等,读入到内存中,之后以查表方式查询
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     12988 =    311712 bytes, avg  24.000
Number of literals      :     12988 =    558664 bytes, avg  43.014
Total footprint         :           =   1030464 bytes
Average bucket size     :     0.649
Variance of bucket size :     0.649
Std. dev. of bucket size:     0.806
Maximum bucket size     :         6
//常量表,底层是hashtable(数组+链表),数组的个数为桶
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000  //桶个数
Number of entries       :      1720 =     41280 bytes, avg  24.000  //键值对个数
Number of literals      :      1720 =    155176 bytes, avg  90.219  //字符串常量
//占用的总字节数,约0.6MB
Total footprint         :           =    676560 bytes
Average bucket size     :     0.029
Variance of bucket size :     0.029
Std. dev. of bucket size:     0.170
Maximum bucket size     :         2

接着我们来创建常量后进行测试

//在上面代码11行添加如下代码,创建常量,这里的字符串常量并没有被引用,所以能够被垃圾回收
for (int j = 0; j < 100000; j++) { // j=100, j=10000
    String.valueOf(j).intern();
    i++;
}

//可以看到进行垃圾回收了三次
[GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->965K(9728K), 0.0010164 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2552K->504K(2560K)] 3013K->1005K(9728K), 0.0027719 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2552K->488K(2560K)] 3053K->1013K(9728K), 0.0024140 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
100000
Heap
 PSYoungGen      total 2560K, used 2394K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 93% used [0x00000000ffd00000,0x00000000ffedc828,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 525K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 7% used [0x00000000ff600000,0x00000000ff6834f0,0x00000000ffd00000)
 Metaspace       used 3247K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13236 =    317664 bytes, avg  24.000
Number of literals      :     13236 =    566320 bytes, avg  42.786
Total footprint         :           =   1044072 bytes
Average bucket size     :     0.661
Variance of bucket size :     0.662
Std. dev. of bucket size:     0.814
Maximum bucket size     :         6
StringTable statistics:  //可以看到当前常量池的数量为35830个,垃圾回收了最起码6万个左右的常量
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :     35830 =    859920 bytes, avg  24.000
Number of literals      :     35830 =   2065872 bytes, avg  57.658
Total footprint         :           =   3405896 bytes  //统计总占用为约3MB
Average bucket size     :     0.597
Variance of bucket size :     0.492
Std. dev. of bucket size:     0.702
Maximum bucket size     :         4

结论:在JDK1.8中,由于常量池在堆里,所以很容易能够进行垃圾回收GC,并且在java程序中内存一不够就会自动触发垃圾回收。



5.7、JVM调优

5.7.1、StringTable

总结:若是桶的个数比较多,就比较分散,那么哈希碰撞的几率就会减少,查找的速度也会变快;反之碰撞的几率较大,链表较长,查找的速度也会受到影响。

结论:若是字符串常量比较多的话,可以适当的将字符串也就是字符串常量表设置大,让其有更好的哈希分布,减少哈希冲突。

JDK1.8

程序说明:读取具有46万个左右的字符串,(约4.5MB左右),将字符串全部放入到常量池中。接下里我们来看一下如何进行常量池调优。

/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Main {

    public static void main(String[] args) throws IOException{
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), StandardCharsets.UTF_8))){
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null){
                    break;
                }
                line.intern();//放入常量池
            }
            //1ms => 1000000ns
            System.out.println("耗费时长(ms):" + (System.nanoTime() - start) / 1000 / 1000);
        }
    }
}

1、首先来看一下原始常量池中的桶数量以及运行的效率

vm参数:-XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc。打印常量池表以及GC垃圾回收信息

分析:jdk1.8的常量池在堆中,堆一般为4G,所以不会在此次运行过程中不会触发垃圾回收。

image-20211116114153383

错误示范:将常量池中的桶数量设置最小1009个

  • vm参数:-XX:StringTableSize=1009 -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

分析:由于桶数量只有1009个,字符串常量有48万左右,那么就很容易出现哈希碰撞,并且每个桶(数组某个)的链表过长,若是出现哈希冲突,每插入一个字符串常量就会比对大量此时,此时也就造成了我们程序运行时长很长的问题!

image-20211116114356913

调优1:若是设置桶1009个情况下,我们可以通过限制堆内存来让程序给我们进行GC垃圾回收,减少常量池中的常量数量,达到优化的效果。

  • vm参数:-Xmx10m -XX:StringTableSize=1009 -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc。这里设置堆内存为10MB,这样能够不断进行GC垃圾回收。

image-20211116114905789

调优2:设置一个适宜的堆内存空间以及能够均匀分配指定数量的桶个数

  • vm参数:-Xms200m -Xmx200m -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc。设置桶的个数为20万个,这样每个桶就平均装2个,减少哈希碰撞与链表比较。

image-20211116115400378



5.7.2、大量重复的字符串案例(调优)

案例描述:若是程序中有大量的重复字符串创建,我们可以通过将字符串放入常量池的方式来节省内存!

/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Main {

    public static void main(String[] args) throws IOException {
        List<String> address = new ArrayList<>();
        System.in.read();//敲回车向下执行
        for (int i = 0; i < 10; i++) {  //循环10次模拟出大量字符串重复情况
            //读取48万个字符串
            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);//直接将字符串添加到集合中
                    //address.add(line.intern());
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}

使用第21行代码来将字符串添加到记录中,使用jvisualvm来进行检测:

image-20211117135405066

可以看到使用了300MB的内存,接下来我们通过修改代码来进行优化:

//第21行代码修改为intern()
address.add(line.intern());

结果:可以看到再次运行时占用的字节数为55MB,减少了6倍。

image-20211117135715260

分析:使用了intern()会进行尝试将当前的字符串对象放入到常量池中,若是常量池有则放入,没有则不放入,返回的是常量池中的字符串引用(相同字符串引用的都是同一个地址),那么此时集合中添加的就不是堆中的引用,过程中在堆中创建的字符串对象由于没有被引用会被垃圾回收掉!



六、直接内存

6.1、定义

Direct Memory:直接内存,并不属于java虚拟机的内存管理,而是属于操作系统的内存。

  • NIO有一个ByteBuffer,这个ByteBuffer所使用与分配的内存就是直接内存,它不属于jvm管理。
  • 传统的IO是阻塞IO。

三个特点

  1. 常见于 NIO 操作时,用于数据缓冲区。
  2. 分配回收成本较高,但读写性能高。
  3. 不受 JVM 内存回收管理。(通过一个虚引用来进行的)


6.2、IO与直接内存比较

6.2.1、比较案例演示

案例描述:一个是使用普通IO进行输入输出、另一个使用直接内存来进行输入输出。操作的视频大小为310MB。

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;


public class Main {
    static final String FROM = "C:\\Users\\93997\\Desktop\\新建文件夹\\from\\math.mp4";
    static final String TO = "C:\\Users\\93997\\Desktop\\新建文件夹\\to\\math1.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:606.3063
        directBuffer(); // directBuffer 用时:246.3773
    }

    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);//反转指针指向:读/写指针position指到缓冲区头部
            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);
    }
}

image-20211117151140915

结论:使用直接内存来进行读取输出效率比普通IO要高!



6.2.2、直接内存原理分析

传统的IO操作

  1. 首先会从用户态切换到内核态(CPU的状态变化),切换到内存态之后可以使用c语言的函数去真正读取磁盘文件的内容。
  2. 切换到内存态之后会在操作系统中划分出来一块缓冲区(系统缓冲区),接着会先将磁盘文件的内容读取到系统缓冲区,注意java代码是不能够对磁盘文件进行读写操作的。
  3. java的话会在堆内存之中分配一块java缓冲区(如new byte[1024]),之后就会不断的从系统缓冲区读取数据到java缓冲区。
  4. CPU会再次切换到用户态后进行读取输入输出文件操作(针对于java缓冲区进行操作),最终将文件复制到目标位置。

image-20211117153836483

在这个过程中实际相当于会复制两份的操作,造成不必要的数据复制,效率不是很高。


直接内存(direct memory):会划分出一块直接内存区域,在这块区域我们java代码可以直接进行访问,系统也同样可以访问,两块代码是系统共享的一块区域,之后磁盘读取的文件会读取到直接内存,之后java代码同样也可以访问到这块直接内存中。

image-20211117154303366

本质:比之前的IO少复制了一次缓冲区的操作,提高了效率。



6.2.3、直接内存-内存溢出

代码说明:每次分配1G直接内存空间,不断循环直到抛出内存溢出报错。

public class Main {
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) throws IOException {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        System.in.read();
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);//分配直接内存1G
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
            System.in.read();
        }
    }
}

image-20211117152521781

image-20211117152449257

说明:直接内存不由JVM来进行管理,但是对于直接内存的申请创建也是有限制的,差不多4G左右内存。



6.3、直接内存(释放原理)

6.3.1、GC垃圾回收释放?

下面的程序我们来尝试使用显示GC垃圾回收的方式来释放直接内存:

import java.io.*;
import java.nio.ByteBuffer;


public class Main {
    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();
    }
}

可以看到当前分配了1G的内存空间

image-20211117154419975

image-20211117154511314

此时就会有疑问了,之前不是说直接内存不受JVM管理嘛,那为什么这里调用GC垃圾回收方法能够将直接内存回收呢?答案:借助虚引用实现,真正分配与释放借助Unsafe这个类。



6.3.2、释放与分配原理探究(Unsafe类)

Unsafe类:干一些分配或释放直接内存的事情,一般都是JDK内部会进行这样一些操作。

案例描述:通过Unsafe来进行分配与释放直接内存。

import sun.misc.Unsafe;
import java.io.*;
import java.lang.reflect.Field;

public class Main {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.out.println("已分配直接内存");
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.out.println("释放直接内存");
        System.in.read();
    }

    //借助反射来获取到Unsafe实例
    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

效果:可以看到通过Unsafe类可以对直接内存进行分配与释放,那么其实我们可以联想到之前的分配内存空间以及垃圾回收肯定也与这个Unsafe类有关!

image-20211117155352347

image-20211117155425727



6.3.3、源码深入(案例+源码)

之前分配直接内存、释放直接内存的代码我们调用的是如下两条:

ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);//分配直接内存
System.gc();//显示垃圾回收  Full GC

分配直接内存

直接看源码即可:内部通过Unsafe来进行分配内存,并且与此同时创建一个虚引用绑定该ByteBuffer实体类,一旦该实体类不被引用进行垃圾回收时就会调用clean()方法来进行

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

DirectByteBuffer(int cap) {  
    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);//使用Unsafe类来进行分配直接内存
    } 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:虚引用类型,若是所关联的对象(指的是bb)被回收时,那么Cleaner就会触发该虚引用的clean方法,该方法中就会执行对应的回调方法(就是第二个参数Deallocator中的run()方法)。对应在后台一个reference线程里执行,该线程会专门检测这些虚引用的情况,一旦虚引用对象关联的实际对象被回收掉以后就会调用该虚引用的clean方法。然后执行任务对象,最终任务对象再来真正执行freeMemory方法。
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

//************看一下Cleaner类**************
public class Cleaner extends PhantomReference<Object> {
    
    private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
    }
    
    //第二个参数就是调用指定的回调方法
    public static Cleaner create(Object var0, Runnable var1) {
        return var1 == null ? null : add(new Cleaner(var0, var1));
    }
    
    //一旦虚引用关联的对象被回收就会执行这个方法
    public void clean() {
        if (remove(this)) {
            try {
                //触发回调方法,来进行垃圾回收,见下面的
                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;
                    }
                });
            }

        }
    }
}

//真正进行直接内存回收的位置
private static class Deallocator
    implements Runnable
{

    private static Unsafe unsafe = Unsafe.getUnsafe();

    private long address;
    private long size;
    private int capacity;

    private Deallocator(long address, long size, int capacity) {
        assert (address != 0);
        this.address = address;
        this.size = size;
        this.capacity = capacity;
    }

    //clean()方法执行触发该run()方法,还是调用的Unsafe来执行清理直接内存操作
    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        //清理直接内存
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }

}

System.gc();//显示垃圾回收 Full GC

接下来说明一下为什么要额外进行调用下面的代码来进行直接内存的垃圾回收:

byteBuffer = null;  //不进行引用
System.gc(); // 显式的垃圾回收,Full GC,对年轻代、老年代都进行垃圾回收,开销较大

一般来说只有在内存空间不足的情况下会进行垃圾回收,若是byteBuffer不进行引用,可能不会触发GC垃圾回收。这也就是为什么我们还要显示调用GC的原因。

一旦byteBuffer不被引用,那么对应的虚引用就会被触发其中的clean()方法来进行释放直接内存操作,这一操作是一个守护进程完成的!

总结:垃圾回收只能释放java的内存,对于直接内存需要我们手动来去调用unfase的freeMemory方法来完成对内存的释放。



6.3.4、直接内存释放最好方式

若是直接使用显示GC方法调用来进行清理直接内存,其实是不太妥当的,因为该显示调用是进行Full GC,无论是年轻代还是老年代都会进行垃圾回收操作,在程序中占用的开销较大。

推荐使用unsafe来进行手动管理直接内存。就是6.3.2案例来进行直接内存的垃圾回收!



6.4、JVM调优(禁用GC垃圾回收)

JVM参数:-XX:+DisableExplicitGC,禁止显示进行GC垃圾回收。

原因:避免程序中有手动进行GC垃圾回收的操作,显示GC是Full GC,会有一定的开销,所以在进行调优时禁用。

import java.io.*;
import java.nio.ByteBuffer;

public class Main {
    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();
    }
}

image-20211117180220570



参考文章

[1]. java.nio.Buffer 中的 flip()方法



整理者:长路 时间:2021.11.14-17

capacity;
}

//clean()方法执行触发该run()方法,还是调用的Unsafe来执行清理直接内存操作
public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    //清理直接内存
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

}


<br/>

> System.gc();//显示垃圾回收  Full GC

接下来说明一下为什么要额外进行调用下面的代码来进行直接内存的垃圾回收:

```java
byteBuffer = null;  //不进行引用
System.gc(); // 显式的垃圾回收,Full GC,对年轻代、老年代都进行垃圾回收,开销较大

一般来说只有在内存空间不足的情况下会进行垃圾回收,若是byteBuffer不进行引用,可能不会触发GC垃圾回收。这也就是为什么我们还要显示调用GC的原因。

一旦byteBuffer不被引用,那么对应的虚引用就会被触发其中的clean()方法来进行释放直接内存操作,这一操作是一个守护进程完成的!

总结:垃圾回收只能释放java的内存,对于直接内存需要我们手动来去调用unfase的freeMemory方法来完成对内存的释放。


参考文章

[1]. java.nio.Buffer 中的 flip()方法

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。