JVM内存模型

1、内存模型

  JVM通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域。而每个线程又有自己单独的工作内存,当线程与内存进行交互时,数据从主存拷贝到工作内存,进而交由线程处理。

2、运行时数据区

2.1、程序计数器(Program Counter Register)

2.1.1、简介

  又称为PC寄存器。在JVM中由于多个线程轮流切换获得CPU的执行时间,因此在任一时刻,一个CPU的内核只会执行一个线程指令。为了能使线程在切换后恢复之间的程序执行位置,每个线程需要一个独立的程序计数器,来记录程序的执行位置,独立存储,避免各线程之间互不影响。因此程序计数器是线程私有的内存区域。

2.1.2、特点

  a、内存空间较小,容量至少能保存一个ReturnAddress类型的数据或一个平台相关的本地指针的值;

  b、各个线程私有,生命周期与线程相同;

  c、记录每个线程当前的执行位置,如果执行的是一个java方法,则记录的是正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,则计数器值是undefine,因为本地方法的运行依赖于硬件PC寄存器,其值由操作系统来维护;

  d、不抛出任何异常,是内存区域中唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2.2、java栈(Java Virtual Machine Stack)

2.2.1、简介

  又称Java虚拟机栈或JVM栈,描述的是java方法执行的内存模型:Java虚拟机是基于栈架构设计的,大多数操作是从当前栈帧取出一个或多个操作数,或将结果压入操作数栈中。每调用一个方法,都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接方法、返回地址(方法出口)等信息。每一个方法被调用直到返回的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。各个线程的栈帧都是相互独立、互不侵犯的,所以这也解释了局部变量在多线程环境下的线程安全特征。

当前栈帧

  当调用一个方法时,如果其调用链很长,可能有许多方法都处于执行状态,JVM栈中相应地有多个栈帧,但对于执行引擎来说,位于JVM栈栈顶的栈帧是有效的,这个栈帧称为当前栈帧,所代表的方法称为当前方法,当前方法所属的类称为当前方法。执行引擎的所有指令都是针对当前栈帧进行操作的。

