本文用最简洁的描述,来总结出 Java 内存区域和常量池的相关知识,如需更加深入学习Java内存区域以及常量池,可参考阅读《深入Java虚拟机》或者网上优秀博文。
运行时数据区
运行数据区包含以下几个区域:
方法区(Method Area)
Java堆(Heap)
本地方法栈(Native Method Stack)
虚拟机栈(VM Stack)
程序计数器(Program Conter Register)
其中方法区和堆是所有线程共享的数据区,而其他三个区收拾线程隔离的数据区。
Java虚拟机在 执行Java程序的过程中 将它管理的内存划分为若干个不同的区域,这些区域都拥有各自的用途以及创建和销毁的时间。
方法区和堆都是依赖虚拟机线程的启动而创建;本地方法栈、虚拟机栈和程序计数器都依赖用户线程的启动和结束而建立和销毁。
程序计数器
该区是一块较小的内存空间。
可以看作是字节码的行号指示器。
分支、循环、跳转、异常处理、线程恢复等基础工作都是基于程序计数器来完成的,因为字节码解释器工作的时候,就是根据程序计数器的值来确认下一条需要执行的字节码子令。
该区是线程独立的,被称为“线程私有”的内存。
该区域是唯一一个在Java虚拟机规范中没有规定任何OOM(OutOfMemoryError)情况的区域。
Java虚拟机栈
该区是线程私有的。
Java虚拟机栈描述的是Java方法的内存模型。
该区就是所谓的“栈内存”,实际上Java内存不可粗糙的分为堆内存和栈内存,因为Java内存区域的划分比这复杂的多。
该区会抛出StackOverflowError异常和OOM(OutOfMemoryError)异常;当线程请求的栈深度大于虚拟机所允许的深度时,会抛出栈溢出异常。因为虚拟机栈是可以动态扩展的,所以当虚拟机栈无法申请到足够的内存时,就会抛出OOM异常。
关于Java方法的内存模型
每个Java方法在执行的同时,都会创建一个栈帧存放于栈中,而该栈帧中会存储 局部变量表 、操作数栈、动态连接、方法出口等信息。当方法结束调用,栈帧就会出栈。
局部变量表
局部变量表存放:
编译期可知的基本数据类型(boolean、char、byte、short、long、flag、int、double)。
对象引用。
returnAddress(指向一条字节码指令的地址)。
对于局部变量表,需要注意:
局部变量表所需的内存空间在编译期就已经完成分配了,当进入一个方法时这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不能改变局部变量表的大小。
本地方法栈
本地方法栈使用到的是Native方法服务,基本的原理和Java虚拟机栈非常相似。
Sun HotSpot将本地方法栈和Java虚拟机栈合二为一。
本地方法栈也会抛出StackOverflowErrot和OOM异常。
Java堆
Java堆是虚拟机所管理的内存中最大的一块。并且是线程共享的,随着虚拟机线程的启动而建立。
此区域的唯一目的就是存储对象,几乎所有的实例对象都在该区分配内存,为什么不是全部呢?因为一个类的java.lang.Object类对象是在方法区中分配内存的。
几乎所有的对象实例以及数组都要在堆上分配内存。
此区是垃圾收集器管理的主要区域,所以此区也被称为GC堆。
Java堆可分为新生代、老年代,新生代又可细分为Eden空间、From Survivor空间、To Survivor空间。
堆内存中的区域是物理上不连续的,但逻辑上是连续的内存区域。
此区域可拓展,也可固定,一般都是定为可拓展(通过-Xmx和-Xms控制)。
堆中如果没有内存分配给实例,并且无法再拓展内存区域了,就会抛出OOM异常。
方法区
方法区是线程共享的区域。
方法区存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区被很多人称为“永久代”,因为HotSpot团队选择把GC延伸至方法区。不过现在已经有放弃永久代并逐步改为采用Native Memory来实现方法区的规划了,在JDK1.7的HotSpot中,一把原本放在永久代中的字符串常量池移出。
内存区域可以是物理上不连续的,但逻辑上是连续的,与堆内存是一致的。
方法区因为总是存放不会轻易改变的内容,故又被称之为“永久代”。HotSpot也选择把GC分代收集扩展至方法区,但也容易遇到内存溢出问题。可以选择不实现垃圾回收,但如果回收就主要涉及常量池的回收和类的卸载。
该区域无法满足内存分配需求时,会抛出OOM异常。
非运行时数据区——直接内存
直接内存不是运行时数据区的一部分,并不是Java定义规范中的内存区域,但是不合理的使用也会导致OOM异常。
JDK 1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储再Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
运行时常量池?静态常量池(class文件常量池)?字符串常量池?有什么区别?下面一去来看看这个让人容易混淆的三个概念。
Java中的常量池
class文件常量池
我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References),这就是我们所说的class文件常量池。 字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
(每种常量类型的数据结构可以查看《深入理解java虚拟机》第六章的内容)
class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
运行时常量池
运行时常量池是方法区中的一部分。
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池(Constant Pool Table)。
常量池中存储的是编译期生成的各种字面量和符号引用,在JDK1.6及其之前的版本,这部分内容都将在类加载完后进入到方法区的运行时常量池中存放。
运行时常量池相比于类常量池的不同特征在于,运行时常量池具备动态性,也就是说不一定要编译期的时候才能产生常量,也可以在运行时产生常量,比如在运行期间调用String.intern()方法,也可以将新的常量放入运行时常量池中。
运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
string.intern()作用:
检查字符串常量池中是否存在String并返回池里的字符串引用;若池中不存在,则将其加入池中,并返回其引用。 这样做主要是为了避免在堆中不断地创建新的字符串对象。
字符串常量池
字符串常量池在每个VM中只有一份,存放的是字符串常量的引用值。
字符串常量池——string pool,也叫做string literal pool。
字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中。
string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
对于string pool:
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
JDK不同版本与常量池位置的变化
在JDK1.6版本以前,运行时常量池在方法区中;JDK1.7版本是,运行时常量池是在堆中;JDK1.8时,运行时常量池是在方法区和堆区相对独立的元空间(Metaspace),而不是在堆区。
不同版本因为OOM导致的问题: JDK1.6版本——java.lang.OutOfMemoryError: PermGen space; JDK1.7版本——java.lang.OutOfMemoryError:Java heap space; JDK1.8版本——java.lang.OutOfMemoryError: Metaspace;