首页 > 架构设计 > 设计模式 > JVM运行时内存数据区域
周志明老师写的《深入理解Java虚拟机》应该很多程序员都读过,第二章中阐述了Java虚拟机在执行Java程序的过程中是如何管理内存的,以及这些内存是如何被划分成更细的逻辑区域的。如下图所示,按照书中的论述JVM运行时数据区域包含以下几个数据区[1]。

按照《Java虚拟机规范(Java SE 7版)》,各区域的功能简要介绍如下:
很多读者了解完这些后还是云里雾里,各论坛还是会出现各种没有定论的问题,比如
下面我们围绕这几个问题做一些讨论和引申,从而帮助我们更好的理解运行时数据区域划分。
我们先来回答第一和第二个问题。
在不同的Java版本中,规范规定的字符串常量池的位置也不一样。以下三张图分别代表了Java6、Java7和Java8体系下的Java虚拟机与运行时数据区域划分,哪些是线程私有,哪些是线程公有,哪些又是进程间公有都比较清晰了。
2.1.1 Java 6 虚拟机运行数据区

当我们听到“字符串常量池也是方法区的一部分”的时候,我们要知道他大概暗指的是Java 6或者之前的版本。如上图所示,在Java 6虚拟机规范中,字符串常量池确实是方法区的一部分,受永久代内存区大小的限制。当频繁使用Spring.intern()时,可能会引发OOM(PermGen space)。
2.1.2 Java 7 虚拟机运行数据区

从Java 7 开始,规范将 字符串常量池迁移到了 Java堆中,受Java堆大小的限制。当频繁大量使用String.intern()时,可能会引发OOM(Java heap space)。
2.1.3 Java 8 虚拟机运行数据区

Java 8 虚拟机规范彻底移除了永久代(-XX:Permsize和-XX:MaxPermsize均已失效),替而代之的则是 元空间(Metaspace)。 字符串常量池仍然在 Java堆中,但方法区已经迁移到了元空间中。这时候由于滥用 String.intern()引发的OOM依旧在Java堆中。
那么字符串常量池的数据结构是怎么实现的呢?答案是HashMap,每个字符串常量池对应了一个StringTable的数据结构,其本质并不是Table,而是一个HashMap。这个HashMap的容量是固定的(默认1009),可以通过 -XX:StringTableSize来设置,注意这个值是指哈希表中桶的数量,不是占用内存的大小。所以这个值最好是一个质数,并且要大于默认的1009[2]。
如以下代码:
String str = "123";
其中”123”就是我们经常看到的“字面量”。字面量是随着Class信息等在类被加载完毕后一起进入 运行时常量池的。 而
String str2 = str.intern();
这句代码则尝试将str的值放入字符串常量池,然而”123”已经在类信息的常量池中了,所以StringTable实际记录的是类信息常量池中该字符串的引用。
对于语句:
String str = new StringBuilder("hello").append(" world").toString().intern();
这会将新创建的“hello world”的堆内对象引用(str)放入到 字符串常量池中,因为这是第一次出现,没有其他地方存在该值的引用。
首先需要说明的是, 本地内存(Native Memory)和 堆外内存(Off-heap Memory)的含义是一样的。而关于 直接内存和 本地内存的关系,StackOverflow上也没有说清楚的帖子,第二部分中的三张图已经可以很好的说明直接内存和本地内存的关系了,所谓的本地内存是操作系统分配给JVM虚拟机(作为一个进程)使用的内存块中除去堆的那一部分。而直接内存则是所有进程共享的操作系统所控制的内存。所以可以这么说:本地内存和直接内存的关系就像“苹果”和“水果”的关系,苹果属于水果,是水果更具体的限定。Java8中的元空间就属于本地内存空间,而他们都是直接内存的一部分。 通过DirectByteBuffer分配的内存区域一定在本地内存中,它也受直接内存大小的限制。本地内存的大小也有限制,比如Window中对每个程序运行所需的内存大小做了2G的默认限制,这只时候其上运行的JVM的本地内存大小≈2G-JVM堆内存大小。
下面我们举2个例子讨论下在Java6和Java7(含之后版本)下字符串常量池迁移带来的变化
请给出以下代码抛出异常的类型:
import java.util.ArrayList;import java.util.List;public class Test { public static void main(String[] args){ List<String> list = new ArrayList<String>(); int i = 0; while(true) { list.add( String.valueOf(i++).intern()); } }}
然后启动参数中我们加上:
-XX:PermSize=10M -XX:MaxPermSize=10M
分析下这个代码,其意图在于不断的产生新的字符串,并且放入 字符串常量池中,试图撑爆永久代。然而这只会在Java 6 中发生,对于Java7和Java8来说,字符串常量池已经迁移到了 Java堆中,如果这时候我们添加以下虚拟机参数:
-Xms10M -Xmx10M
则会引发:java.lang.OutOfMemoryError: GC overhead limit exceeded 这样的错误,这个异常的本质与 OOM(Heap space)一直,都是堆内存溢出。
以下代码在Java6和Java7中输出也不相同:
public class TestStringConstantPool { public static String hello = "Hello Java"; public static void main(String[] args) { String str1 = new StringBuilder("Hello ").append("World").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("Hello ").append("Java").toString(); System.out.println(str2.intern() == str2); }}
在Java6中会输出:
falsefalse
在Java7中则输出:
truefalse
首先我们分析下Java6中的场景,Java6中字符串常量池还是运行时常量池的一部分,所以使用String.intern()时,会把堆中的字符串复制到方法区中,返回的是方法区中的对象引用。所以不管如何,堆中对象和方法区中对象应用都不会想等。 而在Java7中,这个情况发生了变化,字符串常量池转移到了堆中,对于 str1来说,字符串常量池StringTable会记录其在堆中的引用(即str1)。所以 str1.intern() == str1成立。而 str2情况则不一样了,因为 “Hello Java”字符串已经存在于方法区的运行时常量池中,所以intern()返回的是方法区中的对象引用。所以 str2.intern() == str2不成立。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31478593/viewspace-2714845/ ,如需转载,请注明出处,否则将追究法律责任。