小白也能看懂的JVM内存区域

时间:2021-04-30 14:54:40

前言

  最近在准备面试题刷到了JVM这块,作为一个小白,巩固知识点最好的方式就是亲手写出来并分享;相信我的理解,同样是小白的你,一定有很大的帮助。不信,请你往下看!

JVM内存区域简介

  如果有人问Java的内存区域或者运行时数据区域,说的就是JVM内存区域

  Java程序在运行的时候,Java虚拟机所管理的内存是被划分为若干个数据区域,注意这些数据区域不是固定死的,抽象得可以分成为JDK1.8前后的JVM内存区域,但是总体上差别不大。

一.JDK1.8前的JVM内存区域

  JVM内存区域从线程的角度可以分成:线程共享和线程私有;

    》线程共享的区域有:堆,方法区(永久代),直接内存(非运行时数据区域)

    》线程私有的区域有:程序计数器,虚拟机栈,本地方法栈

  如果有人问JVM内存区域,一般讲这5个就行:程序计数器,虚拟机栈,本地方法栈,堆,方法区;注意直接内存并不是运行时数据区域的一部分

小白也能看懂的JVM内存区域

1.程序计算器

  程序计算器是一块比较小的内存区域。大家应该知道线程在轮流切换执行时,线程执行到哪,那下一次拿到CPU使用权执行任务就从哪开始继续执行,简言之:从哪跌倒从哪爬起来;那是不是我们得知道它从哪跌倒的啊?没错,程序计算器的作用就在这了,你可以把它理解成是当前线程所执行的指令(字节码)的行号指示器,保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址)。

  线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,生命周期和线程一致。

  如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

2.虚拟机栈(Java栈)
  虚拟机栈描述的是 Java 方法执行的内存模型。什么意思呢?大家看培训班的视频的时候,机构老师应该会有提过一个术语:方法进栈。没错,这里的栈就是虚拟机栈,在执行java的方法的同时,会对应创建一个栈帧,方法进栈的“方法”明确的讲就是栈帧,虚拟机栈的组成部分也是栈帧;
  当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。看下图:
小白也能看懂的JVM内存区域

  栈帧可以理解为一种保存数据的基本数据结构,你只需要关心它保存的是什么即可,在上面的图说的很清楚了:局部变量表,操作栈等等

    》局部变量表:局部变量表是存放方法参数和局部变量的区域;对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译期就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

    》操作栈:是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令(语句)往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操作栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。

    》动态链接:指向当前方法所属的类的运行时常量池的引用;这个不是必须有的,如果方法中有使用到方法所属类中的常量,那这个动态链接就是这个常量在运行时常量池中的引用

    》方法返回地址:当一个方法执行完毕之后,要返回到之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

3.本地方法栈

  很多人对本地方法栈和java栈给搞混了,由于java栈是为java方法所服务的,因此也被叫做方法栈,那混淆就来了;

  方法栈是虚拟机栈,不是本地方法栈!

  方法栈是虚拟机栈,不是本地方法栈!

  方法栈是虚拟机栈,不是本地方法栈!重要事情说三遍!!

  ok,回到本地方法栈。本地方法栈其实和Java栈的作用和原理是很相似的。最大的区别在于Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。

4.Java堆

  对于大多数Javac程序来说,Java 堆是 Java 虚拟机所管理的内存中最大的一块,也是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

  堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。从GC的角度来看, 堆中还可以细分为:新生代和老年代;新生代再细致一点还可以分为: Eden 空间、From Survivor 空间、To Survivor 空间。

  Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有足够的内存区完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

  来!我们顺便在拓展下新生代和老年代的内容,这可是面试的热点,先上图:

小白也能看懂的JVM内存区域

  我们说过从GC的角度,堆内存还分成老年代和新生代。

  新生代:是用来存放新生或者创建不久的对象。一般占据堆的 1/3 空间。如果频繁创建对象,那么新生代会频繁地触发GC进行垃圾回收,因此新生代又分为 Eden 区、 ServivorFrom、 ServivorTo 三个区。

    1.Eden 区:Java 新对象的出生点(如果新创建的对象占用内存很大,则直接分配到老年代),当 Eden 区内存不够的时候就会触发 GC,对新生代区进行一次垃圾回收。

    2.ServivorFrom:上一次 GC 的幸存对象,但是它会作为本次 GC 的扫描目标

    3.ServivorTo:保留了一次 GC 过程中的幸存对象,简单说就是GC未清除对象会被放在这个区域。

  GC在回收新生代的过程有3个阶段:复制-->清空-->互换

    复制:首先,eden、servicorFrom区域存活的对象会先被复制到 ServicorTo,同时这些对象的年龄+1(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),如果出现 复制的过程中ServicorTo 不够内存了,对象就放到老年区。

    清空:然后,清空 Eden 和 ServicorFrom 中的对象;

    互换:最后, ServicorTo 和 ServicorFrom的对象进行互换,这样的话,原 ServicorTo 将成为下一次 GC要清除的ServicorFrom区

  老年代:主要存放应用程序中生命周期长的内存对象,说简单点,就是很多次逃过GC的回收或者是因为对象内存过大的对象会被放到这里;老年代的对象比较稳定,所以GC 不会频繁执行。在进行 GC 前一般都是先进行了一次GC,使得有一些新生代的对象晋身入老年代,导致空间不够用时才触发GC。当没有足够大的连续空间分配给新创建的较大对象时也会提前触发一次 GC 进行垃圾回收腾出空间。

5.方法区

  方法区(Method Area)也是各个线程共享的内存区域,字节码文件被类加载器加载到JVM的第一块区域就是我们的方法区,它用于存储类信息、常量、静态变量等。

  Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

  方法区还有一个特别重要的区域就是运行时常量池,我们要知道Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于保存编译期生成的各种字面量和符号引用;在类加载并进入方法区后,常量池的内容会被放到运行时常量池中存放。

  运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是说并不是先放到Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

  最后,相信大家经常听到方法区和永久代,元空间等来挂钩甚至等同起来,如果要比喻他们的关系,方法区相当接口,永久代和元空间相当实现类,在JDK1.8前,HotSpot虚拟机(HotSpot是Java虚拟机的实现)以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。

6.直接内存

  直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但它会被频繁得使用;可以简单得理解为JVM的外设内存。

二.JDK1.8的JVM内存区域

    》线程共享的区域有:堆,直接内存(非运行时数据区域),元空间

    》线程私有的区域有:程序计数器,虚拟机栈,本地方法栈

小白也能看懂的JVM内存区域

  到JDK1.8其实和之前差别不大,唯一的不同在于JDK1.8 前,Hotspot 中方法区的实现是永久代,使用的是堆内存来保存对象实例;JDK1.8 开始使用元空间,以前永久代的所有字符串常量由堆内存进行管理,其他内容被放到了元空间,元空间直接在本地内存分配。

  以上是个人的一些理解,如果大家觉得哪里不妥,欢迎指正!

参考资料:

  https://www.cnblogs.com/dolphin0520/p/3613043.html

https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md

  https://www.cnblogs.com/czwbig/p/11127124.html