内存划分
很多人都将JVM的内存划分为 堆内存heap和栈内存 stack,但是这并不完全准确,更详细的划分如下图:
-
helloWorld.java 被编译器调用javac命令 生成 helloWorld.class
-
helloWorld.class 被classLoader加载到 内存中
-
JVM运行时,主要分为5个区
-
每个线程私有的:虚拟机栈,本地方法栈,程序计数器
-
所有线程共享的:方法区,堆
-
1. 程序计数器
JAVA是多线程的,CPU可以在多个线程中切换时间片,当某一个线程被CPU挂起时,需要记录代码已经执行到的位置,以便这个线程重新获得时间片时,知道应该从哪儿继续执行。
程序计数器注意事项:
-
在java虚拟机规范中,程序计数器这一块并没有规定任何 OutOfMemoryError的情况
-
程序计数器是私有的,随着线程的创建而诞生,随着线程的死亡而消亡
-
当一个线程正在执行某个JAVA方法时,这个计数器记录的是正在执行的 虚拟机字节码指令的地址,它无法记录native方法的执行位置
2. 虚拟机栈
也是线程私有的,生命周期与线程完全同步。
在虚拟机规范中,规定了两种异常状况:
-
StackOverflowError: 栈溢出,当线程 请求的深度超出虚拟机栈所允许的深度时抛出
-
OutOfMemoryError:内存溢出,当虚拟机动态扩容到无法申请到足够内存时抛出
JVM是基于 虚拟机栈的解释器执行的。(DVM则是基于寄存器解释器执行的)
虚拟机栈的作用是,描述JAVA方法执行时的内存模型。每个方法执行时,JVM都会在虚拟机栈中创建一个"栈帧",
栈帧概念
所谓 “栈帧” StackFrame ,就是 用于虚拟机进行方法调用和方法执行时的数据结构,栈帧就是一种数据结构。每个栈帧包括如下成分:
-
局部变量表
- 变量值的存储空间,调用方法时传递的参数,以及方法内部创建的局部变量,都存在这里
-
操作数栈
- 它是一个后入先出栈,在方法刚执行的时候,栈是空的,当方法执行时,会有各种字节码指令被压入和弹出栈
-
动态链接
- 当一个方法要调用其他方法时,需要将要调用方法的符号引用转化为内存中的直接引用,而符号表则存在于 方法区中。
-
返回地址
- 当一个方法执行完毕之后,可以正常退出,也可以异常退出。正常退出可以是 执行到方法最后,也可以是遇到某个return指令。异常退出,则是运行中出现了未被处理的异常,导致方法退出。
举例
这是一个简单案例:
最终会输出i+j+10=13的结果,但是在JVM中执行过程远比我们想的复杂。
我们将这个java文件编译成class之后,再使用javap命令来查看字节码指令,结果如下:
3. 本地方法栈
与虚拟机栈基本相同,是针对本地native方法,做jni开发的接触得多一些。
但是有些虚拟机的实现已经将 本地方法栈和虚拟机栈合二为一了。
4. 堆
-
唯一的作用就是存放对象实例。
-
它也是JAVA GC的主要管理区域。
-
堆是所有线程共享的区域,正因为是多线程共享堆空间,所以需要考虑线程安全的问题。
-
堆中的内存分为新生代 和 老年代
-
新生代 又分为Eden和Survivor, 不同的区域存放不同生命周期的对象
-
不同的区域采用不同的垃圾回收算法,具有针对性,从而提高垃圾回收的效率
5. 方法区
-
也是被所有线程共享的
-
用来存储,已经被JVM加载的类信息,常量,静态变量,即时编译后的代码,数据
关于异常
-
StackOverflowError
-
递归调用时造成栈溢出的主要原因,使用递归却没有设定递归的结束的条件,或者执行时永远达不到结束的条件。
-
每调用一次递归方法,都会在虚拟机栈中创建出一个栈帧,而无限创建栈帧,会导致栈溢出
-
-
OutOfMemoryError
-
理论上,虚拟机栈,堆,方法区,都有发生OutOfMemoryError的可能,但是实际上,内存溢出大多发生在堆中。
-
经常出现内存溢出的情况包括:循环或者频繁调用的方法中,如果大量申请内存,就有可能最后导致虚拟机动态扩容到无法申请到足够的内存的程度,于是抛出error。
-
总结
以上5个区域:程序计数器,虚拟机栈,本地方法栈, 堆,方法区,仅仅是虚拟机规范,而不是具体实现。具体实现有:Sun公司的HotSpot,还有 Android的DVM , ART。