6.1 定义
是所有java虚拟机线程共享的,他存储了跟类的结构相关的信息(成员方法,构造器。。。)
方法区在虚拟机启动时被创建,逻辑上是堆的一个组成部分。但是厂商在设置的时候有不同,有些把方法区放堆中,有些没有。
方法区如果内存不足了,也会抛一个内存不足错误。
在1.6里面方法区的实现叫做永久代。1.8里面实现叫元空间
注意1.8里面,他把StringTable不再放到方法区里面,而是放到了堆空间。
6.2 内存溢出
1.8之前会导致永久代内存溢出
1.8之后会导致元空间内存溢出
元空间使用的是系统内存,并不会导致内存溢出。所以我们要加一个参数MaxMetaspaceSize限制元空间大小。
场景:在运行期间动态生成类的字节码完成动态类加载
-
spring(cglib生成代理类)
-
mybatis(用cglib去产生mapper接口实现类)
在实际中我们框架里面大量使用这些加载类,1.8之前还是很容易导致永久代内存溢出,当然1.8以后,元空间使用的是系统内存,空间相对大了。并且垃圾回收效率高。
6.3 常量池
不管是1.6还是1.8,他们都有一个运行时常量池的部分。那这个运行时常量池的内部包含一个叫StringTable的东西,那他到底是什么呢?我们想要了解运行时常量池,首先要来了解一下什么叫常量池。
上面这段代码想要运行,首先要编译为字节码,那字节码宏观来说由三部分组成(类的基本信息,常量池,类方法定义-包含虚拟机指令)
-
常量池的作用就是给我们指令提供一些常量符号,让我们去查找。这样虚拟机指令才能去执行
常量池,就是一张表。虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息。
6.6 运行时常量池
运行常量池,常量池是*.class文件中的,当该类被加载。他的常量池信息就会放入到内存里面,那么这个内存名字叫运行时常量池。并且把里面的符号地址#2 #3变为真实的地址。
6.7 StringTable
6.7.1 几个面试题
接下来我们学习运行时常量池中一个比较重要的组成部分-StringTable。也就是我们俗称的串池。那么在讲StringTable之前,先做一个自我测试。
为了搞清楚上面的答案,我们需要从字节码和常量池的角度来分析一下刚才这些代码他的底层原理。
那么常量池和串池之间有有什么关系呢?
-
那常量池本来是在字节码文件里面的,在运行的时候呢,常量池里面的信息就会被加载到运行时常量池里面去。但是注意,加载完之后啊我们的变量a 啊 ,b啊只是一些我们看不懂的符号,还没有变为java中的字符串对象。那他什么时候变为java中的字符串对象呢?得等到你用到它的时候 ldc #2 会把a符号变为字符串“a”对象。当然他变为a字符串对象以后他还要做一件事情,他会准备好一块空间-stringtable,在数据结构上是一个hash表,刚开始是空的。那我变为a字符串以后啊,我就会去stringtable里面找key,看有没有取值相同的。第一次找是没有的,没有的话他就会把这个“a"字符串对象放入串池。以后执行类似的。
-
注意串池中对象是唯一的。
tostring()的底层实现new出一个对象
综上得出结果
String s5 = "a" + "b";
s3和s5相等,
地址都是一样的,并且s5找的时候直接去常量池的#4的位置找ab这个串。那这个他底层是怎么做的呢?
-
这个是javac在编译期的优化,他认为你a 和b 都是常量,是拼接的,结果肯定也是常量,结果已经在编译期间确定为ab。而我们的s4=s1+s2这一行代码,s1和s2是变量,那既然是变量,将来在运行的时候引用的值有可能发生修改,值不确定,所以必须用stringbuilder在运行期间动态接收。
6.7.2 字符串延迟加载
上面这些代码,并不是说一开始就全部把字符串加进串池里面去,而是我执行一行,就加一个进去。
如果再执行下面的代码。串池数量会变吗?
不会变,还是2285,串池中字符串对象是唯一的,不会变
第二个实例验证
6.7.3总结stringtable的特性
-
常量池中的字符串仅是符号,第一次用到时才变为对象
-
利用串池的机制,来避免重复创建字符串对象
-
字符串拼接的原理是StringBuilder(1.8)
-
字符串常量拼接的原理是编译期优化
-
可以使用intern方法,主动将串池中还没有的字符串对象放入串池
6.7.4 1.8中的intern()
那我能不能主动的把我的ab存入串池呢?在jdk1.8中可以调用s.intern()方法,将这个字符串对象尝试放入串池,如果没有就放入,有的话就不放。不管有没有,会把串池中的对象返回。
疑问:// s 不是堆区的吗,怎么会和常量池的 "ab" 相等?
-
注意啊,我们放入的不是s对象的值,而是把s这个堆区的字符串对象放入到串池。相当于剪切而不是复制。所以上面得出的结果为true而不是false。相当于ctrl+x + ctrl+v
现在我们修改一下代码:
public class test01 { public static void main(String[] args) { String ab = "ab"; String s = new String("a") + new String("b"); // 堆区["ab"] String s2 = s.intern(); // 串池中已经有了"ab",那就不会把堆区的s字符串对象剪切进去,返回的是串池的"ab" System.out.println(s2 == "ab"); // true System.out.println(s == "ab"); // false s是堆区的 "ab"是串池的 当然为false }
6.7.5 1.6中的intern()
1,6中的规则略有不同,当你的串池中没有你要放入的这个对象时,他会复制一份放入串池。相当于ctrl + c 和ctrl+v
6.7.4 面试题再解析
// 串池[s1:"a", s2:"b", s3 s5 s6:"ab" x1:"cd"] public class test01 { public static void main(String[] args) { String s1 = "a"; String s2 = "b"; String s3 = "a" + "b"; // 编译期优化 String s4 = s1 + s2; // 堆区[ s4:"ab" x2:"cd" String s5 = "ab"; // 因为常量池中已经有"ab",直接引用有的 String s6 = s4.intern(); System.out.println(s3 == s4); System.out.println(s3 == s5); System.out.println(s3 == s6); System.out.println("=============="); String x1 = "cd"; String x2 = new String("c") + new String("d"); x2.intern(); System.out.println(x1 == x2); // false }
如果是1.6里面:
String x2 = new String("c") + new String("d"); x2.intern(); String x1 = "cd"; System.out.println(x1 == x2);
这里呢x2.intern并不是x2入池,而是x2的副本入池,所以x1和x2是不一样的
6.7.5 StringTable的位置
1.6->1.8为什么要做这个更改呢?
-
永久代的垃圾回收很差,StringTable用的非常频繁。
-
堆中StringTable触发垃圾回收很容易,减轻内存的占用。
6.7.6 StringTable 垃圾回收‘
加上这些参数能看到具体信息
内存不够的时候,就触发一次垃圾回收
加了1万个字符串进去,但是实际只容纳了7226个,因为内存紧张触发了GC的垃圾回收机制。
6.7.7 StringTable的调优
StringTable的底层是一个哈希表,哈希表如果桶的个数多,元素相对分散,哈希碰撞的几率小,查找速度快。反之则慢。那么调优的话主要就是调整桶的个数
如果string的数量比较大的话,那就把桶的数量调的大一点。减少哈希冲突。
-
调整-XX:StringTableSize = 桶个数
-
考虑将字符串对象是否入池
twitter需要大量存储用户的地址信息,需要用30G内存才能存下,但是用户地址很多都是重复的,如果我们不加区分把重复地址都存进去,那占用空间大。他们采用方法就是用intern方法,相同地址在串池中只会占用一份,使用了之后啊。下降到了几百M。