6.方法区
方法区是一个概念,它包括常量池
+ClassLoader
+Class
还有串常量(StringTable)。
在逻辑上,方法区算是堆内存的一部分使用的是堆的永久代的内存,但是在实际实现上,不一定用堆的内存。
而在1.8之后增加了元空间这种概念,将方法区的实现从堆内存改到了操作系统内存。
它的结构如下:
6.1.内存溢出
-
1.8以前会导致永久代内存溢出
-
1.8以后会导致元空间内存溢出
6.2.常量池
常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,分为常量池
和运行时常量池
。
6.2.1.查看常量池的方法–通过反编译查看字节码文件
-
获得对应类的.class文件
-
在控制台输入 javap -v 类的绝对路径
javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy
-
然后能在控制台看到反编译以后类的信息了
-
类的基本信息
-
-
常量池
-
虚拟机中执行编译的方法(框内的是真正编译执行的内容,#号的内容需要在常量池中查找)
-
6.2.2.运行时常量池和常量池区别
- 常量池
- 就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
- 运行时常量池
- 常量池是*.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
6.2.3.串池(StringTable)
常量池中的字符串仅是符号,只有在被用到时才会转化为对象储存到堆里或者串池里。
串池是什么?
串池指的是串池(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
原来串池里没有字符串
1.6 因为是复制一份新的堆的对象,所以和原来的对象s不同
1.8.因为和原来的对象s相同所以 true
串池垃圾回收
很多人感觉串池不能垃圾回收,但实际上,串池也是可以进行垃圾回收的。
注意:要加上-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);
}
}
}
StringTable 性能调优
- 增加桶的个数
- 尽量不使用堆进行字符串储存
因为StringTable(串池)底层是hashmap,实现了去重的功能,所以它的性能跟桶的个数(链表的节点数息息相关),桶数越多,性能越强,所以,当我们数据量较大的时候,适当增加桶的个数,能有效的提高效率。
相关命令
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
演示代码
同时,用堆内存储存字符串相对于用StringTable储存字符串,因为不能去重,占用的内存资源较大,所以我们尽量用intern()
函数将字符串加入到串池。去掉重复的。
堆储存和串池储存做对比
在48万数据存在重复的情况下
7.直接内存
7.1.什么是直接内存
直接内存指的就是Direct Memory,常见于Nio操作,区别于io,在读写操作时有着更高的效率。他实际上不属于jvm,应该算是一种开辟更快的读写内存的机制。
特点:
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
7.2.直接内存和io的读取对比
很明显,直接内存速度高于io。
7.3.直接内存的回收原理和机制
之前我们讲过,直接内存不能使用JVM的垃圾回收进行处理,他有着自己的回收处理机制,下面我们将详细说说它的机制。
io读写文件的机制如下:
而直接内存读写的机制如下:
直接内存相对于io,省去了中间的系统内存缓存区向java缓存区文件的复制操作。大大提高了速率。
7.4.直接内存的回收机制
无法被jvm回收所导致的内存溢出
如上所示,直接内存对象如果不清空没办法被jvm垃圾回收进行回收。
所以释放它就需要其他函数。
内存释放演示
分配了1g的直接内存
当btye为空引用的时候,jvm就会调用对应的函数来释放这部分内存。
那具体是怎么释放的呢?
玄机就在allocateDirect()
函数里:
unsafe函数释放直接内存(直接内存的释放原理)
那具体是怎么在底层调用unsafe的呢?我们再往下看:
点开allocateDirect()
点开DirectByteBuffer()
点开Deallocator函数。
cleaner的clean函数会执行Dealloctor的run()方法,调用unsafe函数释放
显示gc导致的直接内存难以回收的应对方法
显示gc指的就是System.gc()
指的是程序员写的gc。