方法区(Method Area)
定义: 存储了跟类相关的信息,如:成员变量、方法、构造器及常量池等。
逻辑上是堆的一部分,但是具体的实现是不一样的。比如:oracle公司的Hotspot JVM
在1.8之前方法区的实现叫永久代,就是使用堆的一部分作为方法区。
而1.8之后方法区的实现叫元空间,使用的是本地内存也就是系统内存。
特点:
1.所有Java线程共享的区域
2.能发生outOfMemoryErrot(内存溢出)
产生方法区内存溢出的场景:
动态产生class并加载的场景:如,
spring利用cglib生成的代理类,mybatis用cglib动态生成mapper接口的实现类等
在JDK1.8之前,spring、mybatis等动态生成的类还是很容易造成永久代的内存溢出。
在1.8之后,因为元空间使用的是系统内存,相对来说充裕了很多,而且垃圾回收也是由自己管理的
演示代码:
1.8 以前会导致永久代内存溢出:
注意:该代码应用于JDK1.6版本
/**
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* 设置启动参数,永久代最大内存为8M: -XX:MaxPermSize=8m
*/
public class Demo1_6 extends ClassLoader {// 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_6 test = new Demo1_6();
for (int i = 0; i < 20000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 参数:版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_6, 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);
}
}
}
1.8 之后会导致元空间内存溢出:
注意:该代码应用于JDK1.8版本及之后
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* 设置启动参数,元空间最大内存为8M: -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);
}
}
}
运行时常量池
定义:
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息
运行时常量池:常量池是 class 文件中的,当该类被加载进虚拟机,它的常量池信息就会放入运行时常量
池,并把里面的符号地址变为真实的内存地址
演示代码:
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
输入指令:
javac HelloWorld.java
-将HelloWorld 类编译成class文件
输入指令:javap -v HelloWorld.class
-将class二进制文件反编译
StringTable(串池)
首先看一段面试代码:
public class Demo1 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
//问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
}
}
请问,在不运行的情况下,能否明确肯定的说出结果,如果不能,请看以下分析。
常量池与串池(StringTable)的关系:
1.类加载时常量池中的信息,都会被加载到运行时常量池中, 这时 a,b,ab 都是常量池中的符号,还没有变为 java 字符串对象。
2.第一次用到时才变为对象(懒惰的方式:延迟加载)
3.变成String对象后就会把它当成key到StringTable中去找有没有取值相同的key。
4.StringTable的数据结构就是一个哈希表,长度一开始是固定可并且是不能扩容的。
5.第一次从StringTable中找取值相同的key肯定是没有的,他就会把这个值存入StringTable中。
字符串变量的拼接:
String s4 = s1 + s2;
字符串变量的拼接其实会有以下操作:
1.首先new StringBuilder();说到这你肯定就会想到之后的操作了
2.append一个s1对应的值,在append一个s2对应的值
3.调用toString方法
4.赋值给字符串变量s4
其实做String s4 = s1 + s2;
这样的操作时,底层就是new StringBuilder().append("a").append("b").toString()
那么s3 == s4吗?
System.out.println(s3 == s4);输出结果是什么,相信心中有了答案
是false
也许你有疑惑,s3的值是“ab”,s4的值是“ab”为什么是false呢?
问题就在StringBuilder的toString()
这个方法里。
看toString的源码:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
它是new了一个新的String对象。所以说System.out.println(s3 == s4)
输出结果为false
字符串常量的拼接:
String s5 = "a" + "b";
这个相对于变量的拼接就简单许多,它其实在编译期间就会优化,因为“a”是常量,“b”是常量,“a” + "b"肯定也是常量啊。结果已经在编译期确定为“ab”了。
那么 System.out.println(s3 == s5)
输出的结果是什么呢?
true
你是否存在疑惑?其实很简单:
String s3 = "ab";
这个操作已经把常量"ab"放入StringTable中了。之后的String s5 = "a" + "b";
这个操作就会去StringTable中找对应的值,因为StringTable中有"ab"这个值,所以s3和s5指向的是同一个常量"ab",所以 System.out.println(s3 == s5)
输出的结果为true
。
StringTable 的特性:
1.常量池中的字符串仅是符号,第一次用到时才变为对象
2.利用串池的机制,来避免重复创建字符串对象
3.字符串变量拼接的原理是 StringBuilder (1.8)
4.字符串常量拼接的原理是编译期优化
来源:CSDN
作者:经常的常
链接:https://blog.csdn.net/weixin_45240169/article/details/104042220