Java比起C++一个很大的进步就在于Java不用再手动控制指针的delete与free,统一交由JVM管理,但也正由于如此,一旦出现内存溢出异常,不了解JVM,那么排查问题将会变成一项艰难的工做。java
Java虚拟机在执行Java程序的过程当中会把它所管理的内存划分为若干个不一样的数据区。这些区域都有各自的用途,以及建立销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而创建和销毁。根据《Java虚拟机规范 7》的规定(注意:咱们彻底能够重新的JDK1.9开始了解,可是先讲1.7是由于1.7到1.8是JDK的较大的变化,咱们先经过了解JDK1.7,而后再看一下1.8在1.7的基础上改变了什么。有助于咱们的理解),JVM所管理的内存会被分为如下几个运行时数据区域,以下图所示:linux

1.1 程序计数器
有些地方会将这个地方为寄存器,这其实并无错误,计算机中的寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。在中央处理器的算术及逻辑部件中,寄存器有累加器(ACC)。因此这里咱们将寄存器等于程序计数器并无错。程序员
程序计数器(Program Counter Register)是一块较小的内存空间,它能够看做是当前线程所执行的字节码的行号指示器。字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。算法
因为Java虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个肯定的时刻,一个处理器(对于多核处理器来讲是一个内核)都只会执行一条线程中的指令。所以,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立内存,咱们称这类内存区域为“线程私有”的内存。编程
若是线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是Native方法,这个计数器的值为空(Undefined)。此内存区域是惟一一个在Java虚拟机规范中没有任何OutOfMemoryError状况的区域。segmentfault
问题:Java多线程执行native方法时程序计数器为空,那么线程切换后如何找到以前执行到哪里了?
这里的“程序计数器”是在抽象的JVM层面上的概念——当执行Java方法时,这个抽象的“程序计数器”存的是Java字节码的地址。实现上可能有两种形式,一种是相对该方法字节码开始处的偏移量,叫作bytecode index,简称bci;另外一种是该Java字节码指令在内存里的地址,叫作bytecode pointer,简称bcp。对native方法而言,它的方法体并非由Java字节码构成的,天然没法应用上述的“Java字节码地址”的概念。因此JVM规范规定,若是当前执行的方法是native的,那么pc寄存器的值"未定义"——是什么值均可以。windows
上面是JVM规范所定义的抽象概念,那么实际实现呢?Java线程老是须要以某种形式映射到OS线程上。映射模型能够是1:1(原生线程模型)、n:1(绿色线程 / 用户态线程模型)、m:n(混合模型)。以HotSpot VM的实现为例,它目前在大多数平台上都使用1:1模型,也就是每一个Java线程都直接映射到一个OS线程上执行。此时,native方法就由原平生台直接执行,并不须要理会抽象的JVM层面上的“程序计数器”概念——原生的CPU上真正的程序计数器是怎样就是怎样。就像一个用C或C++写的多线程程序,它在线程切换的时候是怎样的,Java的native方法也就是怎样的。数组
1.2 Java虚拟机栈
与程序计数器同样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每一个方法在执行的同时都会建立一个栈帧(Stack Frame),栈帧是方法运行时的基础数据结构,栈帧用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。缓存
初学时总会将Java内存分为方法区、堆内存和栈内存,Java实际的内存区域划分是比这个复杂的。而咱们这种浅见的认识只能说明大多数程序员最关注就是这三块,这里咱们所说的栈就是Java虚拟机栈,或者说是虚拟机栈中局部变量表部分。安全
在JVM规范中对于该区域规定了两种异常状况:若是线程请求的栈深度大于虚拟机所容许的深度(能够将栈理解为一种数组,栈深度能够理解为数组长度,固然栈和数组仍是很不一样的,这里是为了理解),将抛出StackOverflowError异常;若是虚拟机能够动态扩展(当前大部分的Java虚拟机均可以动态扩展,只不过Java虚拟机规范中也容许固定长度的虚拟机栈),若是扩展时没法申请到足够的内存空间,就会抛出OutOfMemoryError异常。
对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的全部字节码指令都只针对当前栈帧进行操做。

