1.运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
程序计数器
程序计数器是一块很小的内存空间,它可以认作为当前线程执行到某个位置的指示器。计数器记录虚拟机字节码指令的地址。如果为native(底层方法),那么计数器为Undefined。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。
虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型。每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
JVM栈又称Java栈,存放基本数据类型和堆中对象的引用。栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。所以性能快。
本地方法栈
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++的内容。虚拟机规范无强制规定,各版本虚拟机自由实现,HotSpot直接把本地方法栈和虚拟机栈合二为一。
方法区
方法区存储了每一个类的结构信息,例如运行时常量池( Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。
方法区在虚拟机启动的时候被创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集。这个版本的 Java 虚拟机规范也不限定实现方法区的内存位置和编译代码的管理策略。方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。
如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError异常。
常说的永久带、元空间和方法区是什么关系?
平时,说到永久带(PermGen space)的时候往往将其和方法区不加区别。这么理解在一定角度也说的过去。因为,《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。同时,大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。
虽然可以牵强的解释这种将方法区和永久带等同对待观点。但最终方法区和永久带还是不同的。一个是标准一个是实现。这就相当于你将java中的接口和接口的实现等同对待了一样。同时,这种牵强的解释也仅仅是在HotSpot虚拟机上才能勉强成立。其他的虚拟机实现并没有永久带这一说法。有人说,HotSpot之所以用永久带来实现方法区是因为这样可以不必专门为方法区编写一套内存管理的代码。
永久代:永久代在jdk1.7之后就被元空间给取代了,永久代逻辑结构上属于堆,但是物理上不属于堆,会出现OOM异常。
元空间:元数据区取代了永久代,本质和永久代类似逻辑结构上属于堆,区别在于元数据区并不在虚拟机中,而是使用本地物理内存。永久代在虚拟机中,元数据区也可能发生OOM异常。
永久代和元空间都是对方法区的一个实现,这样做的原因是可以不用在单独为方法区去做一个内存管理了。
堆
堆是java虚拟机管理内存最大的一块内存区域,几乎所有对象实例及数组都要在堆上分配内存。同时它也是GC所管理的主要区域,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代。它的内存大小可以设为固定大小,也可以扩展。当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)。
线程问题
程序计数器、虚拟机栈(Java栈)、本地方法栈是线程私有部分。
方法区和堆是线程共享部分。
2.什么是直接内存?
直接内存并不是由JVM管理的内存。他是利用本地方法库直接在java堆之外申请的内存区域。比如NIO中的DirectByteBuffer就是操作直接内存的。
直接内存的好处就是避免了在java堆和native堆直接同步数据的步骤。这块内存不受java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;
3.栈帧概念
JVM 执行 Java 程序时需要装载各种数据到内存中,不同的数据存放在不同的内存区中(逻辑上),其中 JVM Stack(Stack 或虚拟机栈、线程栈、栈)中存放的就是 Stack Frame(Frame 或栈帧、方法栈)。
一个线程对应一个 JVM Stack。JVM Stack 中包含一组 Stack Frame。线程每调用一个方法就对应着 JVM Stack 中 Stack Frame 的入栈,方法执行完毕或者异常终止对应着出栈(销毁)。当 JVM 调用一个 Java 方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入 JVM 栈中。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
局部变量表(Local Variable Table)
1.在编译程序代码的时候就可以确定栈帧中需要多大的局部变量表,具体大小可在编译后的 Class 文件中看到。
2.局部变量表的容量以 Variable Slot(变量槽)为最小单位,每个变量槽都可以存储 32 位长度的内存空间。
3.在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用(在方法中可以通过关键字 this 来访问到这个隐含的参数)。其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot。
4.基本类型数据以及引用和 returnAddress(返回地址)占用一个变量槽,long 和 double 需要两个。
操作数栈(Operand Stack)
1.同样也可以在编译期确定大小。
2.Frame 被创建时,操作栈是空的。操作栈的每个项可以存放 JVM 的各种类型数据,其中 long 和 double 类型(64位数据)占用两个栈深。
3.方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作(与 Java 栈中栈帧操作类似)。
4.操作栈调用其它有返回结果的方法时,会把结果 push 到栈上(通过操作数栈来进行参数传递)。
动态链接(Dynamic Linking)
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在运行时转化为直接引用,这部分称为动态链接。
返回地址(Return Address)
方法开始执行后,只有 2 种方式可以退出 :方法返回指令,异常退出。
4. 内存溢出问题
StackOverFlowError:
当启动一个新的线程是虚拟机会为其分配一个新的栈空间,Java栈以帧为单位保证线程运行状态。当线程调用一个方法时JVM会压入一个新的栈帧到这个线程的栈空间中,只要这个方法还没有返回则这个栈帧就会一直存在。所以方法的嵌套调用太多(如递归调用),随着栈帧的增加导致总和大于JVM设置的-Xss值就会抛出StackOverFlowError异常
OutOfMemoryError:
堆内存溢出:当需要为对象示例化分配内存空间时,而堆的占用已经达到了设置的最大值(-Xmx),就会抛出OutOfMemoryError异常。
方法区内存溢出:方法区存放Java类信息(如类名、访问修饰符、常量池、字段描述、方法描述),在类加载器记载class文件到内存时JVM会提取累的这些信息到方法区,而此时如果需要存储这些类信息且方法区的内存占用已经达到最大值(-XX:MaxPermSize)则会抛出OutOfMemoryError异常。
本机直接内存溢出无法申请到足够的空间时,抛出OutOfMemory异常。
5.虚拟机优化技术
编译优化技术——方法内联
内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换。显然,这样就不会产生转去转回的问题,但是由于在编译时将函数体中的代码被替代到程序中,因此会增加目标程序代码量,进而增加空间开销,而在时间代销上不象函数调用时那么大,可见它是以目标代码的增加为代价来换取时间的节省。
例如
private int add4(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
运行一段时间后JVM会把add2方法去掉,并把你的代码翻译成:
private int add4(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
栈的优化技术—— 栈帧之间数据共享
两个栈帧之间可以共享一部分内存区域,操作数栈和下一个栈帧的局部变量表可以共用一部分区域来进行参数传递。