java内存模型

Java内存模型是每一个java程序员必须掌握理解的,这是Java的核心基础,对咱们编写代码特别是并发编程时有很大帮助。因为Java程序是交由JVM执行的,因此咱们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。java

首先,咱们回顾一下Java程序执行流程:程序员

 

如上图所示,首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),而后由JVM中的类加载器加载各个类的字节码文件,加载完毕以后,交由JVM执行引擎执行。在整个程序执行过程当中,JVM会用一段空间来存储程序执行期间须要用到的数据和相关信息,这段空间通常被称做为Runtime Data Area(运行时数据区),也就是咱们常说的JVM内存。所以,在Java中咱们经常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。编程

 那么本篇文章主要是要分析Runtime Data Area(运行时数据区)的结构。数组

 

 1. 运行时数据区分为几个部分?数据结构

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。多线程


 

名称架构

特征并发

做用jvm

配置参数spa

异常

程序计数器

占用内存小,线程私有,

生命周期与线程相同

大体为字节码行号指示器

虚拟机栈

线程私有,生命周期与线程相同,使用连续的内存空间

Java 方法执行的内存模型,存储局部变量表、操做栈、动态连接、方法出口等信息

-Xss

StackOverflowError

OutOfMemoryError

java堆

线程共享,生命周期与虚拟机相同,能够不使用连续的内存地址

保存对象实例,全部对象实例(包括数组)都要在堆上分配

-Xms

-Xsx

-Xmn

OutOfMemoryError

方法区

线程共享,生命周期与虚拟机相同,能够不使用连续的内存地址

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

-XX:PermSize:

16M

-XX:MaxPermSize

64M

OutOfMemoryError

运行时常量池

方法区的一部分,具备动态性

存放字面量及符号引用

 

 

 

 
1.1 方法区

方法区是java虚拟机规范去中定义的一种概念上的区域,具备什么功能,但并无规定这个区域到底应该位于何处,所以对于实现者来讲,如何来实际方法区是有着很大自由度的。

永生代是hotspot中的一个概念,其余jvm实现未必有,例如jrockit就没这东西。java8以前,hotspot使用在内存中划分出一块区域来存储类的元信息、类变量以及内部字符串(interned string)等内容,称之为永生代,把它做为方法区来使用。

[JEP122][2]提议取消永生代,方法区做为概念上的区域仍然存在。原先永生代中类的元信息会被放入本地内存(元数据区,metaspace),将类的静态变量和内部字符串放入到java堆中。

为了龙清楚方法区那么须要解释两个名词:永久代和元空间

PermGen(永久代)

绝大部分Java程序员应该都见过“java.lang.OutOfMemoryError: PremGen space”异常。这里的“PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是JVM的规范,然后者则是JVM规范的一种实现,而且只有HotSpot才有“PermGen space”,而对于其余类型的虚拟机,如JRockit(Oracle)、J9(IBM)并无“PermGen space”。因为方法区主要存储类的相关信息,因此对于动态生成类的状况比较容易出现永久代的内存溢出。而且JDK 1.8中参数PermSize和MaxPermSize已经失效。

元空间

其实,移除永久代的工做从JDK 1.7就开始了。JDK 1.7中,存储在永久代的部分数据就已经转移到Java Heap或者Native Heap。但永久代仍存在于JDK 1.7中,并无彻底移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap。

JDK1.8对JVM架构的改造将类元数据放到本地内存中,另外,将常量池和静态变量放到Java堆里。HotSpot VM将会为类的元数据明确分配和释放本地内存。在这种架构下,类元信息就突破了原来-XX:MaxPermSize的限制,如今可使用更多的本地内存。这样就从必定程度上解决了原来在运行时生成大量类形成常常Full GC问题,如运行时使用反射、代理等。因此升级之后Java堆空间可能会增长。

元空间的本质和永久代相似,都是对JVM规范中方法区的实现。不过元空间与永久代之间的最大区别在于:元空间并不在虚拟机中,而是使用本地内存。所以,默认状况下,元空间的大小仅受本地内存限制,但能够经过如下参数指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对改值进行调整:若是释放了大量的空间,就适当下降该值;若是释放了不多的空间,那么在不超过MaxMetaspaceSize时,适当提升该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面的两个指定大小的选项外,还有两个与GC相关的属性:

-XX:MinMetaspaceFreeRatio,在GC以后,最小的Metaspace剩余空间容量的百分比,减小为分配空间所致使的垃圾收集。

-XX:MaxMetaspaceFreeRatio,在GC以后,最大的Metaspace剩余空间容量的百分比,减小为释放空间所致使的垃圾收集。

 

因此对于方法区,Java8以后的变化:

移除了永久代(PermGen),替换为元空间(Metaspace);
永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)
 

1.2 虚拟机栈(线程栈)与 堆(Heap)

为更好的理解Java线程栈和堆,咱们简单的认为Java内存模型把Java虚拟机内部划分为线程栈和堆。这张图演示了Java内存模型的逻辑视图。

 

每个运行在Java虚拟机里的线程都拥有本身的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问本身的线程栈。一个线程建立的本地变量对其它线程不可见,仅本身可见。即便两个线程执行一样的代码,这两个线程任然在在本身的线程栈中的代码来建立本地变量。所以,每一个线程拥有每一个本地变量的独有版本。

全部原始类型的本地变量都存放在线程栈上,所以对其它线程不可见。一个线程可能向另外一个线程传递一个原始类型变量的拷贝,可是它不能共享这个原始类型变量自身。

堆上包含在Java程序中建立的全部对象,不管是哪个对象建立的。这包括原始类型的对象版本。若是一个对象被建立而后赋值给一个局部变量,或者用来做为另外一个对象的成员变量,这个对象任然是存放在堆上。

下面这张图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上。

 

一个本地变量多是原始类型,在这种状况下,它老是“呆在”线程栈上。

一个本地变量也多是指向一个对象的一个引用。在这种状况下,引用(这个本地变量)存放在线程栈上,可是对象自己存放在堆上。

一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即便这些方法所属的对象存放在堆上。

一个对象的成员变量可能随着这个对象自身存放在堆上。无论这个成员变量是原始类型仍是引用类型。

静态成员变量跟随着类定义一块儿也存放在堆上。

存放在堆上的对象能够被全部持有对这个对象引用的线程访问。当一个线程能够访问一个对象时,它也能够访问这个对象的成员变量。若是两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,可是每个线程都拥有这个本地变量的私有拷贝。

下图演示了上面提到的点:

 

两个线程拥有一些列的本地变量。其中一个本地变量(Local Variable 2)执行堆上的一个共享对象(Object 3)。这两个线程分别拥有同一个对象的不一样引用。这些引用都是本地变量,所以存放在各自线程的线程栈上。这两个不一样的引用指向堆上同一个对象。

注意,这个共享对象(Object 3)持有Object2和Object4一个引用做为其成员变量(如图中Object3指向Object2和Object4的箭头)。经过在Object3中这些成员变量引用,这两个线程就能够访问Object2和Object4。

 

1.3 程序计数器

程序计数器是一块较小的内存空间,能够看做是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。

因为Java 虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个肯定的时刻,一个处理器(对于多核处理器来讲是一个内核)只会执行一条线程中的指令。所以,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。

若是线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。

此内存区域是惟一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError状况的区域。

 

 1.4 本地方法栈

 

本地方法栈(Native MethodStacks)与虚拟机栈所发挥的做用是很是类似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并无强制规定,所以具体的虚拟机能够自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

 

与虚拟机栈同样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

相关文章
相关标签/搜索