文章转载自:http://blog.csdn.net/u013678930/article/details/51980460
1.2.1 执行引擎
执行引擎是 Java 虚拟机最核心的组成部分之一。“虚拟机” 是一个相对于 “物理机” 的概念,[JVM研发之初是为了应对快速发展的单片机,但愿经过程序模拟出将来程序运行的硬件环境,除此以外也赋予了JVM不少其它功能,如垃圾回收等],这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接创建在处理器、硬件、指令集和操做系统层面上的,而虚拟机的执行引擎则是由本身实现的,所以能够自行制定指令集与执行引擎的结构体系,而且可以执行哪些不被硬件直接支持的指令集格式。
在 Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型称为各类虚拟机执行引擎的统一外观(Facade)。在不一样的虚拟机实现里面,执行引擎在执行 Java代码的时候可能会有解释执行(经过解释器执行)和编译执行(经过即时编译器产生本地代码执行)两种选择,也可能二者兼备,甚至还可能会包含几个不一样级别的编译器执行引擎。但从外观上看起来,全部的 Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果,下面将主要从概念模型的角度来说解虚拟机的方法调用和字节码执行。
1.2.2 运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操做数栈、动态链接和方法返回地址等信息。每个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每个栈帧都包括了局部变量表、操做数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中须要多大的局部变量表,多深的操做数栈都已经彻底肯定了,而且写入到方法表的 Code 属性之中,所以一个栈帧须要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
一个线程中的方法调用链可能会很长,不少方法都同时处于执行状态。对于执行引擎来讲,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的全部字节码指令都只针对当前栈帧进行操做,在概念模型上,典型的栈帧结构如上图所示。
1.2.2.1 局部变量表
局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译为 Class 文件时,就在方法的 Code 属性的 max_locals 数据项中肯定了该方法所须要分配的局部变量表的最大容量。以下代码所示:
public static void test1(int a,int b){
System.out.println(a+b);
int c = 9;
System.out.println(9%4);
}

