Android进阶(一):5分钟带你了解JVM的内存分配

前言

JVM的内存分配是一个老生常谈的话题了,但是如果开发者想要开发出高质量的APP,那么JVM的内存分配是必须要了解的。本文主要介绍JVM的内存分配。

JVM的内存区域划分

在网上一些介绍JVM内存分配的文章中,他们将Java的内存大致分为堆内存(heap)和栈内存(stack),这种划分的方式,体现了开发者最关注的区域,但是并不完全准确。JVM会将内存划分为若干个不同的数据区域,主要分为:程序计数器虚拟机栈本地方法栈方法区这五部分。

在介绍这五个内存区域前,我们先了解一下,Java文件被JVM加载到内存中的过程:

  1. A.java文件经过编译器编译,生成A.class字节码文件
  2. 程序访问A这个类时,会通过ClassLoader(类加载器)将A.class加载到JVM的内存中
  3. JVM将内存区域划分为若干个不同的数据区域,主要分为:程序计数器虚拟机栈本地方法栈方法区这五部分。

JVM内存分布

程序计数器

程序计数器的作用

Java程序是多线程的,线程的执行与挂起由CPU来决定。当CPU将一个线程(暂且就叫A线程)挂起,去执行另一线程(暂且叫B线程)时,这时需要记录代码已经执行到的位置;当CPU重新执行A线程时,就可以根据之前记录的位置,知道自己应该从哪行代码开始继续执行下去。这就程序计数器的作用,程序计数器是虚拟机中一块比较小的内存空间,主要用于记录当前线程执行的位置。

当然,除了上述的线程的恢复线程操作之外,还有一些其他操作也依赖程序计数器来完成,比如:跳转、异常处理等

程序计数器的几个注意点:

  1. 程序计数器是线程私有的,每条线程内部都有一个私有的程序计数器。它的生命周期随着线程创建而创建,随着线程的结束而结束
  2. 在JVM规范中,对程序计数器这一区域没有规定任何的OutOfMemoryError的情况
  3. 当一个线程正在执行一个Java方法的时候,程序计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,则计数器的值为空

虚拟机栈

虚拟机栈也是线程私有的,与线程的生命周期同步。我们常听说的 “JVM是基于栈的解释器执行的,DVM是基于寄存器的解释器执行的” 这句话中,“基于栈”就指的是虚拟机栈。每当有方法被执行时,JVM都会在虚拟机栈中创建一个栈帧。而这这个栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。每当一个线程在执行某个方法时,都会为这个方法创建一个栈帧(所以一个线程中可以有多个栈帧)。所以虚拟机栈其实就是用来描述Java方法执行的内存模型。
栈帧结构

栈帧的结构

这里我们暂时就简单了解一下栈帧的结构。

  • 局部变量表(Local Variable Table)
    局部变量表一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java文件编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

  • 操作数栈(Operand Stack)
    操作数栈也常称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。

    当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的。在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。

  • 动态链接
    class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分符号引用将在每一次运行期间转化为直接引用,这部分称为动态连接。

    虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

  • 方法返回地址
    当一个方法开始执行后,只有两种方式可以退出这个方法:

    • **正常退出:**指方法中的代码正常完成,或者遇到任意一个方法返回的字节码指令(如return)并退出,没有抛出任何异常。
    • **异常退出:**指方法执行过程中遇到异常,并且这个异常在方法体内内部没有得到处理,导致方法退出

    无论当前方法以哪种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复他的上层方法执行状态。

虚拟机栈的几个注意点:

在JVM规范中,对虚拟机栈规定了两种异常状况:

  1. **StackOverflowError:**当线程请求栈深超出JVM所允许的最大深度时抛出
  2. **OutOfMemoryError:**当JVM动态扩展到无法申请足够内存时抛出

本地方法栈

本地方法栈和虚拟机栈基本相同,只不过是针对本地(Native)方法。有些虚拟机的实现中已经两者合二为一了(比如HotSpot)

堆(Heap)是JVM所管理的内存区域中最大的一块,该区域唯一的目的就是存放对象实例,几乎所有对象的实例都在堆里分配,因此它也是Java垃圾收集器(GC)管理的主要区域。同时它也是所有线程共享的内存区域,因此被分配在区域的对象如果被多个线程访问,需要考虑线程安全问题。

而按照对象存储时间的不同,堆中的内存可以划分为新生代和年老代。其中新生代又被划分为Eden区和Survivor区。
堆内存分布

方法区

方法区是JVM规范里规定的一块运行时数据区。方法区主要是存储已经被JVM加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码与数据。方法区也是被各个线程共享的区域。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章