面试:精通Java;面试官:来讲一下JVM虚拟机内存模型的最底层原理,必须说详细说清楚,知其所以然。看完后,你还敢在简历上写精通Java吗?

简介: 面试:精通Java;面试官:来讲一下JVM虚拟机内存模型的最底层原理,必须说详细说清楚,知其所以然。看完后,你还敢在简历上写精通Java吗?

精通Java?来看看下面这些底层中的底层原理你是否知道吧。
提到JVM必不可少的就得谈到它的内存模型,根据 JVM 规范,JVM 内存共分为虚拟机栈VM stack堆heap方法区Method Area程序计数器Program Counter Register本地方法栈Native Method Stack五个部分。如下图,咋们分别对这五个区域进行详细的原理讲解。(为节省读者的时间,方便大家理解记忆,笔者把全部知识点分层分段,用较短的语言去描述,言简意赅,句句都是重点。)

在这里插入图片描述

1.虚拟机栈(VM stack)

  • 每个线程有一个私有的栈,随着线程的创建而创建。

  • 能抛出StackOverflowError和OutOfMemoryError异常。

    • 如果线程请求分配的栈容量超过虚拟机栈允许的最大容量,java虚拟机将会抛出一个Stack Overflow Error异常。
    • 如果虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时,没有足够的内存去创建对应的虚拟机栈,那么java虚拟机将会抛出一个OutOfMemoryError异常。
  • 方法调用相关知识:

    • 方法调用时,创建栈帧,并压入虚拟机栈;方法执行完毕,栈帧出栈并被销毁,
    • 栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧结构分为:局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。
    • 我们debug的时候可以很明确的看到Frames,如下图:
      在这里插入图片描述
      对应的原理图如下:
      在这里插入图片描述
  • 内存指的便是虚拟机栈的栈帧中的局部变量表,因为这里存放了一个方法的所有局部变量。

  • 栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个固定的值,(我们也可以更改vmoptions文件中的参数来调整其大小,具体参见本文最后的附录,这里先不展开),大家可以通过下面的代码进行测试:

public class HeapDeepDemo {
    

    private static int index = 0;

    public void addIndex() {
    
        index++;
        addIndex();
    }


    public static void main(String[] args) {
    

        HeapDeepDemo heapDeepDemo = new HeapDeepDemo();

        try {
    
            heapDeepDemo.addIndex();
        } catch (Error e) {
    
            System.out.println("Stack deep : " + index);
            e.printStackTrace();
        }
    }
}

四次执行结果都不同,如下:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.本地方法栈(Native Method Stack)

Java方法调用本地方法栈的过程如下图:
在这里插入图片描述
什么是本地方法栈(Native Method Stack)?

一个Native Method就是一个Java调用非Java代码的接口。方法的实现由非Java语言实现,比如C或C++。他的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。

为什么要用到本地方法栈(Native Method Stack)?

有些层次的任务用java实现起来不容易,或者对程序的效率有要求时,还有时java应用需要与java外部的环境交互,这就是本地方法存在的主要原因。

本地方法栈的知识点:

上面我们提到了VM虚拟机栈,虚拟机栈用于管理java方法的调用,而本地方法栈用于管理本地方法的调用,各司其职。和虚拟机栈VM stack一样,本地方法栈Native Method Stack同样它也是线程私有的。和虚拟机栈VM stack一样,允许被实现成固定或者是可动态扩展的大小和虚拟机栈VM stack一样,本地方法栈Native Method Stack也能抛出StackOverflowError和OutOfMemoryError异常。 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,java虚拟机将会抛出一个Stack Overflow Error异常。如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时,没有足够的内存去创建对应的本地方法栈,那么java虚拟机将会抛出一个OutOfMemoryError异常。

3.程序计数器(Program Counter Register)

3.1类比X86架构中的IP指令指针寄存器

程序计数器又可翻译为PC寄存器,可类比汇编与微机原理中X86架构的CPU中的IP指令指针寄存器,详情可参考本人另外一篇博客:https://blog.csdn.net/MrYushiwen/article/details/122627634
其中的大致内容截取如下:
在这里插入图片描述

x86架构的寄存器图如下:(记得我们大学的课本上的图也是这么画的,图片恒永久,一张永流传!哈哈哈)
在这里插入图片描述