上面三个参数 stack意味着操做数栈的深度,locals是局部变量表最大容量,args_size是参数数量;
局部变量表的容量以变量槽(Variable Slot,下称 Slot)为最小单位,虚拟机规范中并无明确指明一个 Slot 应占用的内存空间大小,只是颇有导向性地说到每一个 Slot 都应该能存放一个 boolean、byte、char、short、int、float、reference (注:Java 虚拟机规范中没有明确规定 reference 类型的长度,它的长度与实际使用 32 仍是 64 位虚拟机有关,若是是 64 位虚拟机,还与是否开启某些对象指针压缩的优化有关,这里暂且只取 32 位虚拟机的 reference 长度)或 returnAddress 类型的数据,这 8 种数据类型,均可以使用 32 位或更小的物理内存来存放,但这种描述与明确指出 “每一个 Slot 占用 32 位长度的内存空间” 是有一些差异的,它容许 Slot 的长度能够随着处理器、操做系统或虚拟机的不一样而发送变化。只要保证即便在 64 位虚拟机中使用了 64 位的物理内存空间去实现一个 Slot,虚拟机仍要使用对齐和补白的手段让 Slot在外观上看起来与 32 位虚拟机中的一致。
既然前面提到了Java 虚拟机的数据类型,在此再简单介绍一下它们。一个 Slot能够存放一个32 位之内的数据类型,Java 中占用 32位之内的数据类型有 boolean、byte、char、short、int、float、reference 和 returnAddress 8 种类型。前面 6 种不须要多加解释,读者能够按照 Java 语言中对应数据类型的概念去理解它们(仅是这样理解而已,Java语言与 Java虚拟机中的基本数据类型是存在本质差异的),而第 7 种 reference 类型表示对一个对象实例的引用,虚拟机规范既没有说明他的长度,也没有明确指出这种引用应有怎样的结构。但通常来讲,虚拟机实现至少都应当能经过这个引用作到两点,一是今后引用中直接或间接地查找到对象在 Java 堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,不然没法实现 Java 语言规范中定义的语法约束约束。第 8 种即 returnAddress 类型目前已经不多见了,它是为字节码指令 jsr、jsr_w 和 ret 服务的,指向了一条字节码指令的地址,很古老的Java虚拟机曾经使用这几条指令来实现异常处理,如今已经由异常表代替。
对于 64 位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的 Slot 空间。Java 语言中明确的(reference 类型则多是 32 位也多是 64 位)64 位的数据类型只有 long 和 double 两种。值得一提的是,这里把 long 和 double 数据类型分割存储的作法与 “虚拟机经过索引定位的方式使用局部变量表,索引值的范围是从 0 开始至局部变量表最大的 Slot 数量。若是访问的是 32 位数据类型的变量,索引 n 就表明了使用第 n 个 Slot,若是是 64 位数据类型的变量,则说明会同时使用 n 和 n+1 两个 Slot。对于两个相邻的共同存放一个 64 位数据的两个 Slot,不容许采用任何方式单独访问其中的某一个,Java 虚拟机规范中明确要求了若是遇到进行这种操做的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,若是执行的是实例方法(非 static 的方法),那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中能够经过关键字 “this” 来访问到这个隐含的参数。其他参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和做用域分配其他的 Slot。
为了尽量节省栈帧空间,局部变量中的 Slot 是能够重用的,方法体中定义的变量,其做用域并不必定会覆盖整个方法体,若是当前字节码 PC 计数器的值已经超出了某个变量的做用域,那这个变量对应的 Slot 就能够交给其余变量使用。不过,这样的设计除了节省栈帧空间之外,还会伴随一些额外的反作用,例如,在某些状况下,Slot 的复用会直接影响到系统的垃圾收集行为,请看代码清单 8-1 ~ 代码清单 8-3 的 3 个演示。
代码清单 8-1 局部变量表 Slot 复用对垃圾收集的影响之一
public static void main(String[] args) {
byte[] placeholder = new byte[64 * 1024 * 1024];
System.gc();
}
代码清单 8-1 中的代码很简单,即向内存填充了 64 MB 的数据,而后通知虚拟机进行垃圾收集。咱们在虚拟机运行参数中加上“-verbose:gc”来看看垃圾收集的过程,发如今 System.gc() 运行后并无回收这 64 MB 的内存,下面是运行的结果:
[GC (System.gc()) 66847K->66144K(129024K), 0.0015237 secs] //系统垃圾回收
[Full GC (System.gc()) 66144K->66059K(129024K), 0.0074766 secs]//老年代回收,和咱们这里没什么关系
没有回收 placeholder 所占的内存能说得过去,由于在执行 System.gc() 时,变量 placeholder 还处于做用域以内,虚拟机天然不敢回收 placeholder 的内存。那咱们把代码修改一下,变成代码清单 8-2 中的样子。
代码清单 8-2 局部变量表 Slot 复用对垃圾收集的影响之二
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
加入了花括号以后,placeholder 的做用域被限制在花括号以内,从代码逻辑上讲,在执行 System.gc() 的时候,placeholder 已经不可能再被访问了,但执行一下这段程序,会发现运行结果以下,仍是有 64MB 的内存没有被回收,这又是为何呢?
[GC (System.gc()) 66847K->66176K(129024K), 0.0012034 secs]
[Full GC (System.gc()) 66176K->66059K(129024K), 0.0075938 secs]
在解释为何以前,咱们先对这段代码进行第二次修改,在调用 System.gc() 以前加入一行 “int a = 0;”,变成代码清单 8-3 的样子。
代码清单 8-3 局部变量表 Slot 复用对垃圾收集的影响之三
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
这个修改看起来很莫名其妙,但运行一下程序,却发现此次内存真的被正确回收了。
[GC (System.gc()) 66847K->66144K(129024K), 0.0011405 secs]
[Full GC (System.gc()) 66144K->523K(129024K), 0.0100704 secs]
在代码清单 8-1 ~ 代码清单 8-3 中,placeholder 可否被回收的根本缘由是:局部变量中的 Slot是否还存在关于 placeholder 数组对象的引用。第一次修改中,代码虽然已经离开了 placeholder 的做用域,但在此以后,没有任何局部变量表的读写操做,placeholder 本来占用的 Slot尚未被其余变量所复用,因此做为 GC Roots 一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断,在绝大部分状况下影响都很轻微。但若是遇到一个方法,其后面的代码有一些耗时很长的操做,而前面又定义了占用了大量的内存、实际上已经不会再使用的变量,手动将其设置为 null 值(用来代替那句 int a=0,把变量对应的局部变量表 Slot 清空)便不见得是一个绝对无心义的操做,这种操做能够做为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到 JIT 即时编译 的编译条件)下的 “奇技” 来使用。Java 语言的一本著名书籍《Practical Java》中把 “以恰当的变量做用域来控制变量回收时间才是最优雅的解决方法,如代码清单 8-3 那样的场景并很少见。更关键的是,从执行角度来将,使用赋 null 值的操做来优化内存回收是创建在对字节码执行引擎概念模型的理解之上的,而概念模型与实际执行过程是外部看起来等效,内部看上去则能够彻底不一样。在虚拟机使用解释器执行时,一般与概念模型还比较接近,但通过 JIT 编译器后,才是虚拟机执行代码的主要方式,赋 null 值的操做在通过 JIT 编译优化后就会被消除掉,这时候将变量设置为 null 就是没有意义的。字节码被编译为本地代码后,对 GC Roots 的枚举也与解释执行时期有巨大差异,之前面例子来看,代码清单 8-2 在通过 JIT 编译后,System.gc() 执行时就能够正确回收掉内存,无须写成代码清单 8-3 的样子。
关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那样存在 “准备阶段”。经过以前的讲解,咱们已经知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始化;另一次在初始化阶段,赋予程序员定义的初始值。所以,即便在初始化阶段程序没有为类变量赋值也没有关系,类变量仍然具备一个肯定的初始值。但局部变量就不同,若是一个局部变量定义了但没有赋初始值是不能使用的,不要认为 Java 中任何状况下都存在诸如整型变量默认为 0,布尔型变量默认为 false 等这样的默认值。如代码清单 8-4 所示,这段代码其实并不能运行,还好编译器能在编译期间就检查到并提示这一点,即使编译能经过或者手动生成字节码的方式制造出下面代码的效果,字节码校验的时候也会被虚拟机发现而致使类加载失败。
代码清单 8-4 未赋值的局部变量
public static void main(String[] args) {
int a; //会报错未经初始化
System.out.println(a);
}
局部变量运行时被分配在栈中,量大,生命周期短,若是虚拟机给每一个局部变量都初始化一下,是一笔很大的开销,但变量不初始化为默认值就使用是不安全的。出于速度和安全性两个方面的综合考虑,解决方案就是虚拟机不初始化,但要求编写者必定要在使用前给变量赋值。还有一点,若是在类加载完就给局部变量赋值,那么对内存将是很大的开销,而这些开销有不少可能都是没有意义的.
1.2.2.2 操做数栈
操做数栈(Operand Stack)也常称为操做栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表同样,操做数栈的最大深度也在编译的时候写入到 Code 属性的 max_stacks 数据项中。操做数栈的每个元素能够是任意的Java 数据类型,包括long 和 double。32 位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任什么时候候,操做数栈的深度都不会超过在 max_stacks 数据项中设定的最大值。
当一个方法刚刚开始执行的时候,这个方法的操做数栈是空的,在方法的执行过程当中,会有各类字节码指令往操做数栈中写入和提取内容,也就是出栈 / 入栈操做。例如,在作算术运算的时候是经过操做数栈来进行的,又或者再调用其余方法的时候是经过操做数栈来进行参数传递的。
举个例子,整数加法的字节码指令 iadd 在运行的时候操做数栈中最接近栈顶的两个元素已经存入了两个int 型的数值,当执行这个指令时,会将这两个 int值出栈并相加,而后将相加的结果入栈。
操做数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的 iadd 指令为例,这个指令用于整型数加法,它执行时,最接近栈顶的两个元素的数据类型必须为 int 型,不能出现一个 long 和一个 float 使用 iadd 命令相加的状况。
另外,在概念模型中,两个栈帧做为虚拟机栈的元素,是彻底相互独立的。但在大多虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操做数栈与上面栈帧的部分局部变量表重叠在一块儿,这样在进行方法调用时就能够共用一部分数据,无须进行额外的参数复制传递,重叠的过程如图 8-2 所示。

