前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【原创】面试官:JVM内存区域你了解吗?

【原创】面试官:JVM内存区域你了解吗?

作者头像
良月柒
发布2020-04-14 14:42:28
4370
发布2020-04-14 14:42:28
举报

1.JVM 内存区域

该结构图 JDK 版本:JDK 1.7

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【Java 堆、方法区】、直接内存。

线程私有数据区域生命周期与线程相同,依赖用户线程的启动/结束而创建/销毁(在 Hotspot VM 内,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存/否跟随系统本地线程的生/死对应)。

线程共享区随虚拟机的启动/关闭而创建/销毁。

直接内存并不是 JVM 运行时数据区的一部分,但也会被频繁的使用:在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式,它可以使用 Native 函数库直接分配堆外内存,然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作(详见:Java I/O扩展),这样就避免了在 Java 堆和 Native 堆中来回复制数据,因此在一些场景中可以显著提高性能。

2.内存区域思维导图

需要xmind文件的可从我公众号加我微信。

3.程序计数器(线程私有)

程序计数器是一个记录着当前线程所执行的字节码的行号指示器。

只占用一块较小的内存空间(在进行 JVM 计算时,可以忽略不计),每条线程都有一个独立的程序计数器,这类内存也称为“线程私有”的内存。

画外音:假设程序永远只有一个线程,我们就不需要程序计数器。 JVM 的多线程是通过 CPU 时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。 也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。 当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。

正在执行 Java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是执行 Native 方法,则它的值为空。

这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

4.虚拟机栈(线程私有)

描述 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Statkc Frame)用于存储【局部变量表】、【操作数栈帧】、【动态链接】、【方法出口】等信息。

每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

如下图所示:

画外音:栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未捕获的异常)都算作方法结束。

栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

栈帧结构图:

5.本地方法栈(线程私有)

本地方法栈和虚拟机栈(Java Stack)作用类似,区别是虚拟机栈为执行 Java 方法服务,而本地方法栈则为 Native 方法服务,如果一个 VM 实现使用 C-likage 模型来支持 Native 调用,那么该栈将会是一个 C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

画外音:在HotSpot 虚拟机中未对本地方法栈和虚拟机栈作区分,统称为栈。

6. 堆(Heap-线程共享)-运行时数据区

堆是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。

由于现代 VM 采用分代收集算法,因此 Java 堆从 GC 的角度还可以细分为:新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代

如下图:

画外音:新生代划分也有这样的叫法:伊甸园(Eden space),幸存者0区(Survivor 0 space)和幸存者1区(Survivor 1 space)

7.方法区(线程共享)

JDK1.7 及之前版本的方法区和 Java 堆一样,是各个线程共享的内存区域,也称非堆(Non-Heap),用于存储已经被虚拟机加载的【类信息】、【常量】、【静态常量】、【即时编译器(JIT)编译后的代码】等数据,它同样存在垃圾回收,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

画外音:对于方法区,很多人更愿意称为:“永久代(Permanent Generation)”,不过本质上两者并不等价,仅仅是因为习惯使用 HotSpot 虚拟机的设计团队选择把 GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存,能够省去专门为方法区变编写内存管理代码的工作。 不过对于其他虚拟机(如BEA JRockit、IBM J9等)来说并不存在永久代的概念

方法区存储的是每个 class 的信息:

画外音:类型的常量池,也叫运行时常量池,每一个Class文件中,都维护着一个常量池(这个保存在类文件里面,不要与方法区的运行时常量池搞混),里面存放着编译时期生成的各种字面值和符号引用; 这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 。

运行时常量池

除了保存已加载的类信息,还有一个特殊的部分——运行时常量池(Runtime Constant Pool)。

运行时常量是方法区一个特殊的部分,相对于常量来说的,它具备一个重要特征是:动态性。也就是说,除了类加载时将常量池写入其中,Java 程序运行期间也可以向其中写入常量:

代码语言:javascript
复制
//使用StringBuilder在堆上创建字符串abc,再使用intern将其放入运行时常量池
String str = new StringBuilder("abc");
str.intern();

