JVM内存模型如上图,须要声明一点,这是《Java虚拟机规范(Java SE 7版)》规定的内容,实际区域由各JVM本身实现,因此可能略有不一样。如下对各区域进行简短说明。java
程序计数器是众多编程语言都共有的一部分,做用是标示下一条须要执行的指令的位置,分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖程序计数器完成的。算法
对于Java的多线程程序而言,不一样的线程都是经过轮流得到cpu的时间片运行的,这符合计算机组成原理的基本概念,所以不一样的线程之间须要不停的得到运行,挂起等待运行,因此各线程之间的计数器互不影响,独立存储。这些数据区属于线程私有的内存。编程
VM虚拟机栈也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每一个方法在执行的同时都会建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每个方法调用直至执行完的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。多线程
有人将java内存区域划分为栈与堆两部分,在这种粗略的划分下,栈标示的就是当前讲的虚拟机栈,或者是虚拟机栈对应的局部变量表。之因此说这种划分比较粗略是角度不一样,这种划分方法关心的是新申请内存的存在空间,而咱们目前谈论的是JVM总体的内存划分,因为角度不一样,因此划分的方法不一样,没有对与错。编程语言
局部变量表存放了编译期可知的各类基本类型,对象引用,和returnAddress。其中64位长的long和double占用了2个局部变量空间(slot),其余类型都占用1个。这也从存储的角度上说明了long与double本质上的非原子性。局部变量表所需的内存在编译期间完成分配,当进入一个方法时,这个方法在栈帧中分配多大的局部变量空间是彻底肯定的,在方法运行期间不会改变局部变量表大小。函数
因为栈帧的进出栈,显而易见的带来了空间分配上的问题。若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverFlowError异常;若是虚拟机栈能够扩展,扩展时没法申请到足够的内存,将会抛出OutOfMemoryError。显然,这种状况大多数是因为循环调用与递归带来的。性能
本地方法栈与虚拟机栈的做用十分相似,不过本地方法是为native方法服务的。部分虚拟机(好比 Sun HotSpot虚拟机)直接将本地方法栈与虚拟机栈合二为一。与虚拟机栈同样,本地方法栈也会抛出StactOverFlowError与OutOfMemoryError异常。优化
至此,线程私有数据区域结束,下面开始线程共享数据区。ui
Java堆是虚拟机所管理的内存中最大的一块,在虚拟机启动时建立,此块内存的惟一目的就是存放对象实例,几乎全部的对象实例都在对上分配内存。JVM规范中的描述是:全部的对象实例以及数据都要在堆上分配。可是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配(对象只存在于某方法中,不会逃逸出去,所以方法出栈后就会销毁,此时对象能够在栈上分配,方便销毁),标量替换(新对象拥有的属性能够由现有对象替换拼凑而成,就不必真正生成这个对象)等优化技术带来了一些变化,目前并不是全部的对象都在堆上分配了。spa
当java堆上没有内存完成实例分配,而且堆大小也没法扩展是,将会抛出OutOfMemoryError异常。Java堆是垃圾收集器管理的主要区域。
方法区与java堆同样,是线程共享的数据区,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译的代码。JVM规范将方法与堆区分开,可是HotSpot将方法区做为永久代(Permanent Generation)实现。这样方便将GC分代手机方法扩展至方法区,HotSpot的垃圾收集器能够像管理Java堆同样管理方法区。可是这种方向已经逐步在被HotSpot替换中,在JDK1.7的版本中,已经把本来存放在方法区的字符串常量区移出。
至此,JVM规范所声明的内存模型已经分析完毕,下面将分析一些常常提到的与内存相关的区域。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池(Constant Poll Table)用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池存放。
其中字符串常量池属于运行时常量池的一部分,不过在HotSpot虚拟机中,JDK1.7将字符串常量池移到了java堆中,经过下面的实验能够很容易看到。
import java.util.ArrayList; import java.util.List; /** * Created by shining.cui on 2017/7/23. */ public class RunTimeContantPoolOOM { public static void main(String[] args) { List list = new ArrayList(); int i = 0; while(true){ list.add(String.valueOf(i++).intern()); } } }
在jdk1.6中,字符串常量区是在Perm Space中的,因此能够将Perm Spacce设置的小一些,XX:MaxPermSize=10M能够很快抛出异常:java.lang.OutOfMemoryError:Perm Space。
在jdk1.7以上,字符串常量区已经移到了Java堆中,设置-Xms:64m -Xmx:64m,很快就能够抛出异常java.lang.OutOfMemoryError:java.heap.space。
直接内存不是JVM运行时的数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中引入了NIO(New Input/Output)类,引入了一种基于通道(Chanel)与缓冲区(Buffer)的I/O方式,他可使用Native函数库直接分配堆外内存,而后经过一个存储在Java中的DirectByteBuffer对象做为对这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在Java对和Native对中来回复制数据。
最基础的垃圾收集算法是“标记-清除”(Mark Sweep)算法,正如名字同样,算法分为2个阶段:1.标记处须要回收的对象,2.回收被标记的对象。标记算法分为两种:1.引用计数算法(Reference Counting) 2.可达性分析算法(Reachability Analysis)。因为引用技术算法没法解决循环引用的问题,因此这里使用的标记算法均为可达性分析算法。
如图所示,当进行过标记清除算法以后,出现了大量的非连续内存。当java堆须要分配一段连续的内存给一个新对象时,发现虽然内存清理出了不少的空闲,可是仍然须要继续清理以知足“连续空间”的要求。因此说,这种方法比较基础,效率也比较低下。
为了解决效率与内存碎片问题,复制(Copying)算法出现了,它将内存划分为两块相等的大小,每次使用一块,当这一块用完了,就讲还存活的对象复制到另一块内存区域中,而后将当前内存空间一次性清理掉。这样的对整个半区进行回收,分配时按照顺序从内存顶端依次分配,这种实现简单,运行高效。不过这种算法将原有的内存空间减小为实际的一半,代价比较高。
从图中能够看出,整理后的内存十分规整,可是白白浪费通常的内存成本过高。然而这实际上是很重要的一个收集算法,由于如今的商业虚拟机都采用这种算法来回收新生代。IBM公司的专门研究代表,新生代中的对象98%都是“朝生夕死”的,因此不须要按照1:1的比例来划份内存。HotSpot虚拟机将Java堆划分为年轻代(Young Generation)、老年代(Tenured Generation),其中年轻代又分为一块Eden和两块Survivor。
全部的新建对象都放在年轻代中,年轻代使用的GC算法就是复制算法。其中Eden与Survivor的内存大小比例为8:2,其中Eden由1大块组成,Survivor由2小块组成。每次使用内存为1Eden+1Survivor,即90%的内存。因为年轻代中的对象生命周期每每很短,因此当须要进行GC的时候就将当前90%中存活的对象复制到另一块Survivor中,原来的Eden与Survivor将被清空。可是这就有一个问题,咱们没法保证每次年轻代GC后存活的对象都不高于10%。因此在当活下来的对象高于10%的时候,这部分对象将由Tenured进行担保,即没法复制到Survivor中的对象将移动到老年代。
复制算法在极端状况下(存活对象较多)效率变得很低,而且须要有额外的空间进行分配担保。因此在老年代中这种状况通常是不适合的。
因此就出现了标记-整理(Mark-Compact)算法。与标记清除算法同样,首先是标记对象,然而第二步是将存货的对象向内存一段移动,整理出一块较大的连续内存空间。