Java 虚拟机的解释执行引擎称为 “基于栈的执行引擎”,其中所指的 “栈” 就是操做数栈。再看下面一个例子:计算的是两个数的相加100+98

在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操做数栈中,其后iadd指令从操做数栈中弹出那两个整数相加,再将结果压入操做数栈。第四条指令istore_2则从操做数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程当中局部变量和操做数栈的状态变化,图中没有使用的局部变量区和操做数栈区域以空白表示
1.2.2.3 动态链接
每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接(Dynamic Linking)。咱们知道 Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用做为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化成为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分红为动态链接。想要了解更多动态连接能够看一下下面的赘述;
应用程序有两种连接方式,一种是静态连接,一种是动态连接,这两种连接方式各有好处。程序是静态链接仍是动态链接是根据编译器的链接参数指定的。
所谓静态连接就是在编译连接时直接将须要的执行代码拷贝到调用处,优势就是在程序发布的时候就不须要再依赖库,也就是再也不须要带着库一块发布,程序能够独立执行,可是体积可能会相对大一些。(所谓库就是一些功能代码通过编译链接后的可执行形式。)
所谓动态连接就是在编译的时候不直接拷贝可执行代码,而是经过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操做系统,操做系统负责将须要的动态库加载到内存中,而后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时链接的目的。优势是多个程序能够共享同一段代码,而不须要在磁盘上存储多个拷贝,缺点是因为是运行时加载,可能会影响程序的前期执行性能。
上面的都是一些概念性的,也是比较简单的,可能你们都知道,可是具体的实现方式是什么样的那?好比两个最主流的操做系统windows和linux是怎么实现的。
在windows上你们都是DLL是动态连接库,里面是一系列可执行的代码,开发过windows程序的人可能还知道有另一种形式的库,就是LIB,你们可能广泛认为LIB就是静态库,至少我以前是这么认为的,可是在实际的开发过程当中,纠正了我这个错误的想法。LIB形式的文件可能会有两种形式,这里并不排除第三种形式。1:包括符号表和二进制可执行代码,也就是传统意义上理解的静态库,能够被静态链接。2:只有符号表,也就是只有动态库的符号导出信息,经过这些信息能够在程序运行时定位到动态库中,最终实现动态链接。
在linux上你们也都知道SO是动态库,相似于windows下的DLL,实现方式也是大同小异,同时开发过linux下程序的人也都知道另一种形式的库就是A库,一样道理广泛认为是和SO对立的,也就是静态库,否则没道理存在啊,呵呵。可是事实却不是如此,A文件的做用和windows下的LIB文件做用几乎同样,也可能会有两种形式,和windows下的lib文件同样,在此就不在赘述。
动态连接和静态连接的对比
静态连接
优势:
- l 代码装载速度快,执行速度略比动态连接库快;
- l 只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。
缺点:
- l 使用静态连接生成的可执行文件体积较大,包含相同的公共代码,形成浪费;
动态连接
优势:
- l 更加节省内存并减小页面交换;
- l DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件形成任何影响,于是极大地提升了可维护性和可扩展性;
- l 不一样编程语言编写的程序只要按照函数调用约定就能够调用同一个DLL函数;
- l 适用于大规模的软件开发,使开发过程独立、耦合度小,便于不一样开发者和开发组织之间进行开发和测试。
缺点:
- l 使用动态连接库的应用程序不是自完备的,它依赖的DLL模块也要存在,若是使用载入时动态连接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态连接,系统不会终止,但因为DLL中的导出函数不可用,程序会加载失败;速度比静态连接慢。当某个模块更新后,若是新模块与旧的模块不兼容,那么那些须要该模块才能运行的软件,通通撕掉。这在早期Windows中很常见。
1.2.2.4 方法返回地址
当一个方法开始执行后,只有两种方式能够退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocatino Completion)。咱们写代码过程当中发现有些时候没有返回值,咱们省略了,那么此时咱们会认为没有return,其实这是不对的,编译器在编译的时候,会为咱们保留,咱们反编译一下看看;
public static void test1(int a,int b){
System.out.println(a+b);
int c = 9;
System.out.println(9%4);
}

