JVM笔记: JVM内存模型

899 阅读10分钟

内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了java在运行过程中内存申请、分配、管理的策略,保证了jvm的高速运行。不同的JVM对于不同的花费方式和管理机制存在着部分差异。

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

根据JVM 规范, JVM内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五部分。如下图: 在这里插入图片描述

线程

线程是一个程序里的运行单元。jvm运行一个应用有多个线程并行操作。在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。

当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。操作系统负责所有线程的安排调度到任何一个可用的CPU上。一但本地线程初始化成功,它就会调用Java线程中的run()方法。

如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[l的main线程以及所有这个main线程自己创建的线程。

运行时数据区 灰色的为单独线程私有的,红蛇的为多个线程共享的,即: 每个线程: 独立包括程序计数器、栈、本地栈 线程间共享: 堆、对外内存(永久代或元空间、代码缓存)

这些主要的后台系统线程在HOTSpot JVM 里主要试一下几个:

  • 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原雨是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括"stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
  • 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
  • GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。 编译线程:这种线程在运行时会将字节码编译成到本地代码。
  • 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。

PC 寄存器

JVM中的程序计数寄存器(Prodram Counter Register) 中, Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。

这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。 在这里插入图片描述

作用

PC 寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。

它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存 储区域。在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。

任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址:或者,如果是在执行native方法,则是未指定值(undefned)。

它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。它是唯一一个在Java虚拟机规范中没有规定任何OutotMemoryError情况的区域。 在这里插入图片描述 在这里插入图片描述

PC 寄存器线程私有的原因

多线程在一个特定的时间段内只会执行其中一个线程的方法,CPU会不停地做任务切换,会导致经常中断或恢复,为了能能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为了每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

由于CPU时间片轮限制 ,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。

这样必然导致经常中断或恢复,每个线程在创建后,都会产生自己的程序计数器和栈帧,程序极速器在各个线程之间互不影响来保证分毫误差。

虚拟机栈

栈是运行时的单位,而堆是存储的单位,即:栈解决程序运行问题。即程序如何执行,或者说如何处理数据,堆解决是数据存储的问题,即数据怎么放,放在那里。

java虚拟机是线程私有的,他的生命周期与线程相同。虚拟机栈描述的是java方法执行的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口灯信息。每一个方法都被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

基本数据类型在局部变量表中的存储空间以局部变量槽(slot)来表示,其中64位长度的long和double类型的数据占用两个slot, 其余的数据类型只占用一个。

作用

主管java程序的运行,它保存方法是局部变量(8种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用地址(refernce类型))、部分结果,并参与方法的调用和返回。

  • 局部变量 VS 成员变量(或属性)
  • 基本数据变量 VS 引用类型变量(类、数组、接口)

特点

栈一种快速有效的分配存储方式,访问速度仅次于程序计数器。JVM 直接对java栈的操作只有两个:

  • 每个方法执行,伴随着进栈(入栈和压栈)
  • 执行结束后的出栈工作
  • 对于栈来说不存在垃圾回收问题。 在这里插入图片描述

一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。 Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。

《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)。

《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)。从实际使用角度看:“几乎”所有的对象实例都在这里分配内存。

数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

堆是GC(GarbageCollection,垃圾收集器)执行垃圾回收的重点区域。

public class SimpleHeap {
    private int id;

    public  SimpleHeap(int id){
        this.id=id;
    }

    public  void show(){
        System.out.println("My ID is "+ id);
    }

    public static void main(String[] args) {
        SimpleHeap s1 = new SimpleHeap(1);
        SimpleHeap s2 = new SimpleHeap(2);

        int[]  arr= new int[10];

        Object[] arr1 = new Object[10];
      

    }
}

在这里插入图片描述

方法区

《Java虚拟机规范》中明确说明:"尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。所以,方法区看作是一块独立于Java堆的内存空间。

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。方法区 用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的diam缓存等数据。

方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出误:java.lang.OutofMemoryError: PermGen space 或者java.lang.OutOfMemoryError:Metaspace。

本地方法栈

Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法栈也是线程私有的。允许被实现成固定或者是可动态扩展的内存大小。

如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 stackoverflowError 异常。如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutofMemoryError 异常。

本地方法是使用C语言实现的的。它的具体做法是Native Method Stack中登记native方法,在 Execution Engine 执行时加载本地方法库。

当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。它甚至可以直接使用本地处理器中的寄存器直接从本地内存的堆中分配任意数量的内存 .

并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。

总结

根据上述的描述,可用用一下这张图对JVM内存模型做一个总结: 在这里插入图片描述