目录
前言
对于Java程序员来说,JVM相关的知识不管是面试还是写代码都有可能会用到,所以今天就来记录一下这方面的知识。
首先上图:
为了方便记忆,我用红色表示线程私有,而绿色表示线程共享的模块。
PS:一般来说,堆的内存空间总是最大的,因为虚拟机将各种各样的对象都放在这里。
PS:JVM是一种抽象的概念和规范,这就和unicode是一种编码规范,我们使用的时候就是UTF-8,UTF-16等具体实现 类似。如图所示,这篇文章里,我们只研究HotSpot虚拟机。
堆(Heap)
堆内存的空间通常来说在五个模块中是最大的,JVM在上面储存和分配对象实例。而由于内存空间占用大,为了提高利用效率,堆也是Java内存回收的重点区域,因此也被称为“Garbage Collected Heap”。
堆的特点如下:
- 在Hotspot虚拟机中,堆的GC采用分代收集算法。(之后会详细再写一篇)
- 堆区被划分成新生代和老年代
- 新生代又分为:Eden空间、From Survivor(S0)空间、To Survivor(S1)空间。老年代就是Tenured Gen。
Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。也就是说堆的内存是一块块拼凑起来的。要增加堆空间时,往上“拼凑”(可扩展性)即可,但当堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区(Method Area,又被称为Non-Heap)
方法区与堆的相同点有:
- 线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。
不同点如下:
- 它存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 正因为方法区存放的信息一般都会很长时间(直到程序停止运行),所以也被称为永久代,在Java8之前还有对此的专门设置参数。
- 方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,但是一般很难进行。
程序计数器(Program Counter Register)
程序计数器的特点:线程私有。
程序计数器的作用:当前线程所执行的字节码的行号指示器,字节码解释器工作时是通过改变计数器的值来选取下一条字节码指令。其中,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。
虚拟机栈(JVM Stacks)
虚拟机栈的特点:线程私有,生命周期与线程相同。
虚拟机栈中存储的是栈帧(Stack Frame),一种用于支持虚拟机进行方法调用和方法执行的数据结构。
其中栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
-
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
-
操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
-
动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
-
方法返回:无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
本地方法栈(Native Method Stacks)
会抛出StackOverflowError和OutOfMemoryError异常。
和虚拟机栈对比:虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈是为虚拟机使用到的Native方法服务。
小结
Java8之前的JVM内存结构大致就是这些,温故而知新是最好的。