“他强由他强,清风拂山岗;他横由他横,明月照大江。他自狠来他自恶,我自一口真气足。” ——《倚天屠龙记》
毕竟是自己的博客,不希望只是冷冰冰的知识,所以就把我最喜欢的一句话作为序。
就以一张经典的JVM内存结构图作为开头,咱们开始!
不用纠结细节,这张图也只是一个引子。接下来的一段时间里,我也会按照:JVM内存结构,垃圾回收,字节码及类加载前的优化,类加载,解释器和编译器的优化的顺序,陆续更新完从零开始系列,也是对我自己学习路程的一个回顾。
- 一、程序计数器
- 1.1 定义
- 1.2 作用
- 1.3 特点
- 二、虚拟机栈
- 2.1 定义
- 2.2 作用
- 2.3 特点
- 三、本地方法栈
- 3.1定义
- 四、堆
- 4.1 定义
- 4.2 特点
- 五、方法区
- 5.1 定义
- 5.2 具体实现
程序计数器是一个记录着当前线程所执行的字节码的行号指示器,它记的是下一条jvm指令的执行地址。它是java对硬件的抽象,在物理上是由寄存器实现的。
1.2 作用在说明程序计数器的作用之前,我们先来看看这段简单的java代码。
public class PrintNum {
public static void main(String[] args) {
System.out.println(1);
System.out.println(2);
System.out.println(3);
}
}
下面是该源代码被编译成JVM指令的结果。
对于现在的我们,无需理解每条指令的具体作用。观察这张图,不难发现每个指令前都有一个数字,这是指令的偏移地址,我们简称为行号,程序计数器的作用就是存储下一条指令的行号。
以上图为例,当第一条指令执行时,JVM会将下一条指令的行号“3”放入程序计数器中。当第一条指令执行完时,解释器会根据程序计数器中的行号拿到下一条指令,继续运行。
有同学可能要问了,为什么要多此一举先把下一条指令的行号放入程序计数器中,直接取指令然后执行不是更简单吗?
这其实和多线程有关。JVM是支持多个线程同时运行的,这就涉及到CPU的调度问题了。线程甲正执行的好好的, 大哥CPU告诉甲说,你累了,我陪会儿乙,甲只好乖乖休息。一段时间后,大哥回来了,这时甲就可以根据程序计数器中的行号取到下一条指令接着执行。
这里有一点要注意,因为程序计数器记录的是行号,是会重复的,所以多个线程不能同时用一个,不然就乱了。所以程序计数器是线程私有的。
- 线程私有。这点上面已经解释过了。
- 不会存在内存溢出。程序计数器中的行号永远只会有一个,当前指令执行时,会拿下一条指令的行号替换当前的行号。因此就不存在内存溢出问题。
栈:虚拟机栈是每个线程运行所需要的内存。
栈帧:每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存。
虚拟机栈是用于描述java方法执行的内存模型。每一个方法执行时,会创建一个栈帧,栈帧的结构为:局部变量表、操作数栈、动态链接、方法出口,可以理解成一种数据结构,专门用于描述java方法执行的数据结构。虚拟机栈的最小单位就是栈帧。
线程运行中,当执行到一个方法时,就会生成一个栈帧并压入栈中。当这个方法执行完之后,会将这个栈帧弹出,释放对应的内存空间。
一个栈中可能同时存在多个栈帧,如方法一调用方法二时,就会将两个栈帧都压入栈中。
注意每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
简单理解:如当执行main方法时,就会开启一个main线程。这时虚拟机就要拿出一块区域给这个线程使用,这一块区域就是虚拟机栈。同时也不难理解,虚拟机栈也和程序计数器是线程私有的。
java的线程的运行其实就是在执行一个个方法,JVM通过栈帧来表示方法的执行。于是当线程中有方法执行时,就需要对应生成一个栈帧放入虚拟机栈中。表示此线程有此方法正在运行。当该方法执行完之后,这个栈帧也会弹栈,释放其内存,所以也不涉及垃圾回收。
- 不涉及垃圾回收。因为当方法执行完,栈帧会弹栈,对应的内存就会释放掉。不会产生垃圾。
- 注意栈的大小,当分配给栈的内存过大时,相应地,同时可运行地线程数就会减少。
本地方法:java语言是有限制的,不能直接和操作系统打交道,因此就需要通过调用c,c++编写的方法来和底层交互。这些用c,c++编写地方法就是本地方法。(代码中用native修饰的方法就是本地方法。)
本地方法栈:本地方法执行用到的内存空间就是本地方法栈。
堆是我们重点需要了解的区域,也是垃圾回收的重点关注对象。我们将在之后详细讲解。
当前,我们只需要知道:通过new关键字、创建对象都会使用到堆内存即可。
- 存在内存溢出。如果新建的对象一直被回收就会造成内存溢出问题。
- 是线程共享的。所以堆中对象都要考虑线程安全问题。
方法区是JVM所有线程共享的区域,它存储了与类结构相关的信息:运行时常量池,成员变量,方法数据,成员方法、构造方法的代码。
方法区在虚拟机启动时创建,它在逻辑上是堆的组成部分(具体实现上是否为堆的一部分视厂商而定,如永久代,元空间都是其实现)。JVM规范并不强制方法区的位置。
在1.6中方法区的实现叫永久代,1.8叫元空间。二者区别在于StringTable的位置不同。并且在1.8中,方法区被移出JVM内存,被放置在本地内存(操作系统内存)中。
从这两张图中,我们能发现一个特别的区域StringTable,这也是一个很重要的区域,我们之后再聊。
这次我们只是简单介绍了JVM内存结构中的几个大哥。下一次,我们会介绍几种jdk自带的工具,同时也会详细介绍StringTable这块区域。
从零开始,我们一起学JVM!



