JVM内存模型

前言

本文主要讲JVM内存模型,利用简单的代码深入理解JVM结构。

JDK结构体系

  • JDK(Java Development Kid,Java 开发开源工具包),是针对 Java 开发人员的产品,是整个 Java 的核心,包括了 Java 运行环境 - JRE、Java 工具和 Java 基础类库。
  • JRE(Java Runtime Environment,Java 运行环境)是运行 JAVA 程序所必须的环境的集合,包含 JVM 标准实现及 Java 核心类库。
  • JVM(Java Virtual Machine,Java 虚拟机)是整个 Java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的软件程序。

关系图如下:

在这里插入图片描述
由图中可以看出以下几点:

  • JDK=JRE+多种Java开发工具
  • JRE=JVM+各种类库
  • 这三者的关系是一层层的嵌套关系。JDK>JRE>JVM

JVM结构体系

如下图,Helloworld程序编译过程如下:

  1. 将java文件编译成(.class)文件(所有的平台上生成的字节码文件都是相同的)。
  2. 通过类装载子系统将文件加载到JVM虚拟机上。
  3. 使用字节码执行引擎运行字节码文件(在不同的平台上运行字节码文件的Java虚拟机是不同的)
    在这里插入图片描述

在这里插入图片描述

由上图可以看出:

  • Java语言的一个非常重要的特点就是跨平台,而使用Java虚拟机是实现这一特点的关键。
  • 不同操作系统上的JVM是不同的。
  • Java是一门高级语言,而JVM就是翻译官,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

JVM内部组成

在这里插入图片描述

程序计数器

在这里插入图片描述

  • 程序计数器(Program Counter Register)是JVM中一块较小的内存区域,保存着当前线程执行的虚拟机字节码指令的内存地址(可以看作当前线程所执行的字节码的行号指示器)。
  • 如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址(可以理解为上图所示的行号),如果正在执行的是native方法,这个计数器的值为undefined。
  • JVM的多线程是通过线程轮流切换并分配CPU执行时间片的方式来实现的,任何一个时刻,一个CPU都只会执行一条线程中的指令。为了保证线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间的程序计数器独立存储,互不影响。
  • 此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,因为程序计数器是由虚拟机内部维护的,不需要开发者进行操作。

Java栈(虚拟机栈)

虚拟机栈(Java Virtual Machine Stacks)是线程隔离的,每创建一个线程时就会对应创建一个Java栈,即每个线程都有自己独立的虚拟机栈。这个栈中又会对应包含多个栈帧,每调用一个方法时就会往栈中创建并压入一个栈帧,栈帧存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法从调用到最终返回结果的过程,就对应一个栈帧从入栈到出栈的过程。

虚拟机栈是一个后入先出的数据结构,线程运行过程中,只有处于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法,当前活动帧栈始终是虚拟机栈的栈顶元素。

几个重要概念

栈:栈先进后出。
栈帧: 是用来存储数据和部分过程结果的数据结构(可以理解为执行中的一个方法)。
栈帧的位置:  内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> here[在这里]
栈帧大小确定时间: 编译期确定,不受运行期数据影响。
为什么程序要使用先进后出的数据结构"栈"? 
如下图代码,程序先进入main方法-> compute方法,然后compute方法执行完,跳出栈进入main方法,程序从上往下执行!!!
public class JvmDemo {

    public static final int initData = 999;  //编译时常量
    public static User user = new User();    //运行时常量

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        JvmDemo math = new JvmDemo();
        math.compute();
        System.out.println("test");
    }
}

上图代码对应的栈模型如下:
在这里插入图片描述

局部变量表

  • 局部变量表存放了编译期可知的各种基本数据类型和对象引用类型。通常我们所说的“栈内存”指的就是局部变量表这一部分。
  • 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,运行期间不会改变局部变量表的大小。
  • 64位的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。

在这里插入图片描述

操作数栈

  • 操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值。

比如上图代码a=1,将1先压入操作数栈,然后弹出赋值给a。

动态链接

存放方法里的指令码地址,比如上图代码compute方法,我得知道里边的指令(int a = 1;int b = 2…)这些指令码存放在哪吧!

方法出口

记录执行完该方法后,返回到上一个方法执行的出口位置,比如上图代码(math.compute()执行完后,要继续执行System.out.println(“test”);

栈的大小可以固定也可以动态扩展

  • 在固定大小的情况下,JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,会抛出StackOverflowError异常。
  • 在动态扩展的情况下,当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时,就会抛出OutOfMemoryError异常。

注意
-Xss参数 影响的是每个线程栈深度(局部变量表内容越多,栈帧越大,栈深度越小)

Java虚拟机栈可能出现两种类型的异常:

  1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
  2. 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。
栈溢出代码:
public class StackTest implements Runnable{
    private int count = 0;

    public void recursiveCalls(String a){
        count++;
        System.out.println(Thread.currentThread().getName() + "stack depth: " + count);
        recursiveCalls(a);
    }
    

    public static void main(String[] args) {

        new Thread(new StackTest(),"Thread1").start();
        new Thread(new StackTest(),"Thread2").start();
    }

    @Override
    public void run() {
        try {
            recursiveCalls("a");
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}
Thread1stack depth: 5816
Exception in thread "Thread2" java.lang.StackOverflowError
堆溢出代码:
public class JavaVMStackOOM {

    private void dontStop() {
        while (true) {
        }
    }

    //通过不断的创建新的线程使Stack内存耗尽
    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(() -> dontStop());
            thread.start();
        }
    }

    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }

}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

本地方法栈

  • 本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常。
  • 不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。
  • HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。

方法区

在这里插入图片描述

  • 保存在着被加载过的每一个类的信息(虚拟机加载的类信息(类的版本、字段、方法、接口),常量,静态变量,即时编译器编译后的代码等数据);这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;static变量信息也保存在方法区中。
  • 可以看做是将类(Class)的元数据,保存在方法区里。
  • 方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
  • HotSpot虚拟机使用永久代来实现方法区,使得HotSpot虚拟机的垃圾收集器可以像管理堆内存一样来管理这部分内存,能省去专门为方法区编写内存管理代码工作。所以开发者喜欢将方法区称为永久代,本质上两者并不等价,对于其他虚拟机来说不存在永久代的概念。
  • 方法区是线程共享的;当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待。
  • 方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。jvm也可以允许用户和程序指定方法区的初始大小,最小和最大限制(-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置元空间初始大小以及最大可分配大小。)。
  • 方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展Java程序,这样可能会导致一些类,不再被使用,变为垃圾。这时候需要进行垃圾清理。

注意
常量区分为编译时常量和运行时常量

    public static final int initData = 999;  //编译时常量,存放在方法区里
    public static User user = new User();    //运行时常量,User对象存放在堆中 这里只存放引用地址

Java1.8之前采用分代模型,如图所示
在这里插入图片描述

  • JVM管理的最大的一块内存区域,存放着对象的实例,是线程共享区。

  • 堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”。

  • JAVA堆的分类:
    从内存回收的角度上看,可分为新生代(Eden空间,From Survivor空间、To Survivor空间)及老年代(Tenured Gen)。
    从内存分配的角度上看,为了解决分配内存时的线程安全性问题,线程共享的JAVA堆中可能划分出多个线程私有的分配缓冲区(TLAB)。

  • JAVA堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

  • 可通过参数 -Xmx -Xms 来指定运行时堆内存的大小,堆内存空间不足也会抛OutOfMemoryError异常。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昨日晨曦丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值