面试:精通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
目录
相关文章
|
4月前
|
消息中间件 存储 缓存
大厂面试高频:Kafka 工作原理 ( 详细图解 )
本文详细解析了 Kafka 的核心架构和实现原理,消息中间件是亿级互联网架构的基石,大厂面试高频,非常重要,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:Kafka 工作原理 ( 详细图解 )
|
3月前
|
存储 SQL 关系型数据库
MySQL进阶突击系列(03) MySQL架构原理solo九魂17环连问 | 给大厂面试官的一封信
本文介绍了MySQL架构原理、存储引擎和索引的相关知识点,涵盖查询和更新SQL的执行过程、MySQL各组件的作用、存储引擎的类型及特性、索引的建立和使用原则,以及二叉树、平衡二叉树和B树的区别。通过这些内容,帮助读者深入了解MySQL的工作机制,提高数据库管理和优化能力。
|
4月前
|
监控 算法 Java
Java虚拟机(JVM)的垃圾回收机制深度解析####
本文深入探讨了Java虚拟机(JVM)的垃圾回收机制,旨在揭示其背后的工作原理与优化策略。我们将从垃圾回收的基本概念入手,逐步剖析标记-清除、复制算法、标记-整理等主流垃圾回收算法的原理与实现细节。通过对比不同算法的优缺点及适用场景,为开发者提供优化Java应用性能与内存管理的实践指南。 ####
|
3月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
90 0
|
27天前
|
存储 NoSQL 前端开发
美团面试:手机扫描PC二维码登录,底层原理和完整流程是什么?
45岁老架构师尼恩详细梳理了手机扫码登录的完整流程,帮助大家在面试中脱颖而出。该过程分为三个阶段:待扫描阶段、已扫描待确认阶段和已确认阶段。更多技术圣经系列PDF及详细内容,请关注【技术自由圈】获取。
|
2月前
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
51 6
|
3月前
|
Java 数据库连接 Maven
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
自动装配是现在面试中常考的一道面试题。本文基于最新的 SpringBoot 3.3.3 版本的源码来分析自动装配的原理,并在文未说明了SpringBoot2和SpringBoot3的自动装配源码中区别,以及面试回答的拿分核心话术。
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
|
3月前
|
Rust 安全 Java
JVM原理与实现——Synchronized关键字
在多线程Java程序中,`Synchronized`关键字用于确保线程安全。本文深入探讨其工作原理,通过分析字节码`monitorenter`和`monitorexit`,解释JVM如何实现同步机制。文章展示了`Synchronized`方法的编译结果,并详细解析了轻量锁和重度锁的实现过程,包括Mark Word的状态变化及CAS操作的应用。最后简要介绍了`ObjectMonitor::enter()`函数在获取重度锁时的作用。
JVM原理与实现——Synchronized关键字
|
4月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
3月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。

热门文章

最新文章