黑马程序员jvm笔记(二)--方法区 串池 直接内存

6.方法区

方法区是一个概念,它包括常量池+ClassLoader+Class还有串常量(StringTable)。

在逻辑上,方法区算是堆内存的一部分使用的是堆的永久代的内存,但是在实际实现上,不一定用堆的内存。

而在1.8之后增加了元空间这种概念,将方法区的实现从堆内存改到了操作系统内存。

它的结构如下:

20200608150547

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

  • 1.8以后会导致元空间内存溢出

    QQ截图20220120112737

6.2.常量池

常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,分为常量池运行时常量池

6.2.1.查看常量池的方法–通过反编译查看字节码文件
  • 获得对应类的.class文件

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

      img

    • 输入 javac 对应类的绝对路径(编译一次代码,可以用idea运行一次)

      F:\JAVA\JDK8.0\bin>javac F:\Thread_study\src\com\nyima\JVM\day01\Main.javaCopy
      

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

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

    javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy
    
  • 然后能在控制台看到反编译以后类的信息了

    • 类的基本信息

    • 常量池

      20200608150630

      20200608150641

    • 虚拟机中执行编译的方法(框内的是真正编译执行的内容,#号的内容需要在常量池中查找

      20200608150653

6.2.2.运行时常量池和常量池区别
  • 常量池
    • 就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
  • 运行时常量池
    • 常量池是*.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
6.2.3.串池(StringTable)

常量池中的字符串仅是符号,只有在被用到时才会转化为对象储存到堆里或者串池里。

串池是什么?

20200608150547

QQ截图20220120150414

串池指的是串池(StringTable),如上图所示是方法区的一员,1.6之前串池在常量池里储存在逻辑上的堆内存的永久代里。因为永久代需要重GC清理,所以1.8之后对串池的位置进行了更改,使他物理意义上脱离了大多数方法区成员的位置,不在元空间(本地内存)里。

串池是用来保存String对象的,当常量池的字符被String对象引用时,若串池里无重复的对象,将该对象加入到串池。以后若再遇到相同String对象引用相同的字符串则直接使用串池里的对象。(拼接的时候不用,用的是堆内存)

实际代码如下图:

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a"; 
		String b = "b";
		String ab = "ab";
	}
}

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

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: returnCopy

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

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

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

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

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

串池的存在避免了字符串对象的重复创建。

字符串变量拼接和字符串常量拼接
  • 字符串变量拼接的原理是StringBuilder,拼接后的对象放在堆内存里。
  • 字符串常量拼接的原理是编译器优化,串池里如果有你拼接完的字符串则直接返回,没有则创建一个加入到串池。

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

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a";
		String b = "b";
		String ab = "ab";
		//拼接字符串对象来创建新的字符串a
		String ab2 = a+b; 
	}
}Copy

反编译后的结果

	 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/Str
ing;
        27: astore        4
        29: returnCopy

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

最后通过toString方法的返回值是一个新的字符串.

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

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

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a";v
		String b = "b";
		String ab = "ab";
		String ab2 = a+b;
		//使用拼接字符串的方法创建字符串
		String ab3 = "a" + "b";
	}
}

反编译后的结果

 	  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/Str
ing;
        27: astore        4
        //ab3初始化时直接从串池中获取字符串
        29: ldc           #4                  // String ab
        31: astore        5
        33: returnCopy
  • 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。
  • 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建
intern方法

1.8

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

  • 如果串池中没有该字符串对象,则放入成功,返回引用的对象
  • 如果有该字符串对象,则放入失败,返回字符串里有的该对象

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

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

1.6

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

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

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

测试

原来串池有该字符串

此时,无论1.6还是1.8 x都不等于s

QQ截图20220120153336

原来串池里没有字符串

1.6 因为是复制一份新的堆的对象,所以和原来的对象s不同

QQ截图20220120153755

1.8.因为和原来的对象s相同所以 true

QQ截图20220120153813

串池垃圾回收

很多人感觉串池不能垃圾回收,但实际上,串池也是可以进行垃圾回收的。

QQ截图20220125151450

QQ截图20220125152020

注意:要加上-XX:+PrintStringTableStatistics底下才会显示详细信息。

几个基本的指令

-Xmx10m 指定堆内存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次数,耗费时间等信息

垃圾回收的演示代码:

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Code_05_StringTableTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            for(int j = 0; j < 100; j++) { // j = 100, j = 10000
                String.valueOf(j).intern();
                i++;
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }

}

QQ截图20220125152143

QQ截图20220125152339

StringTable 性能调优
  • 增加桶的个数
  • 尽量不使用堆进行字符串储存

因为StringTable(串池)底层是hashmap,实现了去重的功能,所以它的性能跟桶的个数(链表的节点数息息相关),桶数越多,性能越强,所以,当我们数据量较大的时候,适当增加桶的个数,能有效的提高效率。

相关命令

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

演示代码

QQ截图20220125152614

QQ截图20220125152848

QQ截图20220125152936

QQ截图20220125153453

同时,用堆内存储存字符串相对于用StringTable储存字符串,因为不能去重,占用的内存资源较大,所以我们尽量用intern()函数将字符串加入到串池。去掉重复的。

堆储存和串池储存做对比

​ 在48万数据存在重复的情况下QQ截图20220125154343

QQ截图20220125154726

QQ截图20220125154845

7.直接内存

7.1.什么是直接内存

直接内存指的就是Direct Memory,常见于Nio操作,区别于io,在读写操作时有着更高的效率。他实际上不属于jvm,应该算是一种开辟更快的读写内存的机制。

特点:

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

7.2.直接内存和io的读取对比

QQ截图20220125155226

QQ截图20220125155156

QQ截图20220125155409

很明显,直接内存速度高于io。

7.3.直接内存的回收原理和机制

之前我们讲过,直接内存不能使用JVM的垃圾回收进行处理,他有着自己的回收处理机制,下面我们将详细说说它的机制。

io读写文件的机制如下:

QQ截图20220125160217

而直接内存读写的机制如下:

QQ截图20220125160609

直接内存相对于io,省去了中间的系统内存缓存区向java缓存区文件的复制操作。大大提高了速率。

7.4.直接内存的回收机制

无法被jvm回收所导致的内存溢出

QQ截图20220125160924

如上所示,直接内存对象如果不清空没办法被jvm垃圾回收进行回收。

所以释放它就需要其他函数。

内存释放演示

分配了1g的直接内存

QQ截图20220125161243

当btye为空引用的时候,jvm就会调用对应的函数来释放这部分内存。

QQ截图20220125161420

那具体是怎么释放的呢?

玄机就在allocateDirect()函数里:

unsafe函数释放直接内存(直接内存的释放原理)

QQ截图20220125161844

那具体是怎么在底层调用unsafe的呢?我们再往下看:

QQ截图20220125160924

点开allocateDirect()

QQ截图20220125162115

点开DirectByteBuffer()

QQ截图20220125162843

点开Deallocator函数。

QQ截图20220125162513

cleaner的clean函数会执行Dealloctor的run()方法,调用unsafe函数释放

QQ截图20220125162950

显示gc导致的直接内存难以回收的应对方法

显示gc指的就是System.gc()指的是程序员写的gc。

QQ截图20220125163339

QQ截图20220125163538

QQ截图20220125163637

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值