为什么要类比我们x86架构中的ip指令指针寄存器呢,因为在 Java1.2 之后. Linux中的JVM是基于pthread实现的, 可以直接说 Java 线程就是依赖操作系统实现的,Java线程总是需要以某种形式映射到OS线程上;映射模型可以是1:1(原生线程模型)、n:1(绿色线程 / 用户态线程模型)、m:n(混合模型)。它目前在大多数平台上都使用1:1模型。也就是每个Java线程都直接映射到一个OS线程上执行。此时,native方法就由原生平台直接执行,并不需要理会抽象的JVM层面上的“pc寄存器”概念——原生的CPU上真正的PC寄存器是怎样就是怎样。现在的Java中线程的本质,其实就是操作系统中的线程。

3.2JVM中的程序计数器

它在JVM中是一块较小的内存空间,JVM支持多个线程同时运行,每个线程都有自己的程序计数器。它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。运行时的特点: 随着线程的创建而启动;如果线程正在执行的是Java 方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是Native 方法,则这个技术器值为空(Undefined);此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

4.方法区(Method Area)

方法区是所有线程共享。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
在这里插入图片描述方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。永久代(PermGen)、方法区(Method Area)、元空间(Metaspace)之间的关系:
方法区(Method Area)是规范层面的东西,规定了这一个区域要存放哪些东西,永久代(PermGen)和元空间(Metaspace)是对方法区(Method Area)的不同实现。永久代(PermGen)是Java7以及之前JVM对于方法区(Method Area)的实现。元空间(Metaspace)是Java8以及之后JVM对于方法区(Method Area)的实现。举个例子:方法区比作手机,那么永生带可以比作诺基亚手机,元空间可以比作华为手机。Java8的时候为什么要用元空间(Metaspace)替换掉永久代(PermGen): 永久代大小有限制,如果加载的类太多,很可能导致永久代内存溢出,即java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM。移除 永久代 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。在Java7的时候,对于interned strings,不再分配在堆的永久代中了,而是分配在了堆中的主要部分:新生代和老年代中。在Java8的时候官方文档讲到了移除了永久代,但没有说其它关于interned strings相关的变化信息,因此,可以确定在Java8中字符串常量池存放在堆中。也就是说在Java8的时候方法区由原来的永久代变成了元空间(类信息)和堆实现(常量池、静态变量)两个部分。堆中包含正常对象和常量池,new String()放入堆中,String::intern方法会首先从堆中的常量池中取,如果常量池中没有,就在常量池中保存String的值,然后返回其引用,下次在调用String::intern方法时,会直接返回常量池中的该值。我们在Java8中也可以说常量池在方法区,因为永久代(PermGen)和元空间(Metaspace)是对方法区(Method Area)的不同实现,在上面我们刚刚也提到过。元空间是使用本地内存(Native Memory)实现的,也就是说它的内存是不在虚拟机内的,所以可以理论上物理机器还有多个内存就可以分配,而不用再受限于JVM本身分配的内存如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。

5.堆(heap)

Java7以及之前的结构图:
在这里插入图片描述
在4章节方法区(Method Area)中以及提到过Java8的时候用元空间(Metaspace)替换掉了永久代(PermGen),所以Java8以及之后的图如下:
在这里插入图片描述

堆为什么为什么分代:

分代的唯一理由就是优化GC性能。如果没有分代,所有的对象都在一块,GC的时要找到哪些对象是没用的,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,比如年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法(后续会写一遍博文详细介绍)。如果分代的话,把新创建的对象放到某一地方,当GC的时先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

Minor GC、Major GC和Full GC之间的区别:

Minor GC或Young GC,用来回收年轻代(包括 Eden 和 Survivor 区域)内存空间的 。Old GC ,是清理老年代内存空间的。Full GC ,是回收整个堆空间,包括年轻代和老年代。在Java7以及之前还包括永久代;Java8及以后由于改成了元空间,它的垃圾回收就不是由java来控制了,元空间的默认情况下内存空间是使用的操作系统的内存空间,所以空间的容量是比较充裕的,不会发生元空间的空间不足问题,如果Metaspace的空间占用达到了设定的最大值,那么也会触发GC来收集死亡对象和类的加载器。Major GC ,有的人会把它和 Old GC等价,有的人会把它和Full GC等价,我们尽量不提这个Major GC,如果提到了,要问清楚对方指的是Old GC还是Full GC。关于GC以及GC回收算法,笔者会在后续写一遍博文详细介绍。

