面试:精通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
目录
相关文章
|
5天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
4天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
16 2
|
5天前
|
存储 安全 Java
什么是 Java 的内存模型?
Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范的一部分,它定义了一套规则,用于指导Java程序中变量的访问和内存交互方式。
18 1
|
10天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
21 1
|
14天前
|
监控 安全 Java
Java Z 垃圾收集器如何彻底改变内存管理
大家好,我是V哥。今天聊聊Java的ZGC(Z Garbage Collector)。ZGC是一个低延迟垃圾收集器,专为大内存应用场景设计。其核心优势包括:极低的暂停时间(通常低于10毫秒)、支持TB级内存、使用着色指针实现高效对象管理、并发压缩和去碎片化、不分代的内存管理。适用于实时数据分析、高性能服务器和在线交易系统等场景,能显著提升应用的性能和稳定性。如何启用?只需在JVM启动参数中加入`-XX:+UseZGC`即可。
143 0
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
331 0
|
13天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
25 1
|
17天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
21天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
26天前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
37 4