学习笔记 第1讲 程序运行时内存到底是如何进行分配的
学习笔记 第01讲:程序运行时,内存到底是如何进行分配的?
拉勾教育:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1855
这一讲详细介绍了jvm运行时的内存如何分布的,并举例说明了程序的运行过程。
一、JVM运行时内存数据区
先看一下这张图:
从这张图中可以看出,根据数据能否被线程共享,可分为线程共享数据区及线程私有数据区,
线程共享数据区包括:
方法区:主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。是被各个线程共享的内存区域。
堆:Java堆(Heap)是JVM所管理的内存中最大的一块,该区域唯一目的就是***存放对象实例,几乎所有对象的实例都在堆里面分配***,因此它也是Java垃圾收集器(GC)管理的主要区域,有时候也叫作“GC 堆”(关于堆的 GC 回收机制将会在后续课时中做详细介绍)。同时它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。
线程私有数据区包括:
程序计数器:“程序计数器”是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。当某一个线程被 CPU 挂起时,需要记录代码已经执行到的位置,方便 CPU 重新执行此线程时,知道从哪行指令开始执行。我们熟悉的分支操作、循环操作、跳转、异常处理等也都需要依赖这个计数器来完成。
注意事项
- 在 Java 虚拟机规范中,对程序计数器这一区域没有规定任何 OutOfMemoryError 情况
- 程序计数器是线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
- 当一个线程正在执行一个 Java 方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。
虚拟机栈:虚拟机栈是用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧,用于存储***局部变量表、操作数栈、动态连接、返回地址***等信息,每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到栈的过程。
本地方法栈:本地方法栈和虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及 JNI 可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。
二、栈帧的内部结构
栈帧(StackFrame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。我们可以这样理解:一个线程包含多个栈帧,而每个栈帧内部包含***局部变量表、操作数栈、动态连接、返回地址***等。如下图所示:
2.1、局部变量表
局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。在Java编译成class文件的时候,就会在方法的 Code 属性表中的 max_locals 数据项中,确定该方法需要分配的最大局部变量表的容量。
2.2、操作数栈
操作数栈(OperandStack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入方法的Code属性表中的max_stacks数据项中,栈中的元素可以是任意Java数据类型,包括long和double。
2.3、动态链接
每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用***一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析***。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
(关于这个概念理解不是很清楚,动态链接是地址引用?这个引用是在运行期间产生的?用于区别静态链接?)
2.3、返回地址
当一个方法开始执行以后,只有两种方法可以退出当前方法:
- 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。
三、*Error与OutOfMemoryError
3.1、*Error
每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧,如果方法的嵌套调用层次太多(如递归调用),随着java栈中的帧的增多,最终导致这个线程的栈中的所有栈帧的大小的总和大于-Xss设置的值,而产生*Error溢出异常。
示例:
public class * {
public int stackSize = 0;
public void stackIncre() {
stackSize++;
//递归
stackIncre();
}
public static void main(String[] args) throws Throwable{
* sof = new *();
try {
sof.stackIncre();
} catch (Throwable e) {
System.out.println(sof.stackSize);
throw e;
}
}
}
3.2、OutOfMemoryError
理论上,虚拟机栈、堆、方法区都有发生OutOfMemoryError的可能。但是实际项目中,大多发生于堆当中。java堆用于存放对象的实例,当需要为对象的实例分配内存时,而堆的占用已经达到了设置的最大值(通过-Xmx设置最大值),则抛出OutOfMemoryError异常。
示例:
public class HeapOverFlow {
public static void main(String[] args) {
ArrayList<HeapOverFlow> list = new ArrayList<HeapOverFlow>();
while (true) {
list.add(new HeapOverFlow());
}
}
}
最后上一张总结图片:
由于水平有限,如果文中存在错误之处,请大家批评指正,欢迎大家一起来分享、探讨!
博客:http://blog.csdn.net/MingHuang2017
GitHub:https://github.com/MingHuang1024
Email: [email protected]
Huang2017)
GitHub:https://github.com/MingHuang1024
Email: [email protected]
微信:724360018
下一篇: 达内课程-面向对象之继承与重写