HotSpot JVM把年轻代分为了三部分:

三个部分分别是1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,老年代的内存空间超过某个阈值或者远大于新生代时,会进行一次Full GC,而Full GC消耗的时间比Minor GC长得多。设置两个Survivor区最大的好处就是解决了碎片化。

年轻代如何变成老年代

一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,
如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,
当它的年龄增加到一定程度时(一般16次),就会被移动到年老代中。

6.附录(VM options参数)

在这里插入图片描述
打开后的内容如下:
在这里插入图片描述

文件路径如下:
在这里插入图片描述
文件内容如下:
在这里插入图片描述
参数值如下:

-Xms1024m,设置JVM初始堆内存为1024m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。-Xmx2048m,设置JVM最大堆内存为2048m。-Xss512k,设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。-Xmn341m,设置年轻代大小为341m。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。-XX:NewSize=341m,设置年轻代初始值为341M。-XX:MaxNewSize=341m,设置年轻代最大值为341M。-XX:PermSize=512m,设置持久代初始值为512M,但在java8及之后就不支持了,改用-XX:MetaspaceSize=512m。-XX:MaxPermSize=512m,设置持久代最大值为512M,同样在java8及之后就不支持了,改用-XX:MaxMetaspaceSize=512m。-XX:NewRatio=2,设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示年轻代比年老代为1:2。-XX:SurvivorRatio=8,设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1个Eden区的比值为1:1:8,即1个Survivor区占整个年轻代大小的1/10。-XX:MaxTenuringThreshold=15,具体参看JVM系列之内存分配和回收策略中对象的衰老过程。-XX:ReservedCodeCacheSize=240m,设置代码缓存的大小,用来存储已编译方法生成的本地代码。更多参数配置说明请参考官方文档:https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html该篇已完结
2022.2.15,著于CSDN
YuShiwen
目录
相关文章
|
6天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
22天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
28天前
|
监控 Java easyexcel
面试官:POI大量数据读取内存溢出?如何解决?
【10月更文挑战第14天】 在处理大量数据时,使用Apache POI库读取Excel文件可能会导致内存溢出的问题。这是因为POI在读取Excel文件时,会将整个文档加载到内存中,如果文件过大,就会消耗大量内存。以下是一些解决这一问题的策略:
67 1
|
1月前
|
Java API 对象存储
JVM进阶调优系列(2)字节面试:JVM内存区域怎么划分,分别有什么用?
本文详细解析了JVM类加载过程的关键步骤,包括加载验证、准备、解析和初始化等阶段,并介绍了元数据区、程序计数器、虚拟机栈、堆内存及本地方法栈的作用。通过本文,读者可以深入了解JVM的工作原理,理解类加载器的类型及其机制,并掌握类加载过程中各阶段的具体操作。
|
2月前
|
缓存 监控 NoSQL
阿里面试让聊一聊Redis 的内存淘汰(驱逐)策略
大家好,我是 V 哥。粉丝小 A 面试阿里时被问到 Redis 的内存淘汰策略问题,特此整理了一份详细笔记供参考。Redis 的内存淘汰策略决定了在内存达到上限时如何移除数据。希望这份笔记对你有所帮助!欢迎关注“威哥爱编程”,一起学习与成长。
|
1月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
2月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
112 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
1月前
|
前端开发 Java 应用服务中间件
JVM进阶调优系列(1)类加载器原理一文讲透
本文详细介绍了JVM类加载机制。首先解释了类加载器的概念及其工作原理,接着阐述了四种类型的类加载器:启动类加载器、扩展类加载器、应用类加载器及用户自定义类加载器。文中重点讲解了双亲委派机制,包括其优点和缺点,并探讨了打破这一机制的方法。最后,通过Tomcat的实际应用示例,展示了如何通过自定义类加载器打破双亲委派机制,实现应用间的隔离。
|
2月前
使用qemu来dump虚拟机的内存,然后用crash来分析
使用qemu来dump虚拟机的内存,然后用crash来分析