JVM初探(一):jvm内存结构

概述

我们知道java代码先编译为.class文件,然后再将.class文件交由jvm执行。在程序运行的这一过程中,jvm会将其管理的内存空间划分为不同的区域,这些区域各有各的用途,我们将其分为五类:

  1. 方法区
  2. 虚拟机栈
  3. 本地方法栈
  4. 程序计数器

其中方法区和堆是线程共享的,随jvm启动和停止而创建和销毁;

而虚拟机栈、本地方法栈和程序计数器则是线程私有的,随线程的创建和结束而创建和销毁。

jvm内存体系

一、线程隔离数据区

包括程序计数器,虚拟机栈,本地方法栈三部分,是线程私有的数据区。

1.程序计数器

程序计数器

程序计数器用于记录当前线程执行的字节码指令的地址。

我们知道cup实现多线程操作是根据每个线程分配是时间片来决定处的,每一个时间片cup都只处理抢到那个时间片的线程,因此很可能出现线程1指令执行到一半,结果下一个时间片又去处理另一个线程了。

为了能够在线程切换后依然能恢复到正确的指令位置,每一个线程都需要一个独立的计数器去记录正在执行的字节码指令地址,我们可以简单的理解为一个记录执行到的指令行数的一个指示器。

如果指向的是java方法,计数器记录执行的字节码的地址,如果是非java代码的Native方法,这计数器为空。

计数器是唯一一个没有规定OutOfMemoryError的区域。

2.虚拟机栈

虚拟机栈

虚拟机栈是描述java方法执行的一个内存模型。

每个方法执行的时候会常见一个栈帧,栈帧中会储存局部变量表。操作数栈、动态链接、方法出口信息等。比如方法的局部变量会插入局部遍历表,对局部变量的运算和传递则通过数栈等等。

每个方法从调用到完成就是一个栈帧在虚拟机栈中入栈到出栈的一个过程。我们使用递归时提到的栈就是虚拟机栈。

虚拟机栈规定有两种异常:StackOverflowErrorOutOfMemoryError

我们知道方法调用实际就是栈帧入栈,如果栈的深度超过规定,就会抛出StackOverflowError异常

栈的大小可以规定也可以动态扩展,如果栈扩展大小时申请不到足够的内存,就会抛出OutOfMemoryError异常.

3.本地方法栈

本地方法栈

本地方法栈是描述j非java的方法执行的一个内存模型。

它与虚拟机栈功能一样,但是不同的是本地方法栈用于存放实现方法非java代码的方法。当一个java方法要调用的的时候,会将java栈帧入虚拟机栈,而当非java方法要调用的时候就会入本地方法栈。

实际上两种栈之间往往会互相调用对方的方法,比如java方法A调用了java方法B,java方法B调用了C++方法C,这个C++方法又调用了java方法D,描述一下过程就会是:

A =》虚拟机栈,B =》虚拟机栈,C =》本地方法栈,D =》虚拟机栈

二、线程共享数据区

1.堆

java堆

堆用于存放对象实例、数组和字符串常量池

堆用于存放类的实例对象、数组和字符串常量池、另外,由于实例对象存储于此区域,所以也是垃圾收集器管理的主要区域,故又称GC堆

java对可以是固定大小,也可以是动态大小,如果堆中没有内存分配给新的实例对象的时候,就会抛出OutOfMemoryError异常

1.2字符串常量池

这里稍微提一下字符串常量池,正由于字符串常量池的存在,当创建字符串常量时,首先检查字符串常量池是否存在该字符串,存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

这也是为什么字符串明明是对象却可以直接使用 == 比较,因为同样的字符指向的都是常量池里同一个字符串对象。

1.3内存分配策略

另外值得一提的是,堆往往和垃圾回收问题一起出现,所以这里也简单的介绍一下内存分配的策略:

由于jvm内存回收机制采用了分代收集算法,所以java堆中还分为新生代和老年代,新生代中又分为占大部分控件的eden区域和占较小空间的survivorSpace0survivorSpace1

根据分代收集算法,堆中内存分配时一般遵循以下原则:

  • 对象优先分配给eden区域。当eden区域没有足够空间时,发起一次GC。当垃圾回收时,根据复制算法:

    eden和一个survivorSpace中还存活的对象会复制到另一个survivorSpace中,然后清理原先的空间

  • 需要大量连续内存空间的大对象直接进入老年代。比如巨长的数组或者字符串,还有非常高的树之类的。

  • 长期存活的对象会进入老年代。对象在新生代活过一定次数GC后会移入老年代。

  • 动态对象年龄判定。如果在survivorSpace空间中相同年龄所有对象大小的总和大小大于survivorSpace空间的一半,年龄大于或等于该年龄的对象直接进入老年代

当然,不同的垃圾收集器和不同的垃圾收集算法适应不同的程序运行情况,实际的内存回收机制要复杂的多,这里以后会在新随笔里另外再展开,这里就不再赘述了。

2.方法区

方法区主要用来存放类信息、类的静态变量、常量、运行时常量池等

方法区和堆功能类似,主要用与存放类信息,常量和即时编译器编译后的代码等数据。

2.2永久代

很多文章提到方法区的时候都会涉及到这个“永久代”这个词。实际上,方法区是jvm的一个规范,永久代是这种规范的另一种实现,类似的还有元空间,这也是方法区的一种实现。

jvm虚拟机分为很多种,比如HotSpot ,JRockit(Oracle)、J9(IBM)等等,但是只有HotSpot才有永久代这个说法。硬要说的话,方法区可以理解为一个接口,永久区是这个接口的实现类。

2.3 运行时常量池

运行时常量池

类似的问题还有运行时常量池。运行时常量池是方法区的一部分,用于存储各种编译时以及运行时产生的新常量,类加载以后的数据就存放于此,还有字符串手动入池方法intern()

这里的 “运行时常量池”同上文提到的方法区和永久代的关系一样,也是jvm的规范而不是实现,运行时常量必然会有一个专门的储存空间,但是放在哪就得看虚拟机各自的实现了。

不过这里要额外理解一下字符串常量池:

常量池分为两块,一块是堆中的字符串常量池,一块是方法区中的常量池。实际上JDK8之前字符串常量池也在方法区中的常量池里边,而在JDK8之后被单独分离出来放到了堆里

字符串常量池在JDK8被分离

0%