JVM运行时内存数据区域

1 讨论背景

周志明老师写的《深刻理解Java虚拟机》应该不少程序员都读过,第二章中阐述了Java虚拟机在执行Java程序的过程当中是如何管理内存的,以及这些内存是如何被划分红更细的逻辑区域的。以下图所示,按照书中的论述JVM运行时数据区域包含如下几个数据区[1]。java

按照《Java虚拟机规范(Java SE 7版)》,各区域的功能简要介绍以下:程序员

  • 程序计数器:各线程私有。用于记录每一个线程下一条待执行的字节码指令以及相关信息。这是惟一的不会抛出OOM异常的区域。
  • Java虚拟机栈:各线程私有。虚拟机栈由一个个的栈帧组成,每一个栈帧包含了对应方法执行所须要的信息,具体包括:局部变量表、操做数栈(相似于编译型语言体系下的数据寄存器)、动态连接(某些接口符号可能会动态的指向不一样的目标方法)、函数返回地址以及其余一些相关信息。理论上当函数调用链超过栈的深度时就会触发StackOverflow,当该区域设置为动态扩展时,虚拟机没法为栈申请到更多内存时就会触发OOM。事实中基本上无论哪一种状况,结果都极可能会是StackOverflow,由于栈容量和栈帧的大小决定了栈的深度(栈帧大小*深度<=栈容量),因此当OOM时,栈深度必定也已经不够用了,因此抛出StackOverflow异常也无可厚非。能够经过“-Xss”来配置虚拟机栈固定大小。
  • Java堆:各线程公有。虚拟机工做的主要内存区域(大部分状况下也是最大的),绝大部分对象实例的内存分配都在这里进行。Java 7和以前的Java堆细分为:新生代(伊甸区、存活区0、存活区1)、年老代和永久代。Java 8去除了永久代,替换以Metaspace。在JVM的运行中,大部分状况下,GC主要就发生在堆区域,
  • 方法区:各线程公有。用于存放类定义、常量池、静态变量(static修饰)、编译后的字节码等。方法区其实是从堆上划分出来的一块区域,可是其GC机制是单独的,与堆不一样,因此为了区分方法区和堆,一般又把方法区叫作“非堆”。方法区对应了堆中的永久代。所以在Java8以及以后版本中,永久代被抹除了,方法区也移到了元数据空间(metaspace)中。
  • 运行时常量池:各线程公有。用于存放类信息中的常量(字面量、符号引用等),每一个类编译后的信息中的都有一个常量池,能够经过javap -vebose xxxx.class命令来查看。
  • 直接内存:进程间公有。直接内存不属于Java虚拟机运行时数据区的一部分,它是指操做系统分配给虚拟机以及其余进程所运行的那块内存区域,之因此这么说,是由于不少服务器都是虚拟机(操做系统级别),对于物理机来讲,这块内存就是指操做系统所管控的物理内存。经过在堆中建立一个DirectByteBuffer实例来对直接内存进行访问。

不少读者了解完这些后仍是云里雾里,各论坛仍是会出现各类没有定论的问题,好比服务器

  1. 字符串常量池属于哪一个数据区?书中对字符串常量池和运行时常量池描述的至关晦涩和模糊。
  2. Java六、Java7和Java8的运行时内存数据区域到底有何不同?
  3. 什么是字面量,什么又是字符串常量?
  4. 什么是本地内存?他和直接内存相同嘛?什么又是堆外内存

下面咱们围绕这几个问题作一些讨论和引伸,从而帮助咱们更好的理解运行时数据区域划分。数据结构

2 字符串常量池

咱们先来回答第一和第二个问题。app

2.1 字符串常量池在哪

在不一样的Java版本中,规范规定的字符串常量池的位置也不同。如下三张图分别表明了Java六、Java7和Java8体系下的Java虚拟机与运行时数据区域划分,哪些是线程私有,哪些是线程公有,哪些又是进程间公有都比较清晰了。函数

