JVM内存模型与GC算法
1.JVM内存模型
JVM内存模型如上图,需要声明⼀点,这是《Java虚拟机规范(Java SE 7版)》规定的内容,实际区域由各JVM⾃⼰实现,所以可能略有不同。以下对各区域进⾏简短说明。
1.1程序计数器
程序计数器是众多编程语⾔都共有的⼀部分,作⽤是标⽰下⼀条需要执⾏的指令的位置,分⽀、循环、跳转、异常处理、线程恢复等基础功能都是依赖程序计数器完成的。
对于Java的多线程程序⽽⾔,不同的线程都是通过轮流获得cpu的时间⽚运⾏的,这符合计算机组成原理的基本概念,因此不同的线程之间需要不停的获得运⾏,挂起等待运⾏,所以各线程之间的计数器互不影响,独⽴存储。这些数据区属于线程私有的内存。
1.2 Java虚拟机栈
VM虚拟机栈也是线程私有的,⽣命周期与线程相同。虚拟机栈描述的是Java⽅法执⾏的内存模型:每个⽅法在执⾏的同时都会创建⼀个栈帧(Stack Frame)⽤于存储局部变量表、操作数栈、动态链接、⽅法出⼝等信息。每⼀个⽅法调⽤直⾄执⾏完的过程,就对应着⼀个栈帧在虚拟机栈中⼊栈到出栈的过程。
有⼈将java内存区域划分为栈与堆两部分,在这种粗略的划分下,栈标⽰的就是当前讲的虚拟机栈,或者是虚拟机栈对应的局部变量表。之所以说这种划分⽐较粗略是⾓度不同,这种划分⽅法关⼼的是新申
请内存的存在空间,⽽我们⽬前谈论的是JVM整体的内存划分,由于⾓度不同,所以划分的⽅法不同,没有对与错。
局部变量表存放了编译期可知的各种基本类型,对象引⽤,和returnAddress。其中64位长的long和double占⽤了2个局部变量空间(slot),其他类型都占⽤1个。这也从存储的⾓度上说明了long与double本质上的⾮原⼦性。局部变量表所需的内存在编译期间完成分配,当进⼊⼀个⽅法时,这个⽅法在栈帧中分配多⼤的局部变量空间是完全确定的,在⽅法运⾏期间不会改变局部变量表⼤⼩。
由于栈帧的进出栈,显⽽易见的带来了空间分配上的问题。如果线程请求的栈深度⼤于虚拟机所允许的深度,将抛出StackOverFlowError 异常;如果虚拟机栈可以扩展,扩展时⽆法申请到⾜够的内存,将会抛出OutOfMemoryError。显然,这种情况⼤多数是由于循环调⽤与递归带来的。
1.3 本地⽅法栈
本地⽅法栈与虚拟机栈的作⽤⼗分类似,不过本地⽅法是为native⽅法服务的。部分虚拟机(⽐如 Sun HotSpot虚拟机)直接将本地⽅法栈与虚拟机栈合⼆为⼀。与虚拟机栈⼀样,本地⽅法栈也会抛出StactOverFlowError与OutOfMemoryError异常。
⾄此,线程私有数据区域结束,下⾯开始线程共享数据区。
1.4 Java堆
Java堆是虚拟机所管理的内存中最⼤的⼀块,在虚拟机启动时创建,此块内存的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例都在对
上分配内存。JVM规范中的描述是:所有的对象实例以及数据都要在堆上分配。但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配(对象只存在于某⽅法中,不会逃逸出去,因此⽅法出栈后就会销毁,此时对象可以在栈上分配,⽅便销毁),标量替换(新对象拥有的属性可以由现有对象替换拼凑⽽成,就没必要真正⽣成这个对象)等优化技术带来了⼀些变化,⽬前并⾮所有的对象都在堆上分配了。
当java堆上没有内存完成实例分配,并且堆⼤⼩也⽆法扩展是,将会抛出OutOfMemoryError异常。Java堆是垃圾收集器管理的主要区域。
1.5 ⽅法区
⽅法区与java堆⼀样,是线程共享的数据区,⽤于存储被虚拟机加载的类信息、常量、静态变量、即时编译的代码。JVM规范将⽅法与堆区分开,但是HotSpot将⽅法区作为永久代(Permanent Generation)实现。这样⽅便将GC分代⼿机⽅法扩展⾄⽅法区,HotSpot的垃圾收集器可以像管理Java堆⼀样管理⽅法区。但是这种⽅向已经逐步在被HotSpot替换中,在JDK1.7的版本中,已经把原本存放在⽅法区的字符串常量区移出。
⾄此,JVM规范所声明的内存模型已经分析完毕,下⾯将分析⼀些经常提到的与内存相关的区域。
1.6 运⾏时常量池
运⾏时常量池是⽅法区的⼀部分。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。
1.7 直接内存
直接内存不是JVM运⾏时的数据区的⼀部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中引⼊了NIO(New Input/Output)类,引⼊了⼀种基于通道(Chanel)与缓冲区(Buffer)的I/O⽅式,他可以使⽤Native函数库直接分配堆外内存,然后通过⼀个存储在Java中的DirectByteBuffer对象作为对这块内存的引⽤进⾏操作。这样能在⼀些场景中显著提⾼性能,因为避免了在Java对和Native对中来回复制数据。
2.GC算法
2.1 标记-清除算法
最基础的垃圾收集算法是“标记-清除”(Mark Sweep)算法,正如名字⼀样,算法分为2个阶段:1.标记处需要回收的对象,2.回收被标记的对象。标记算法分为两种:1.引⽤计数算法(Reference Counting) 2.可达性分析算法(Reachability Analysis)。由于引⽤技术算法⽆法解决循环引⽤的问题,所以这⾥使⽤的标记算法均为可达性分析算法。
如图所⽰,当进⾏过标记清除算法之后,出现了⼤量的⾮连续内存。当java堆需要分配⼀段连续的内存给⼀个新对象时,发现虽然内存清理出了很多的空闲,但是仍然需要继续清理以满⾜“连续空间”的要求。
所以说,这种⽅法⽐较基础,效率也⽐较低下。
2.2 复制算法
为了解决效率与内存碎⽚问题,复制(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中的对象将移动到⽼年代。
2.3 标记-整理算法
复制算法在极端情况下(存活对象较多)效率变得很低,并且需要有额外的空间进⾏分配担保。所以在⽼年代中这种情况⼀般是不适合的。
所以就出现了标记-整理(Mark-Compact)算法。与标记清除算法⼀样,⾸先是标记对象,然⽽第⼆步是将存货的对象向内存⼀段移动,整理出⼀块较⼤的连续内存空间。
3. 总结
1. Java虚拟机规范中规定了对内存的分配,其中程序计数器、本地⽅法栈、虚拟机栈属于线程私有数据区,Java堆与⽅法区属于线程共
享数据。
2. Jdk从1.7开始将字符串常量区由⽅法区(永久代)移动到了Java堆中。
3. Java从NIO开始允许直接操纵系统的直接内存,在部分场景中效率很⾼,因为避免了在Java堆与Native堆中来回复制数据。
4. Java堆分为年轻代有年⽼代,其中年轻代分为1个Eden与2个Survior,同时只有1个Eden与1个Survior处于使⽤中状态,⼜有年轻代的
对象⽣存时间为往往很短,因此使⽤复制算法进⾏垃圾回收。
5. 年⽼代由于对象存活期⽐较长,并且没有可担保的数据区,所以往往使⽤标记-清除与标记-整理算法进⾏垃圾回收。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论