前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM系列(一):Java虚拟机内存模型

JVM系列(一):Java虚拟机内存模型

原创
作者头像
xcbeyond
修改2020-02-11 09:51:03
8990
修改2020-02-11 09:51:03
举报
文章被收录于专栏:技术那些事技术那些事

一、前言

Java虚拟机,简称JVM(Java Virtual Machine),是Java语言中最为核心的一个东西,Java程序运行离不开它,因为它的存在,使得Java拥有“一次编译,多次运行”的特点。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。

JVM是Java中最难以理解、而且非常重要的知识点,也常常用来衡量一个人Java基本功是否牢靠,更是在面试中被问及最多、最频繁的知识点之一。本文将从Java虚拟机内存模型开始入手,一步步来了解它。

Java虚拟机内存模型是Java程序运行的基础,为了使Java应用程序正常运行,JVM将其内存数据分为程序计数器、虚拟机栈、本地方法栈、堆和方法区,如下图所示:

JVM内存模型
JVM内存模型

(在JDK1.8开始,已经去掉了方法区的概念,用元空间(Metaspace)进行了代替.)

程序计数器用于存放下一条运行的指令;虚拟机栈和本地方法栈用于存放函数方法调用堆栈信息;Java堆用于存放Java程序运行时所需的对象等数据;方法区用于存放程序的元数据信息

其中,一部分是线程私有的,而另一部分却是线程共享的。

  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:堆、方法区

二、程序计数器

程序计数器是一块很小的内存空间,用于存放下一条运行的指令,它是线程私有的,可以认作为当前线程的行号指示器。

由于Java是支持线程的语言,当线程数量超过CPU数量时,线程之间根据时间片轮询抢夺CPU资源。对于单核CPU而言,每一时刻,只能有一个线程在运行,而其他线程必须被切换出去。为此,每一个线程都必须用一个独立的程序计数器,来记录下一条要运行的指令。各个线程之间的计数器互不影响,独立工作。

如果当线程正在执行一个Java方法,则程序计数器记录正在执行的Java字节码地址,如果当前线程正在执行一个Native方法,则程序计数器为空。

三、虚拟机栈(栈)

栈保存的是方法的局部变量、部分结果,并参与方法的调用和返回,即:栈帧数据。

1.栈帧

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接方法、返回地址等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈(方法调用)到出栈(方法返回)的过程。

栈帧结构如下图所示:

栈帧结构
栈帧结构

如果方法调用时,方法的参数和局部变量相对较多,那么栈帧中局部变量表就会比较大,栈帧就很很大,因此,单个方法调用所需的栈空间大小也会很大。(在程序开发时,尽量避免这种情况,尤其是递归方法中要避免递归调用的深度)

以下代码片段中,通过逐步设置递归方法调用的深度,将会抛出栈溢出异常(StackOverflowError)。

代码语言:javascript
复制
public class StackTest {
    // 递归次数
    private final int count = 100000;

    /**
     * 递归方法
     * @param num
     */
    public void recursionMethod(int num) {
        num++;
        if (num < count) {
            recursionMethod(num);
        }
    }

    @Test
    public void stackDepthTest() {
        recursionMethod(0);
    }
}

2.栈溢出、内存溢出

Java虚拟机规范中允许栈的大小是动态的或者是固定的,定义了两种异常与栈空间相关:StackOverflowErrorOutOfMemoryError。如果线程在计算过程中,请求的栈深度大于最大可用的栈深度,则会抛出StackOverflowError异常,如果栈能够动态扩展,而在扩展过程中,没有足够的内存空间来支持栈的扩展,则会抛出OutOfMemoryError异常。

其中,可以使用JVM参数-Xss来调整设置栈的大小,从而决定了方法调用可以达到的深度。

针对上述代码StackTest中,在递归次数为100000时,将-Xss参数调整为-Xss512M后,未抛出异常。

-Xss参数调整
-Xss参数调整

3.jclasslib工具

篇外话,但觉得还是有必要提出来,在研究JVM时,总是会去研究一些字节码指令、Class类文件结构、大小等数据,而jclasslib工具恰恰满足这些,有了它更有助于我们对Java、JVM有更深入的了解。

大家可根据自己的喜好,选择安装,有单机软件版、IDE插件可供使用,在此,我选择的是在idea中安装了jclasslib插件,方便使用。此工具将伴随着你在JVM的世界里翱翔,一探JVM究竟。

以上述代码为例进行说明,如下图所示,在idea中通过jclasslib插件查看StackTest.class文件,展开方法recursionMethod后,查看Code属性的Misc页签中,当前方法的最大局部变量表的容量为2。因为在该方法中只有一个int类型的参数,所以共占2字。

jclasslib使用示例
jclasslib使用示例

关于jclasslib工具的更多使用技巧,在不断的使用中去摸索吧。

四、本地方法栈

本地方法栈和虚拟机栈的功能很相似,虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用

本地方法并不是用Java实现的,而是使用C实现的。本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法。

在Hot Spot虚拟机中,是不区分本地方法栈和虚拟机栈的。因此,本地方法栈一样也会抛出异常StackOverflowError和OutOfMemoryError。

五、堆

堆可以说是Java运行时内存中最为重要的部分,几乎所有的对象和数组都是在堆中分配空间的。堆分为新生代和老年代两部分,新生代用于存放刚刚产生的对象和年轻的对象,如果对象一直没有被收回,生存得足够长,老年对象就会被移入老年代。

新生代又可以进一步细分为edensurvivor space0(s0或者from space)和survivor space1(s1或者to space)。eden称之为伊甸园,即对象的出生地,大部分对象刚刚创建时,通常会存放在这里。s0和s1为survivor空间,直译为幸存者,就是指存放其中的对象至少经历了一次垃圾回收,并得以幸存。如果在幸存区的对象到了指定年龄仍未被回收,则有机会进入老年代。

换言之,堆空间简单分为新生代和老年代,新生代用于存放刚产生的新对象,老年代则存放年长的对象(存放时间较长,经过垃圾回收次数较多的对象)。

堆空间结构如下图所示:

堆空间结构
堆空间结构

六、方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。主要保存的信息是类的元数据,即类的类型信息、常量池、域信息、方法信息,如static修饰的变量加载类的时候就被加载到方法区中。

类型信息包括类的完整名称、父类的完整名称、类型修饰符(public/protected/private)和类型的直接接口类表;常量池包括这个类方法、域等信息所引用的常量信息;域信息包括域名称、域类型和域修饰符;方法信息包括方法名称、返回类型、方法参数、方法修饰符、方法字节码、操作数栈和方法帧栈的局部变量区大小以及异常表。总之,方法区内保存的信息,大部分都来自于class文件。

在Hot Spot虚拟机中,方法区也成为永久区,是一块独立于Java堆的内存空间。虽然叫做永久区,但是永久区中的对象,同样也是可以被GC回收的。只是对于GC的表现和Java堆空间略不相同。对永久区GC的回收,通常主要从两个方面分析:一是GC对永久区常量池的回收,二是永久区对类元数据的回收。

方法区也成为永久区,主要存放常量和类的定义信息。

(在JDK1.8的HotSpot虚拟机中,已经去掉了方法区的概念,用 Metaspace代替,并且将其移到了本地内存来规划了。)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、程序计数器
  • 三、虚拟机栈(栈)
    • 1.栈帧
      • 2.栈溢出、内存溢出
        • 3.jclasslib工具
        • 四、本地方法栈
        • 五、堆
        • 六、方法区
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档