而JDK 7(以及部分其他虚拟机, 例如JRockit)的intern()
方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()
返回的引用和由StringBuilder创建的那个字符串实例就是同一个。
而对str2比较返回false,这是因为“java”(它是在加载sun.misc.Version
这个类的时候进入常量池的)这个字符串在执行StringBuilder.toString()
之前就已经出现过了,字符串常量池中已经有它的引用(因此,它指向的是其他对象实例的应用,而不是str2对象实例的应用),不符合intern()
方法要求“首次遇到”的原则。
通过java -version
确实是可以看到JVM中这个的java
字符串确实已存在字符串常量池:
java version "11.0.10" 2021-01-19 LTS Java(TM) SE Runtime Environment 18.9 (build 11.0.10+8-LTS-162) Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.10+8-LTS-162, mixed mode) 复制代码
而“StringTest”这个字符串则是首次出现的,因此,结果返回true。
String
的intern()
方法的使用总结:
- JDK6中,如果串池中有,则不会放入,返回已有的串池中的对象地址;如果串池中没有,会把此对象复制一份,类似于new,放入串池,并返回串池中的对象地址
- JDK7/8中,如果串池中有,则不会放入,返回已有的串池中的对象地址;如果串池中没有,会把对象的引用地址复制一份,放入串池,并返回串池中的对象地址
方法区内存溢出
方法区的主要职责是用于存放类型的相关信息, 如类名、 访问修饰符、 常量池、 字段描述、 方法描述等。
异常原因
该区域内存溢出通常是运行时产生大量的类去填满了方法区。比如:CGLib直接操作字节码运行时生成了大量的动态类。另外,很多运行于Java虚拟机上的动态语言(例如Groovy等)通常都会持续创建新类型来支撑语言的动态性,随着这类动态语言的流行,类似的溢出场景也越来越容易遇到。
异常现象及预防措施
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,要达成的条件是比较苛刻的。在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。
在JDK 7中的运行结果如下所示:
Caused by: java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616) ... 8 more 复制代码
这类场景除了之前提到的程序使用了CGLib字节码增强和动态语言外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类) 、 基于OSGi的应用(即使是同一个类文件, 被不同的加载器加载也会视为不同的类)等。
在JDK 8以后, 永久代便完全退出了历史舞台, 元空间作为其替代者登场。在默认设置下, 前面列举的那些正常的动态创建新类型已经很难再迫使虚拟机产生方法区的溢出异常了。 不过为了让使用者有预防实际应用里出现类似具有破坏性的操作, HotSpot还是提供了一些参数作为元空间的防御措施,主要包括:
-XX:MaxMetaspaceSize
:设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。-XX:MetaspaceSize
:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值; 如果释放了很少的空间,那么在不超过-XX: MaxMetaspaceSize
(如果设置了的话) 的情况下,适当提高该值。-XX:MinMetaspaceFreeRatio
:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比, 可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio
, 用于控制最大的元空间剩余容量的百分比。
本机直接内存溢出
直接内存(Direct Memory
)的容量大小可通过-XX:MaxDirectMemorySize
参数来指定, 如果不去指定, 则默认与Java堆最大值(由-Xmx
指定) 一致。
本机直接内存溢出异常演示
下面演示使用unsafe分配本机内存,出现内存溢出,代码如下所示:
/** * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M * @author zzm */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } } 复制代码
上面的代码越过了DirectByteBuffer
类直接通过反射获取Unsafe
实例进行内存分配(Unsafe类的getUnsafe()
方法指定只有引导类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能, 在JDK 10时才将Unsafe的部分功能通过VarHandle开放给外部使用), 虽然使用DirectByteBuffer
分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常, 真正申请分配内存的方法是Unsafe::allocateMemory()
。
以上代码的运行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20) 复制代码
解决方案
由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况, 如果发现内存溢出之后产生的Dump文件很小, 而程序中又直接或间接使用了DirectMemory
(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。
总结
虽然Java有垃圾收集机制, 但内存溢出异常离我们并不遥远。因此,我们需要熟悉JVM各个内存可能发生的内存溢出异常及其解决方法。