常量池

概念

String.intern()是一个Native方法。

如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

不同版本下常量池位置

JDK1.6方法区

JDK1.7堆内存,官方文档

Area: HotSpot
Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.
RFE: 6962931

JDK1.8元空间,和堆相独立,本地内存,About G1 Garbage Collector, Permanent Generation and Metaspace

改变的原因

  • PermGen需要的大小很难预测,它导致配置不足或触发java.lang.OutOfMemoryError:Permgen大小错误或过度配置导致资源浪费。
  • GC性能改进,使得无需GC暂停和特定元数据迭代器的并发类数据分配成为可能
  • 支持进一步优化,诸如G1并发类卸载

所以如果对PermGen熟悉,那么所需要知道就是,不管在Java8以前的版本中PermGen有什么(类的名称和字段,类方法的字节码,常量池,JIT优化等等),现在都分配在Metaspace中。

元空间大小要求取决于加载的类的数量以及这些类声明的大小,所以很容易看到导致java.lang.OutOfMemoryError:Metaspace异常的主要原因,要么是类太多,要么是太大的类被加载到Metaspace

元数据区泄漏实例

ClassLoader
  • 接口类ClassA
1
2
3
4
5
package com.super2bai.jvm.metaspace;

public interface ClassA {
void method(String input);
}
  • 实现类ClassAImpl
1
2
3
4
5
6
7
8
9
package com.super2bai.jvm.metaspace;

public class ClassAImpl implements ClassA{

@Override
public void method(String input) {
// TODO Auto-generated method stub
}
}
  • Handler类ClassAInvocationHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.super2bai.jvm.metaspace;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class ClassAInvocationHandler implements InvocationHandler{
private Object classAImpl;

public ClassAInvocationHandler(Object impl) {
this.classAImpl=impl;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(classAImpl, args);
}
}
  • 入口类ClassMetadataLeakSimulator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.super2bai.jvm.metaspace;

import java.lang.reflect.Proxy;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;

public class ClassMetadataLeakSimulator {
private static Map<String, ClassA> classLeakingMap = new HashMap<>();

public static void main(String[] args) {
int i = 0;
try {
while (true) {
String fictiousClassloaderJAR = "file:" + (i++) + ".jar";
URL[] fictiousClassloaderURL=new URL[]{new URL(fictiousClassloaderJAR)};
URLClassLoader newClassLoader=new URLClassLoader(fictiousClassloaderURL);
ClassA t=(ClassA)Proxy.newProxyInstance(newClassLoader, new Class<?>[]{ClassA.class}, new ClassAInvocationHandler(new ClassAImpl()));
classLeakingMap.put(fictiousClassloaderJAR, t);
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
  • JVM参数
1
-verbose:gc -XX:MaxMetaspaceSize=20M -XX:MetaspaceSize=10M
  • 运行结果:java.lang.OutOfMemoryError: Metaspace

JVM-1.8-metaspace溢出

CGLib
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.super2bai.jvm.metaspace.cglib;

import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

public class MetaspaceOOM {
static class OOMObject {

}

public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invoke(obj, args);
}
});
enhancer.create();
}
}
}
  • JVM参数
1
-verbose:class -XX:MetaspaceSize=3M -XX:MaxMetaspaceSize=3M -XX:+TraceClassLoading -XX:+TraceClassUnloading
  • 运行结果

CGLib

有意思的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.super2bai.jvm;

public class RuntimeConstantPoolOOM {

public static void main(String[] args) {
// true
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println("str1.intern()==str1 : " + (str1.intern() == str1));

// false
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println("str2.intern()==str2 : " + (str2.intern() == str2));
}
}
  • JDK1.7JDK1.8中运行,会得到一个true一个false
  • 因为JDK1.7+intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。
  • str2比较返回false是因为java这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

关于第三点,有个疑问。如果是第一次出现,则结果为true。不是第一次出现,结果为false。 拼接了ja和va,比较结果一直都是false。那么问题来了,既然不是首次出现,那java这词是什么时候进入到的常量池。为什么要进入。

具体可参考此篇文章,写的比较不错。

关于String.intern()的一个疑惑

此处不再赘述。

收获

不介绍版本就讨论JVM都是耍流氓的行为。

参考

《深入理解JVM》

String.intern in Java 6, 7 and 8 – string pooling

关于String.intern()的一个疑惑

如何理解《深入理解java虚拟机》第二版中对String.intern()方法的讲解中所举的例子?

Java中几种常量池的区分

Understand the OutOfMemoryError Exception