JVM三大常量池与方法区

版本主线

hotspot从1.6到1.8,方法区的实现从永久代转移到元空间(Metaspace,位于Native Memory)。
Java 1.7作为过渡版本,率先实现部分区域的转移,分别是:

  • 字符串常量池(也就是String Table)转移到heap
  • 静态变量转移到heap(java.lang.Class对象的末尾)
  • 把SymbolTable引用的Symbol移动到了native memory

1.8在此基础上,将方法区剩余部分一鼓作气转移。
关于各种常量池的位置和组成,各种说法鱼龙混杂,笔者也是看过众多资料后,选择相信下面的结论:
静态变量 + class常量池(也被叫 静态常量池) + 类信息(构造方法/接口定义) + 运行时常量池(including String Table/Symbol Table) = 方法区

为什么要这么转移?
  • 字符串存在方法区中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于方法区的大小指定比较困难,太小容易出现方法区溢出,太大则容易导致老年代溢出。
  • 方法区会为GC带来不必要的复杂度,并且回收效率偏低。
  • Oracle 可能会将HotSpot 与 JRockit 合二为一。
验证类信息转移
while (true) {
	Enhancer enhancer = new Enhancer();
	enhancer.setSuperclass(MetaSpaceOomMock.class);
	enhancer.setCallbackTypes(new Class[]{Dispatcher.class, MethodInterceptor.class});
	enhancer.setCallbackFilter(new CallbackFilter() {
		@Override
		public int accept(Method method) {
			return 1;
		}
		
		@Override
		public boolean equals(Object obj) {
			return super.equals(obj);
		}
	});

借助cglib框架重复生成新类来验证,在JDK8上是metaspace OOM:
在这里插入图片描述

验证String Table转移的方法
// -Xmx20m -Xms20m
for (int i = 0; i < Integer.MAX_VALUE; i++) {
	list.add(String.valueOf(i).intern());
}

1.7报的异常是在heap而不是1.6的PermGen space
在这里插入图片描述
转移也影响了intern()的实现,虽然依旧是返回引用

  • 在1.6中,intern体现了运行时常量池不同于class常量池的的动态性。intern的处理是先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,则将该字符串常量加入到字符串常量区
  • 当常量池中没有该字符串时,JDK7的intern()方法的实现不再是在常量池中创建与此String内容相同的字符串,而改为在常量池中记录Java Heap中首次出现的该字符串的引用,并返回该引用。
  • String s = "abc"等效于String s = new String(“abc”).intern();在JDK7后也等效于(new String(“a”)+“bc”).intern()
    String s = "adb"也等效于String s2 = new String(“adb”)中的"adb"对象;但s和s2明显地址不同
    这几种写法都会让我们意识到StringTable的存在。
String b = "计算机";
String a = b + "软件"; //不能直接“计算机”+“软件”
System.out.println(a.intern() == a);
//JDK1.6:false,指向perm区,无论怎么样也false
//JDK1.7:true   

这里需要注意字符串的编译器优化

    1.  String s = "a" + "b";
    2.  final String s0 = "a";
    	String s = s0 +"b";
String Table的实质

字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表 HashSet,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

三个常量池的关系

静态变量 + class常量池 + 类信息(构造方法/接口定义) + 运行时常量池(including String Table/Symbol Table) = 方法区

contentlocation
静态变量heap
class常量池metaspace
类信息metaspace
运行时常量池如下两个
String Tableheap
Symbol Tablemetaspace

class文件常量池存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量。

方法区里存储着class文件的信息和运行时常量池,class文件的信息包括类信息和class文件常量池。
运行时常量池是当class文件被加载完成后,java虚拟机会将class文件常量池里的内容转移到运行时常量池里,在class文件常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

但运行时常量池里的内容是可以动态添加的。例如调用String的intern方法就能将string的值添加到String常量池中。
在这里插入图片描述
在这里插入图片描述

详细解析String Table

解析题目本来打算写注释的方式来解释的,但好像挺难说清楚的。我还是画图吧…

public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);// false
}

第一句:String s = new String(“1”);
在这里插入图片描述
第二句:s.intern();发现字符串常量池中已经存在"1"字符串对象,直接返回字符串常量池中对堆的引用(但没有接收)–>此时s引用还是指向着堆中的对象
在这里插入图片描述
第三句:String s2 = “1”;发现字符串常量池已经保存了该对象的引用了,直接返回字符串常量池对堆中字符串的引用
在这里插入图片描述
很容易看到,两条引用是不一样的!所以返回false。

public static void main(String[] args) {
        String s3 = new String("1") + new String("1");
        s3.intern();
        String s4 = "11";
        System.out.println(s3 == s4); // true
    }

第一句:String s3 = new String(“1”) + new String(“1”);注意:此时"11"对象并没有在字符串常量池中保存引用。
在这里插入图片描述
第二句:s3.intern();发现"11"对象并没有在字符串常量池中,于是将"11"对象在字符串常量池中保存当前字符串的引用,并返回当前字符串的引用(但没有接收)
在这里插入图片描述
第三句:String s4 = “11”;发现字符串常量池已经存在引用了,直接返回(拿到的也是与s3相同指向的引用)
在这里插入图片描述
根据上述所说的:最后会返回true~~~如果还是不太清楚的同学,可以试着接收一下intern()方法的返回值,再看看上述的图,应该就可以理解了。下面的就由各位来做做,看是不是掌握了:

public static void main(String[] args) {
        String s = new String("1");
        String s2 = "1";
        s.intern();
        System.out.println(s == s2);//false

        String s3 = new String("1") + new String("1");
        String s4 = "11";
        s3.intern();
        System.out.println(s3 == s4);//false
    }

还有:

public static void main(String[] args) {
        String s1 = new String("he") + new String("llo");
        String s2 = new String("h") + new String("ello");
        String s3 = s1.intern();
        String s4 = s2.intern();
        System.out.println(s1 == s3);// true
        System.out.println(s1 == s4);// true
    }

池化技术

除了String之外也有包装类用到池化技术,常见的考题如Integer的池化范围是-128~127

public static void main(String [] args){
    Integer a=100;
    Integer b=100;
    System.out.println(a==b);//true
     Integer c=200;
     Integer d=200;
    System.out.println(c==d);//false 
}

Integer a=100;这句代码会进行自动装箱,实际会调用Integer.valueOf(100)将100转化成Integer类型的Integer类中valueOf方法的实现中,对数据分开处理的:当数据在-128到127之间a和b就使用同一个对象
当数据不在这个范围之内比如c和d就使用不同的对象。然后用c和d比较两个对象的内存地址就是false

public static Integer valueOf(int i) {
    if(i >= -128 && i <= IntegerCache.high)
        return IntegerCache.cache[i + 128];
    else
        return new Integer(i);
}

Integer i3 = new Integer(10);
Integer i4 = new Integer(10);
但这种写法和new String(“hello”)一样已明确创建了新对象,因此地址不同。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值