深入理解JVM(1)——JVM内存模型

时间:2023-12-04 17:21:20

Java虚拟机的内存空间分为五个部分,分别是:

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

接下来对这五部分分别进行详细的介绍

1、程序计数器:

  a)什么是程序计数器:程序计数器是内存中的一个很小的空间,可以看作是当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是当前线程正在执行的字节码指令的地址。需要注意的是:如果当前线程正在执行的是一个本地方法,那么此时程序计数器为空。

b)  程序计数器的作用:字节码解释器通过改变程序计数器依次读取指令,实现程序的流程控制;在多线程的情况下,程序计数器用来记录当前线程的执行位置,以便于当线程被切回来的时候能够记住该线程上次运行到那里了。

c) 程序计数器的特点:是一块较小的内存空间;线程私有,每一个线程都有一个程序计数器;是唯一一个不会出现OutOfMemoryError的区域;生命周期随着线程的创建而创建,线程的结束而死亡。

2、Java虚拟机栈

a) 什么是Java虚拟机栈:Java虚拟机栈是描述Java方法运行过程的内存模型。Java虚拟机会为每一个即将运行的java方法创建一个叫“栈帧”的区域,该区域用来存放Java方法运行过程中需要的一些信息,这些信息主要包括:局部变量表、操作数栈、动态链接、方法出口信息等。

注意:当方法在运行过程中需要创建局部变量的时,就会将变量的值存入局部变量表中。人们常说的Java内存空间分为“栈”和“堆”,栈中存放的是局部变量,堆中存放的是对象,这句话是不完全正确的,几乎所有的对象都是存放在堆中,但是Java虚拟机会为每一个Java方法创建一个“栈帧”,其中栈帧包括局部变量表,但还包括其他的如操作数栈、动态链接和方法出口信息等。

b)Java虚拟机栈的主要特点:

  1) 局部变量表的创建是在Java方法即将运行的时候随着栈帧的创建而创建,但是局部变量表的大小是在编译阶段就已经确定的,创建的时候只是按照实现分配好的大小进行创建,而且在Java方法运行的过程中局部变量表的大小不会改变。

  2)Java虚拟机栈是线程私有的,每个线程都有自己的Java虚拟机栈,并且随着线程的创建而创建,随着线程的结束而死亡。

  3) Java虚拟机栈一般会出现两种错误:StackOverFloeError和OutOfMemoryError。

若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出*Error异常。*Error表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。

若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。

3、本地方法栈

  a)什么是本地方法栈:

    1) 本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。

    2)本地方法栈也是在方法即将运行被执行的时候,本地方方法栈也会为本地方法创建一个栈帧,用于存放该本地方法在运行过程中需要用到的一些信息。包括:局部变量表、操作数栈、动态链接、方法出口信息等。

    3) 本地方法在执行完毕也会将相应 的栈帧出栈并释放内存空间。

    4) 本地方法栈也会抛出*Error和OutOfMemoryError异常。

4、堆

a) 什么是堆

堆是用来存放对象的内存空间,几乎所有的对象都存储在堆中。

 b)堆的特点是什么

  1)线程共享:整个虚拟机只有一个堆,所有的线程都访问同一个堆。

  2)在虚拟机启动的时候创建堆,在虚拟机关闭的时候销毁堆

  3)堆是垃圾回收的主要场所

  4) 堆可以进一步分为新生代和老年代,其中新生代有可以进一步分为Eden、From Survior、To Survior.不同的区域存放不同声明周期的对象,这样可以根据不同的区域使用不同的垃圾收集算法,从而是对象回收而更具有针对性,也会变的更加高效。

  5)  堆的大小即可以固定也可以动态的扩展,但是主流的虚拟机堆的大小都是可以动态扩展的。

5、方法区

  a)什么是方法区

在Java虚拟机规范中定义方法区是堆的一个逻辑部分,方法区中存放已经被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等

  b) 方法区的特点

        1)  线程共享:方法区是堆的一个逻辑部分,因此和堆一样是线程共享的,整个虚拟机中只有一个堆区域。

    2)永久代:方法区中存放的都是需要永久保存的信息,而且它还是堆空间的一个逻辑部分,因此用堆的方法进行划分把方法区分为老年代。

    3) 内存回收效率低:方法区中的信息一般需要长期存在,内存回收一遍只有少量的信息是无效的,在方法区中垃圾回收的主要对象是对常量池的回收和对类型的卸载。

    4)Java虚拟机规范对方法区的要求比较宽松:和堆一样允许固定大小,也允许动态扩展,还允许不进行垃圾回收。

  c)  什么是运行时常量池

  一般在一个类中通过public static final来声明一个常量,这个类被编译之后会生成一个Class文件,这个类的全部信息都会包含在这个Class文件中,当这个类被虚拟机加载之后,Class文件中的常量就会被放在方法区的运行时常量池中。在程序运行期间,也可以向常量池中添加常量。如果运行时常量池中的常量长时间没有被对象引用,也没有被变量引用,那么就会被垃圾收集器进行回收。

6、直接内存

直接内存是除了Java虚拟机之外的内存,但也能被Java利用。在NIO中引入了一种基于通道和缓冲的IO方式,它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,从而提升了数据操作的效率。

直接内存的大小不受Java虚拟机的控制,但既然是内存,当内存不足的时候也会出现抛出OOM异常。

综上所述:

a)Java虚拟机的内存模型中一共有两个“栈”,分别是:Java虚拟机栈和本地方法栈。两个“栈”的功能类似,都是方法运行过程的内存模型。并且两个“栈”内部构造相同,都是线程私有。只不过Java虚拟机栈描述的是Java方法运行过程的内存模型,而本地方法栈是描述Java本地方法运行过程的内存模型。

b)Java虚拟机的内存模型中一共有两个“堆”,一个是原本的堆,一个是方法区。方法区本质上是属于堆的一个逻辑部分。堆中存放对象,方法区中存放类信息、常量、静态变量、即时编译器编译的代码。

c)堆是Java虚拟机中最大的一块内存区域,也是垃圾收集器主要的工作区域。

d) 程序计数器、Java虚拟机栈、本地方法栈是线程私有的,即每个线程都拥有各自的程序计数器、Java虚拟机栈、本地方法区。并且他们的生命周期和所属的线程一样。

e)而堆、方法区是线程共享的,在Java虚拟机中只有一个堆、一个方法栈。并在JVM启动的时候就创建,JVM停止才销毁。