//直接使用字符串字面量xyz,其被放入运行时常量池
String str2 = "xyz";

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

  • 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
  • 节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

方法区的实现

方法区只是JVM规范定义,而永久代为具体的实现,方法区的实现在虚拟机规范中并未明确规定,目前有2种比较主流的实现方式:

(1)HotSpot 虚拟机1.7-:在 JDK1.6 及之前版本,HotSpot 使用“永久代(permanent generation)”的概念作为实现,即将 GC 分代收集扩展至方法区。

这种实现比较偷懒,可以不必为方法区编写专门的内存管理,但带来的后果是容易碰到内存溢出的问题(因为永久代有 -XX:MaxPermSize 的上限)。

在 JDK1.7+ 之后,HotSpot 逐渐改变方法区的实现方式,如 1.7 版本移除了方法区中的字符串常量池,放到了堆中,符号引用(Symbols)转移到了 Native Heap;字面量(interned strings)转移到了 Java heap;类的静态变量(class statics)转移到了 Java heap。

画外音:什么是字符串常量池? 在 JAVA 语言中有8中基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。 8种基本类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。
  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

(2)HotSpot 虚拟机 1.8+:1.8 版本中移除了方法区并使用 MetaSpace(元数据空间)作为替代实现,MetaSpace 存储类的元数据信息。

MetaSpace 占用系统内存,也就是说,只要不碰触到系统内存上限,方法区会有足够的内存空间。

但这不意味着我们不对方法区进行限制,如果方法区无限膨胀,最终会导致系统崩溃

画外音:什么是类的元数据? 元数据是指用来描述数据的数据,更通俗一点,就是描述代码间关系,或者代码与其他资源(例如数据库表)之间内在联系的数据。 在一些技术框架,如struts、EJB、hibernate就不知不觉用到了元数据。 对struts来说,元数据指的是struts-config.xml;对EJB来说,就是ejb-jar.xml和厂商自定义的xml文件;对hibernate来说就是hbm文件。

JDK 1.8 结构图如下:

我们思考一个问题,为什么使用“永久代”并将 GC 分代收集扩展至方法区这种实现方式不好,会导致OOM?

首先要明白方法区的内存回收目标是什么,方法区存储了类的元数据信息和各种常量,它的内存回收目标理应当是对这些类型的卸载和常量的回收。

但由于这些数据被类的实例引用,卸载条件变得复杂且严格,回收不当会导致堆中的类实例失去元数据信息和常量信息。

因此,回收方法区内存不是一件简单高效的事情,往往 GC 在做无用功。

另外随着应用规模的变大,各种框架的引入,尤其是使用了字节码生成技术的框架,会导致方法区内存占用越来越大,最终 OOM。

两者结构上的区别

为什么 JDK 1.8 要把方法区从 JVM 里移到直接内存?

原因一:因为直接内存,JVM将会在 IO 操作上具有更高的性能,因为它直接作用于本地系统的 IO 操作。而非直接内存,也就是堆内存中的数据,如果要作 IO 操作,会先复制到直接内存,再利用本地 IO 处理。

  • 从数据流的角度,非直接内存是下面这样的作用链:本地 IO --> 直接内存 --> 非直接内存 --> 直接内存 --> 本地 IO。
  • 而直接内存是:本地 IO --> 直接内存 --> 本地 IO。

原因二:整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。

  • 可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。
  • -XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

画外音:而且应该为 PermGen 分配多大的空间很难确定,因为 PermSize 的大小依赖于很多因素,比如 JVM 加载的 class 总数,常量池的大小,方法的大小等。

原因三:PermGen 中类的元数据信息在每次 FullGC 的时候可能被收集,但成绩很难令人满意

原因四:官方文档表示,移除永久代是为融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,不需要配置永久代。

不管做什么,只要坚持下去就会不一样!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员的成长之路 微信公众号,前往查看

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

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.JVM 内存区域
  • 2.内存区域思维导图
  • 3.程序计数器(线程私有)
  • 4.虚拟机栈(线程私有)
  • 5.本地方法栈(线程私有)
  • 6. 堆(Heap-线程共享)-运行时数据区
  • 7.方法区(线程共享)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档