另一种退出方式是,在方法执行过程当中遇到了异常,而且这个异常没有在方法体内获得处理,不管是 Java虚拟机内部产生的异常,仍是代码中使用 athrow 字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会致使方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
不管采用何种退出方式,在方法退出以后,都须要返回到方法被调用的位置,程序才能继续执行,方法返回时可能须要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。通常来讲,方法正常退出时,调用者的 PC 计数器的值能够做为返回地址,栈帧中极可能会保存这个计数器值。而方法异常退出时,返回地址是要经过异常处理器表来肯定的,栈帧中通常不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈,所以退出时可能执行的操做有:恢复上层方法的局部变量表和操做数栈,把返回值(若是有的话)压入调用者栈帧的操做数栈中,调整 PC 计数器的值以指向方法调用指令后面的一条指令等。
1.2.2.5 附加信息
虚拟机规范容许具体的虚拟机实现增长一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息彻底取决于具体的虚拟机实现。在实际开发中,通常会把动态链接、方法返回地址与其余附加信息所有归为一类,称为栈帧信息。
1.3 本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的做用十分类似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中使用的语言、使用方式和数据结构并无强制规定,所以具体的虚拟机能够自由实现它。甚至有的虚拟机(如HotSpot)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈同样,本地方法栈也会抛出OutOfMemoryError异常。
1.3.1 JVM怎样使Native Method跑起来
咱们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,而且只会回载一次。在这个被加载的字节码的入口维持着一个该类全部方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。
若是一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实如今一些DLL文件内,可是它们会被操做系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,所以指向方法实现的指针并不会被设置。当本地方法被调用以前,这些DLL才会被加载,这是经过调用java.system.loadLibrary()实现的。
最后须要提示的是,使用本地方法是有开销的,它丧失了java的不少好处。若是别无选择,咱们能够选择使用本地方法。
1.4 Java堆
对于大多数应用来讲,Java堆(Java Heap)是Java虚拟机所管理内存中最大的一块。Java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。这一点在JVM规范中的描述是:全部对象实例以及数组都要在堆上分配,可是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会致使一些微妙的变化发生,全部的对象都分配在堆上也逐渐变得不是那么“绝对”了。
Java堆是垃圾收集器管理的主要区域,所以不少时候也被称为“GC堆”(Garbage Collected Head,注意这不是垃圾堆)。从内存回收的角度来看,因为如今收集器基本都采用分代收集算法,因此Java堆中能够细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过不管如何划分,都与存放内容无关,不管哪一个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地内存回收,或者更快的分配内存。这一节,咱们仅仅了解一下JVM的Runtime Memory Area,而针对垃圾回收和堆的详细构成及其余,后面在专门挑章节介绍。
根据JVM规范的规定,Java堆能够处于物理上不连续的内存空间中,只要逻辑上是连续的就能够,就好像咱们的磁盘空间同样(这其实涉及到了数据结构)。在实现时,既能够实现成固定大小的,也能够是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(经过-Xmx和-Xms控制)。若是在堆中没有内存完成实例分配,而且堆也没法再扩展时,将会抛出OutOfMemoryError异常。
1.4.1 字符串常量池
本节参看:https://segmentfault.com/a/1190000009888357
谈到堆就不得不说一下字符串常量池了,字符串常量池是咱们平常很容易搞混的同样事物了。提起字符串常量池咱们就不禁会想到String,没错字符串常量池的确是和String相关的。做为最基础的引用数据类型,高频率的使用,使得Java设计者为String提供了字符串常量池以提升其性能,那么字符串常量池的具体原理是什么,咱们带着如下三个问题,去理解字符串常量池:
-
字符串常量池的设计意图是什么?
-
字符串常量池在哪里?
-
如何操做字符串常量池?
1.4.1.1 字符串常量池的设计思想
代码:从字符串常量池中获取相应的字符串
String str1 = “hello”;
String str2 = “hello”;
System.out.printl("str1 == str2" : str1 == str2 ) //true
1.4.1.2 字符串常量池在哪里
在分析字符串常量池的位置时,首先简短回顾一下堆、栈、方法区:
-
堆
-
存储的是对象,每一个对象都包含一个与之对应的class
-
JVM只有一个堆区(heap)被全部线程共享,堆中不存放基本类型和对象引用,只存放对象自己
-
对象的由垃圾回收器负责回收,所以大小和生命周期不须要肯定
-
栈
-
每一个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)
-
每一个栈中的数据(原始类型和对象引用)都是私有的
-
栈分为3个部分:基本类型变量区、执行环境上下文、操做指令区(存放操做指令)
-
数据大小和生命周期是能够肯定的,当没有引用指向数据时,这个数据就会自动消失
-
方法区
JDK1.6(包括)以前字符串常量池存在于方法区,JDK1.7之后字符串常量池被转移到了堆中。
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
会分配一个11长度的char数组,并在常量池分配一个由这个char数组组成的字符串,而后由m去引用这个字符串
用n去引用常量池里边的字符串,因此和n引用的是同一个对象
生成一个新的字符串,但内部的字符数组引用着m内部的字符数组
一样会生成一个新的字符串,但内部的字符数组引用常量池里边的字符串内部的字符数组,意思是和u是一样的字符数组
使用图来表示的话,状况就大概是这样的(使用虚线只是表示二者其实没什么特别的关系):
测试demo:
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
System.out.println(m == n); //true
System.out.println(m == u); //false
System.out.println(m == v); //false
System.out.println(u == v); //false
结论:
m和n是同一个对象
m,u,v都是不一样的对象
m,u,v,n但都使用了一样的字符数组,而且用equal判断的话也会返回true
1.5 方法区
方法区(Method Area)与Java堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,可是它却有一个别名叫作Non-Heap(非堆),目的应该是与Java堆区分开来。
不少时候咱们看到一些文献和资料称呼HotSpot的方法区为“永久代”(Permanent Generation),本质上二者是不等价的,仅仅是由于HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器能够像管理Java堆同样来管理这部份内存,可以省去专门为方法区编写内存管理代码的工做。对于其它虚拟机(如BEA JRockit、IBM J9等)来讲是不存在永久代的概念的。原则上,如何实现方法区属于虚拟机的实现细节,不受虚拟机规范约束,但使用永久代来实现方法区,如今看来不是一个好的主意,由于这样很容易形成内存泄漏问题(永久代有-XX:MaxPermSize的上限,J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB,就不会出现问题),并且有极少数方法(例如String.intern())会因这个缘由致使不一样虚拟机下有不一样的表现。所以,对于HotSpot虚拟机,根据官方发布的路线图信息,如今也有放弃永久代并逐步改成采用Native Memory来实现方法区的规划了(实际上JDK1.8已经实现了该计划,永久代消除,以元空间Metaspace替代),在JDK1.7中,已经将本来放在方法区中字符串常量池移出到堆中。
JVM规范对于方法区的限制很是宽松,除了和Java堆同样不须要连续的内存和能够选择固定大小或者可扩展外,还能够选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并不是数据进入了方法区就如永久代的名字同样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,通常来讲,这个区域的回收成绩比较使人难以满意,尤为是类型的卸载,条件至关苛刻,可是这部分区域的回收确实是很必要的。在Sun公司的BUG列表中,曾出现过的若干个严重的BUG就是因为低版本的HotSpot虚拟机对此区域未彻底回收而致使内存泄漏。
根据Java虚拟机规范的规定,当方法区没法知足内存分配需求时,将抛出OutOfMemoryError异常。
1.5.1 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。
JVM对于Class文件每一部分(天然包括常量池)的格式都有严格规定,每个字节用于存储哪一种数据都必须符合规范上的要求才会被虚拟机承认、装载和执行,但对于运行时常量池,Java虚拟机规范没有作任何细节的要求,不一样的提供商实现的虚拟机能够按照本身的需求来实现这个内存区域。不过,通常来讲,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class文件常量池的另一个重要的特征是具有动态性,Java语言并不要求常量必定只有编译期才能产生,也就是并不是预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的即是String类的intern()方法。
既然运行时常量池是方法区的一部分,天然受到方法区的限制,当常量池没法再申请到内存时会抛出OutOfMemoryError异常。
1.6 直接内存
直接内存(Direct Memory)并非虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。可是这部份内存也会被频繁使用,并且也可能致使OutOfMemoryError异常出现,因此咱们放到这里一块儿讲解。
在JDK1.4中新加入了NIO(New Input/Output)类,引入一种新的基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可使用Native函数库直接分配堆外内存,而后经过一个存储在Java堆中的DirectByteBuffer对象做为这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在Java堆和Native堆中来回复制数据。这也是NIO的优势。
显然,本机直接内存的分配不会受到Java堆大小的限制,可是,既然是内存,确定仍是会受到机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存配置-Xmx等参数信息,但常常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操做系统级的限制),从而致使动态扩展时出现OutOfMemoryError异常。
1.7 元空间
上面咱们知道移除永久代的工做从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没彻底移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。咱们能够经过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:
package cn.metaspace.error;
import java.util.ArrayList;
import java.util.List;
public class MethodAreaTest {
static String base = "String";
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = base + base;
base = str;
list.add(str.intern());
}
}
}
这段程序以2的指数级不断的生成新的字符串,这样能够比较快速的消耗内存。咱们经过 JDK 1.6、JDK 1.7 和 JDK 1.8 分别运行:
JDK 1.6 的运行结果:

