从0到1理解JVM运行时数据区

与操作系统类似,JVM(JAVA虚拟机)拥有自己的运行时数据区。在操作系统中,通常有代码区、数据区、堆、栈和静态区等,而JVM中的数据区则有所不同(虽然JVM号称虚拟机,是机器,但它和现实机器始终还是有差别的)。

JVM的运行时数据区类型有下面5种:

类型 属于 抛出异常类型
程序计数器 线程私有
虚拟机栈 线程私有 SOF、OOM
本地方法栈 线程私有 SOF、OOM
公有 OOM
方法区 公有 OOM

程序计数器

程序计数器和CPU中的PC寄存器起着同样的作用:保存下一条指令的地址。不同的是,CPU中的PC寄存器是共有的,JVM中的程序计数器是线程私有的,每个线程都需要一个程序计数器来记住自己已经执行到哪儿了,线程之间轮流执行一段时间,轮到自己时,就可以借助程序计数器从正确的地方往下执行。

虚拟机栈

顾名思义,该栈属于JVM,用来保存一些必要的数据。熟悉操作系统的人都知道,在执行一个方法时,系统会将返回地址和形参等压入栈,执行过程中会从栈里获得形参,执行完方法的全部指令后再从栈中获得返回地址以便跳出方法。

虚拟机栈的原理也是一样的,在准备执行一个方法时,JVM会为该方法生成一个栈帧,将栈帧压入栈;在执行方法的过程中,JVM借用该栈帧搞一些动作;执行完方法后将该栈帧出栈。也就是说,虚拟机栈主要是服务于方法们的执行过程的,一个方法的诞生需要虚拟机栈,一个方法的死亡也需要虚拟机栈,虚拟机栈是多么的有爱和有担当。

虚拟机栈的栈帧中都有些什么玩意呢?

  • 局部变量表:这玩意可不得了,它保存了一个方法执行过程中所需要的所有局部变量!比如说,我们在一个save()方法里定义了一个int类型的temp变量,每次执行save()时,temp都会出现在局部变量表中。更不得了的是,局部变量表的大小在编译时就固定了!不过值得注意的是,该表中只存放变量,不存放对象。
  • 返回地址:也就是俗话中的“return到哪儿去”。
  • 操作数栈:熟悉计算机组成原理或编译原理的童鞋都知道,虽然现代计算机很多都是基于寄存器,但在很早以前曾有一种基于栈的机器,后来因效率问题基于栈的设计被弃用。而JVM,恰恰就是一种基于栈的设计。写过基于栈的四则计算器的同志都知道操作数栈是什么玩意。
  • 动态链接:我还不是特别明白动态链接,以后再回来补充。

因为该区基于栈结构,所以不可避免在极端情况下会出现栈溢出(SOF);另外呢,该区的大小是可以动态扩展的,有时候机器没有多余供其扩展,供其挥霍,也就不可避免地出现内存不足(OOM)。

本地方法栈

随着时代的进步,JAVA的执行效率在逐渐地提升,JAVA已经能够做到在合适的时候“调用操作系统的本地方法,以一种C或C++的速度去执行”。那么在执行系统本地方法时,本地方法也需要栈呀,这些栈在哪里呢?不太可能复用虚拟机栈吧(虚拟机栈专门服务于JAVA语言)。应该在哪里设置个栈呢?为了解决这个问题,聪明的人类推出了本地方法栈,它是属于本地方法的栈

堆是什么呢?熟悉C语言的同学都知道,在C中动态申请一块空间需要使用malloc函数,该函数将在堆里获得空间,这里的堆和JVM中的堆其实也是差不多的。
JVM中的堆,主要用于内存的动态分配。一个对象需要内存时,主要从堆中申请到(注意我用了‘主要’,现如今有栈上分配等技术可以给对象分配内存)。简单地说,堆中充满了大大小小密密麻麻的对象

由于对象主要都在堆里,当一个对象不再有用时,我们可以将它从堆里抹除,这种做法叫做垃圾回收,英文名叫GC。

出于现代垃圾回收器的需要,堆通常被分为新生代(内含一个EDEN区和两个Survivor区)和老生代。垃圾回收器不断地监视着堆里的众多对象,看谁不爽就把谁KO,将之扼杀于堆中。

堆存放了所有线程的对象,它顺理成章地成为公共区域。

方法区

方法区是运行时数据区中的最后一个,它存放的主要是运行时的代码。当一个类被加载时,它会从class文件里被加载到方法区中,它的类版本、字段信息、方法信息、接口信息和常量池都会被加载到方法区中。除了存放类的信息,方法区还存放常量、静态变量和代码。也就是说,方法区中的内容大部分都是不变的,除了一个叫做常量池的奇葩

常量池里边主要是符号引用和直接引用(不明白这两个概念的可以将它们看作一条条的字符串,如“hi”,“hello”等),之所以说常量池会变化,是因为运行时产生的常量也可以加入常量池。

将JVM的运行时数据区和操作系统的运行时数据区做比较,结果如下

JVM 操作系统
程序计数器 PC寄存器
虚拟机栈、本地方法栈
GC堆 malloc堆
方法区 代码区