2.1.1 Java 6 虚拟机运行数据区

Java 6 内存数据区域划分

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

2.1.2 Java 7 虚拟机运行数据区

Java 7 内存数据区域划分

从Java 7 开始,规范将字符串常量池迁移到了Java堆中,受Java堆大小的限制。当频繁大量使用String.intern()时,可能会引起OOM(Java heap space)。spa

2.1.3 Java 8 虚拟机运行数据区

Java 8 内存数据区域划分

Java 8 虚拟机规范完全移除了永久代(-XX:Permsize和-XX:MaxPermsize均已失效),替而代之的则是元空间(Metaspace)字符串常量池仍然在Java堆中,但方法区已经迁移到了元空间中。这时候因为滥用 String.intern()引起的OOM依旧在Java堆中。操作系统

2.2 字符串常量池是啥

那么字符串常量池的数据结构是怎么实现的呢?答案是HashMap,每一个字符串常量池对应了一个StringTable的数据结构,其本质并非Table,而是一个HashMap。这个HashMap的容量是固定的(默认1009),能够经过-XX:StringTableSize来设置,注意这个值是指哈希表中桶的数量,不是占用内存的大小。因此这个值最好是一个质数,而且要大于默认的1009[2]。线程

3 字面量和字符串常量

如如下代码:

String str = "123";

其中”123”就是咱们常常看到的“字面量”。字面量是随着Class信息等在类被加载完毕后一块儿进入运行时常量池的。 而

String str2 = str.intern();

这句代码则尝试将str的值放入字符串常量池,然而”123”已经在类信息的常量池中了,因此StringTable实际记录的是类信息常量池中该字符串的引用。

对于语句:

String str = new StringBuilder("hello").append(" world").toString().intern();

这会将新建立的“hello world”的堆内对象引用(str)放入到字符串常量池中,由于这是第一次出现,没有其余地方存在该值的引用。

4 本地内存和直接内存

首先须要说明的是,本地内存(Native Memory)和堆外内存(Off-heap Memory)的含义是同样的。而关于直接内存本地内存的关系,StackOverflow上也没有说清楚的帖子,第二部分中的三张图已经能够很好的说明直接内存和本地内存的关系了,所谓的本地内存是操做系统分配给JVM虚拟机(做为一个进程)使用的内存块中除去堆的那一部分。而直接内存则是全部进程共享的操做系统所控制的内存。因此能够这么说:本地内存和直接内存的关系就像“苹果”和“水果”的关系,苹果属于水果,是水果更具体的限定。Java8中的元空间就属于本地内存空间,而他们都是直接内存的一部分。 经过DirectByteBuffer分配的内存区域必定在本地内存中,它也受直接内存大小的限制。本地内存的大小也有限制,好比Window中对每一个程序运行所需的内存大小作了2G的默认限制,这只时候其上运行的JVM的本地内存大小≈2G-JVM堆内存大小。

5 字符串常量池所属数据区的具体说明

下面咱们举2个例子讨论下在Java6和Java7(含以后版本)下字符串常量池迁移带来的变化

5.1 例子1

请给出如下代码抛出异常的类型:

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)一直,都是堆内存溢出。

5.2 例子2

如下代码在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中会输出:

false
false

在Java7中则输出:

true
 false

首先咱们分析下Java6中的场景,Java6中字符串常量池仍是运行时常量池的一部分,因此使用String.intern()时,会把堆中的字符串复制到方法区中,返回的是方法区中的对象引用。因此无论如何,堆中对象和方法区中对象应用都不会想等。 而在Java7中,这个状况发生了变化,字符串常量池转移到了堆中,对于str1来讲,字符串常量池StringTable会记录其在堆中的引用(即str1)。因此str1.intern() == str1成立。而str2状况则不同了,由于“Hello Java”字符串已经存在于方法区的运行时常量池中,因此intern()返回的是方法区中的对象引用。因此str2.intern() == str2不成立。

相关文章
相关标签/搜索