JDK 1.7的运行结果:

JDK 1.8的运行结果:

取消配置命令

-XX:PermSize=8m -XX:MaxPermSize=8m -Xmx16m

从上述结果能够看出,JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7和 JDK 1.8 中,会出现堆内存溢出,而且 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。所以,能够大体验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,而且 JDK 1.8 中已经不存在永久代的结论。如今咱们看看元空间究竟是一个什么东西?
元空间的本质和永久代相似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。所以,默认状况下,元空间的大小仅受本地内存限制,但能够经过如下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:若是释放了大量的空间,就适当下降该值;若是释放了不多的空间,那么在不超过MaxMetaspaceSize时,适当提升该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项之外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC以后,最小的Metaspace剩余空间容量的百分比,减小为分配空间所致使的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC以后,最大的Metaspace剩余空间容量的百分比,减小为释放空间所致使的垃圾收集
如今咱们在 JDK 8下从新运行一下下面代码段,不过此次再也不指定 PermSize 和 MaxPermSize。而是指定 MetaSpaceSize 和 MaxMetaSpaceSize的大小。输出结果以下:
package cn.metaspace.error;
import java.io.File;
import java.lang.management.ClassLoadingMXBean;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class OOMTest {
public static void main(String[] args) {
URL url = null;
List classLoaderList = new ArrayList();
try {
url = new File("/tmp").toURI().toURL();
URL[] urls = {url};
while (true){
ClassLoader loader = new URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass("cn.metaspace.error.ClassA");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m

从输出结果,咱们能够看出,此次再也不出现永久代溢出,而是出现了元空间的溢出。
1.8 总结
经过上面分析,大体了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。不过你们应该都有一个疑问,就是为何要作这个转换?因此,最后给你们总结如下几点缘由:
1、字符串存在永久代中,容易出现性能问题和内存溢出。因此JDK1.7实现了将字符串常量池转移到堆中的操做,利用堆的大空间和垃圾回收帮助解决这个问题。
2、类及方法的信息等比较难肯定其大小,所以对于永久代的大小指定比较困难,过小容易出现永久代溢出,太大则容易致使老年代溢出。故1.8实现了去除永久代的操做。
3、永久代会为 GC 带来没必要要的复杂度,而且回收效率偏低。
四、Oracle 可能会将HotSpot 与 JRockit 合二为一。
总之,元数据的出现大大减小了OutOfMemoryError的出现几率.