局部变量表

  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象的引用(reference类型,不等同于对象本身,根据不同的虚拟机实现,可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或者其他与对象相关的位置)和 returnAdress类型(指向下一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,在方法在运行之前,该局部变量表所需要的内存空间是固定的,运行期间也不会改变(详细信息请参见Class文件中的Code属性)。

  虚拟机栈的大小决定了方法调用的最大深度(如递归层次、嵌套调用层数等,-Xss参数配置虚拟机栈的发小)。栈的大小可以是固定的,也可以是动态扩展的。

  在虚拟机规范中,对这个区域规定了两种异常状况:(1)如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,当扩展时也无法申请到足够的内存时会抛出OutOfMemoryError异常。

2.2.2、特点

  a、栈结构,先进后出;

  b、存放的是方法执行的内存模型(栈帧),方法调用时入栈,调用返回时出栈;

  c、线程私有,生命周期与线程相同;

  d、在虚拟机规范中,会抛出StackOverflowError和OutOfMemoryError两种异常。

2.3、本地方法栈(Native Method Stacks)

  本地方法栈的作用类似于java虚拟机栈,区别在于虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为Native方法服务。即管理的不是JAVA方法,是本地方法,本地方法是用C实现的。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

  特点同虚拟机栈。

2.4、java堆(java heap)

2.4.1、简介

java堆是java虚拟机所管理的内存中最大一块内存,被所有进程所共享。此内存的唯一目的就是存放对象实例和数组,几乎所有的对象实例都在这里分配内存。java堆也是垃圾回收器管理的主要区域,因此有时候也被称为GC。从垃圾回收的角度,可以进一步细分:

New----年轻代、新生代

  用来存放刚创建的对象和年轻的对象。年轻代又划分为以下几个部分:

  Eden Space:用来存放JVM刚分配的对象

  Survivor Space0(from space):

  Survivor Space1(to space)

  两个Survivor的空间大小一样,当Eden中的对象经过垃圾回收没有被回收掉时,并不是立即拷贝到年老代内存中,而是在两个Survivor之间来回复制,当满足某个条件,如复制次数,就会被复制到年老代内存中。两个Survivor的存在是为了增加对象在年轻代内存中停留的时间,增加被垃圾回收的可能性。两者分别称为From Survivor、To Survivor

Tenured----年老代

  如果年轻代中的对象一直未被回收,生存时间足够长,会被移入到年老代;

  Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,可以是固定大小,也可以是可扩展的(通过-Xmx和-Xms控制)。如果堆中没有内存完成实例分配,并且也无法扩展时,将会抛出OutOfMemoryError异常。

对象的内存分配

  往大方向上讲就是在堆上分配,对象主要分配在年轻代的Eden Space和From Space,少数情况下会直接分配在年老代。如果年轻代的Eden Space和From Space的空间不足,则会发起一次GC,会将Eden Space和From  Space中的存活对象复制到To Space,然后将Eden Space和From Space进行清理。如果在复制过程中,To Space没有足够空间来存储某个对象,就会将该对象移动到年老代中。在进行了GC之后,使用的便是Eden space和To Space了,下次GC时再将存活对象复制到From Space,如此反复循环。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到年老代中。

  一般来说,大对象会被直接分配到年老代,所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组,比如:byte[] data = new byte[4*1024*1024],这种一般会直接在老年代分配存储空间。

2.4.2、特点

  a、各线程共享的内存区域

  b、存放程序创建的对象实例

  c、是垃圾回收器管理的主要区域

  d、从垃圾回收角度可划分为新生代和年老代,其中新生代可进一步划分为Eden、Survivor1、Survivor2

  e、没有内存完成实例分配,并且也无法扩展时,将会抛出OutOfMemoryError异常

2.5、方法区(Method Area)

2.5.1、简介

  方法区与java堆一样,是各线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量等数据,其大小跟项目的规模、类/方法的多少有关。java虚拟机规范把方法区描述为堆的一个逻辑部分,但是他有一个别名叫做Non-Heap(非堆),目的是与java堆区分开。

方法区与永久代的关系

  很多人把方法区成为“永久代”(Permanent Generation),但两者本质上并不等价。仅仅是因为HotSpot虚拟机的设计团队把GC分代收集扩展到方法区,即使用永久代来实现方法区而已,即“永久代”的概念只使用在垃圾回收的范畴内。

  Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾回收在这个区域会比较少出现,这个区域内存回收的目的主要是针对常量池的回收和类的卸载。

  根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

运行时常量池(Runtime Constant Pool

  运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

2.5.2、特点

  a、各线程共享的内存区域

  b、存放类信息,大小项目的规模、类的多少有关;

  c、垃圾回收较少出现,主要针对常量池的回收和类的加载。

  d、当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

2.6、直接内存(Direct Memory)

  直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范定义的内存区域,但当虚拟机内存动态扩展时,将使用到这一区域,且可能会导致OutOfMemoryError异常。

  在JDK1.4中新加入了NIO(New Input/Output),引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,他可以使用Native函数库直接分配对外内存,然后通过一个存储在Java堆里面的DirectBuffer对象作为这块内存的引用。这样能在一些场景中现住提高性能,因为避免了在Java堆和Native堆中来回复制数据。

  如果频繁使用直接内存,可能造成虚拟机内存在动态扩展时物理内存不足,进而出现OutOfMemoryError异常。

3、对象访问的底层实现

对象访问在java语言中无处不在,是最普通的程序行为,即使是最简单的访问,也会涉及到java虚拟机栈、java堆和方法区这三个内存区域的关联关系。

  Object obj = new Object();

  (1)局部变量Object obj的语义反映到java虚拟机栈的本地变量表中,作为一个reference类型数据出现。

  (2)新建对象new Object()的语义将会反映到java堆中,分配一块能够存储Object类型所有实例数据值的结构化内存,此外,在java堆中还必须包含能找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这类数据存储在方法区中。

  由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。

3.1、句柄访问方式

  Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

  优势:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference 本身不需要被修改。

3.2、指针访问方式

  如果使用直接指针访问方式,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址。

  优势:速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

  就虚拟机HotSpot而言,它使用指针方式进行对象访问。

4、参考资料

https://www.cnblogs.com/dingyingsi/p/3760447.html

http://blog.csdn.net/qq_36859415/article/details/53407439

Java虚拟机规范(Java SE 7版)Tim Lindholm, Frank Yelin, Gilad Bracha, Alex Buckley著,周志明、薛笛、吴璞渊、冶秀刚译。

posted @ 2017-12-04 16:29  simple-clean-opt  Views(159)  Comments(0Edit  收藏  举报