Java内存区域和常量池的总结
本文用最简洁的描述,来总结出Java内存区域和常量池的相关知识,如需更加深刻学习Java内存区域以及常量池,可参考阅读《深刻Java虚拟机》或者网上优秀博文。java
运行时数据区
运行数据区包含如下几个区域:数组
- 方法区(Method Area)
- Java堆(Heap)
- 本地方法栈(Native Method Stack)
- 虚拟机栈(VM Stack)
- 程序计数器(Program Conter Register)
其中方法区和堆是全部线程共享的数据区,而其余三个区收拾线程隔离的数据区。数据结构
Java虚拟机在执行Java程序的过程当中将它管理的内存划分为若干个不一样的区域,这些区域都拥有各自的用途以及建立和销毁的时间。函数
方法区和堆都是依赖虚拟机线程的启动而建立;本地方法栈、虚拟机栈和程序计数器都依赖用户线程的启动和结束而创建和销毁。学习
程序计数器spa
- 该区是一块较小的内存空间。
- 能够看做是字节码的行号指示器。
- 分支、循环、跳转、异常处理、线程恢复等基础工做都是基于程序计数器来完成的,由于字节码解释器工做的时候,就是根据程序计数器的值来确认下一条须要执行的字节码子令。
- 该区是线程独立的,被称为“线程私有”的内存。
- 该区域是惟一一个在Java虚拟机规范中没有规定任何OOM(OutOfMemoryError)状况的区域。
Java虚拟机栈线程
- 该区是线程私有的。
- Java虚拟机栈描述的是Java方法的内存模型。
- 该区就是所谓的“栈内存”,实际上Java内存不可粗糙的分为堆内存和栈内存,由于Java内存区域的划分比这复杂的多。
- 该区会抛出StackOverflowError异常和OOM(OutOfMemoryError)异常;当线程请求的栈深度大于虚拟机所容许的深度时,会抛出栈溢出异常。由于虚拟机栈是能够动态扩展的,因此当虚拟机栈没法申请到足够的内存时,就会抛出OOM异常。
关于Java方法的内存模型指针
每一个Java方法在执行的同时,都会建立一个栈帧存放于栈中,而该栈帧中会存储局部变量表、操做数栈、动态链接、方法出口等信息。当方法结束调用,栈帧就会出栈。cdn
局部变量表对象
局部变量表存放:
- 编译期可知的基本数据类型(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;