方法区
(此图省略了栈等结构,JVM结构详细图在JVM简介中,方法区中常量池应为运行时常量池)
定义
方法区(Method Area)是Java虚拟机(JVM)的一部分,它与Java堆一样,是被JVM实例中所有线程共享的区域。方法区在JVM启动时创建,可以选择固定大小或允许动态扩展。这个区域的大小直接影响到系统能够保存的类数量,如果定义了过多的类,可能会导致内存溢出(OOM)错误。随着JVM的关闭,方法区占用的内存也会被释放。
在JDK 7及以前的版本中,方法区通常被称为永久代(PermGen),但在JDK 8及以后,永久代的概念被废弃,转而使用本地内存中实现的元空间(Metaspace)来替代。与永久代不同,元空间的大小默认情况下不受JVM内存的限制,而是使用本地内存,因此可能会耗尽所有可用的系统内存。(引用百度及几篇博客)
内存溢出
-
1.8以前内存溢出(永久代)
报错内容: java.lanng.OutOfMemoryError: PermGen space -
1.8以后内存溢出(元空间)
报错内容:java.lanng.OutOfMemoryError: metaspace
场景
- 动态生成类的数量过多
场景描述:在应用程序中使用了大量动态生成的类,比如使用反射、动态代理或字节码生成工具(如 ASM、CGLIB、Javassist)创建大量类。
示例:某些框架(如 Hibernate、Spring 等)会在运行时创建代理类,如果没有合理限制,可能会导致方法区的内存被耗尽。 - 使用大量的字符串常量
场景描述:应用中存在大量的字符串常量,特别是在运行时生成的字符串常量可能会导致方法区的内存占用增加。
示例:通过拼接字符串或使用大量的字面量字符串,可能在方法区中生成多个相同的字符串对象。 - 类的重复加载
场景描述:在某些复杂的应用中,特别是使用多个类加载器的情况,可能导致同一个类被加载多次,从而占用更多的内存。
示例:使用 OSGi 框架或某些应用服务器时,类可能被多次加载,增加了方法区的内存消耗。 - 未被及时回收的类
场景描述:当类被卸载的条件不满足时,即使类的实例已经被销毁,方法区中的类仍然存在,导致内存使用的增加。
示例:长期运行的应用(如服务器)如果不断加载新类而不卸载旧类,可能会导致方法区的内存消耗逐渐增加。 - 过多的常量池
场景描述:每个类在方法区中都有自己的常量池,常量池中存储了类的常量值,如果常量过多,可能导致内存溢出。
示例:使用大量的注解,特别是当注解中包含大量数据时,会增加常量池的内存占用。 - 配置不当的 JVM 参数
场景描述:方法区的大小可以通过 JVM 参数进行配置。如果配置的内存太小,可能在正常使用情况下就会出现内存溢出。
示例:在启动 JVM 时未合理配置 -XX:PermSize 和 -XX:MaxPermSize(对于 Java 7 及之前的版本),可能导致方法区内存溢出。
解决方法
- 优化类的生成和加载:尽量减少动态生成类的数量,合理使用代理和反射。
- 控制字符串常量的数量:避免在运行时生成过多的字符串常量。
- 合理配置 JVM 内存参数:根据实际应用需求调整方法区的大小。
- 使用类卸载:在适当的情况下,确保类能够被及时卸载。
- 监控与分析:使用监控工具(如 JVisualVM、JConsole)监测方法区的内存使用情况,以便及早发现和解决问题。
运行时常量池
定义
-
常量池: 就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息
-
运行时常量池: 常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面改的符号地址变为真实地址
StringTable(字符串常量池简称“串池”)
StringTable的特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译器优化
- 可以用intern方法,主动将串池中还没有的字符串放入串池
面试题代码
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();
//问
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");
String x1 = "cd";
x2.intern();
//问,如果调换了【最后空两行代码的位置呢】,如果是jdk1.6呢 true false
System.out.println(x1 == x2)//false
常量池与串池的关系
字符串变量拼接
String s1 = "a";//创建一个a字符串类型,放入StringTable
String s2 = "b";//创建一个b字符串类型,放入StringTable
String s3 = "ab";//创建一个c字符串类型,放入StringTable
//StringTable['a','b','ab']
String s4 = "a" + "b"; //这段代码创建了一个StringBuilder对象
//new StringBuilder().append("a").append("b").toString()
System.out.println(s4 == s3)//答案是错误的的,s4在堆中,s3在串池中,不是一个对象
编译期优化
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s5 = "a" + "b";//javac在编译期间的优化,直接在串池中找到了“ab”
System.out.println(s4 == s3)//答案是true
intern
- 1.8将这个字符串尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
//StringTable["a","b"]
String s = new String("a") + new String("b");
//堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();
//StringTable["a","b","ab"]
System.out.println(s == "ab");//true
System.out.println(s2 == "ab");//true
String x = "ab";
String s = new String("a") + new String("b");
String s2 = s.intern();
Systen.out.println(s == x);//false s在堆中,串池中有ab不会把s放入
Systen.out.println(s2 == x);//true s2为串池返回的ab
- 1.6将这个字符串尝试放入串池,如果有则不会放入,如果没有则把对象复制一份放入串池,会把串池中的对象返回
String s = new String("a") + new String("b");
String s2 = s.intern();
String x = "ab";
Systen.out.println(s == x);//false s在堆中,串池中的ab是复制的另一个对象
Systen.out.println(s2 == x);//true s2为串池返回的ab
位置
- 1.6在永久代中
- 1.8在堆中
详情在本文第一张图片
垃圾回收
StringTable会发生垃圾回收
性能调优
- 调整 -XX:StringTableSize = 桶个数
- 考虑字符串对象是否入池
参考学习视频为黑马程序员JVM课程地址:https://www.bilibili.com/video/BV1yE411Z7AP?p=1
标签:ab,String,池及,StringTable,串池,内存,JVM,字符串,常量 From: https://www.cnblogs.com/Wylie1207/p/18501507