面试官:JVM调优,主要针对是哪一个区域?JVM内存结构是怎样的?

jvm · 浏览次数 : 0

小编点评

Java内存结构详解 Java虚拟机(JVM)内存结构是程序员在编写Java程序时不可忽视的一部分。了解和掌握Java内存结构有助于开发者更好地理解和优化程序的性能。 一、JVM内存结构概述 Java虚拟机的内存结构可以分为以下几个部分: 1. 程序计数器(Program Counter Register) 2. Java虚拟机栈(Java Virtual Machine Stacks) 3. 本地方法栈(Native Method Stack) 4. Java堆(Java Heap) 5. 方法区(Method Area) 这些区域各自的作用如下: - 程序计数器:记录当前线程正在执行的字节码指令地址。 - Java虚拟机栈:存储局部变量表、操作数栈、动态链接、出口信息等。 - 本地方法栈:为虚拟机使用到的Native方法服务。 - Java堆:存储对象实例和数据。 - 方法区:存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量等。 二、线程私有与线程共享区域 - 程序计数器:线程私有,记录当前线程执行的字节码指令地址。 - Java虚拟机栈:线程私有,存储局部变量表、操作数栈、动态链接、出口信息等。 - 本地方法栈:线程私有,为虚拟机使用到的Native方法服务。 - Java堆:线程共享,存储对象实例和数据。 - 方法区:线程共享,存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量等。 三、JVM调优关注区域 在JVM调优时,主要关注以下区域: - Java堆:优化堆的大小、分配策略、垃圾回收算法等。 - 方法区:优化常量池大小、类型信息存储方式、字段和方法的访问权限等。 四、JVM内存异常 - OutOfMemoryError:当内存不足时,可能会抛出此错误。 - StackOverflowError:当线程请求的栈深度超过Java虚拟机栈的最大深度时,会抛出此错误。 五、方法区的演进 JDK6及之前有永久代,后来逐步淘汰,改为使用元空间(元空间完全由本地内存构成,不再局限于永久代)。JDK8及以后的版本中,方法区主要由本地内存的元空间和堆中的字符串常量池、静态变量组成。 总之,了解Java内存结构对于Java程序员来说是至关重要的。在实际开发中,正确地使用内存、避免内存泄漏和溢出等问题都是内存管理的核心任务。

正文

作为一个Java程序员,在日常的开发中,不必像C/C++程序员那样,为每一个内存的分配而操心,JVM会替我们进行自动的内存分配和回收,方便我们开发。但是一旦发生内存泄漏或者内存溢出,如果对Java内存结构不清楚,那将会是一件非常麻烦的事情!本文笔者将为大家详解Java内存结构。

面试tips

  1. 聊聊Java内存结构?都有哪些组成部分?哪些是线程共享?哪些是线程私有?
  2. 我们通常说的JVM调优,主要针对是哪一个区域?这个区域中那一块是最大的?主要用于存放什么内容?
  3. Java虚拟机栈存储的内容是什么?
  4. 程序计数器的作用是什么?当内存不足时,程序计数器会发生OOM吗?
  5. 聊聊你对方法区的看法?在不同JDK版本中,方法区的演进过程是什么?

你是否对这些问题都了如指掌?看完本文相信你心中就会有答案!

JVM架构

JVM的平台无关性

jvm与操作系统

  1. 计算机的CPU、内存、显卡等等属于硬件
  2. 常用的MacOs、Windows、Linux属于计算机的操作系统
  3. 而Java的虚拟机,也就是JVM是运行在操作系统之上的,与硬件没有直接联系,JVM也是Java能够跨平台的根本原因。

JVM架构

image-20240630174946980

1. Class Loader 类加载器

类加载器的作用是加载类文件到内存,比如编写一个 HelloWord.java 文件,然后通过 javac 编译成 class 文件,那怎么才能加载到内存中被执行呢?答案就是 Class Loader。当然,不是任何 .class 文件就能被加载的,Class Loader 加载的 class 文件是有格式要求

2. Execution Engine 执行引擎

Class Loader 只负责加载,只要符合文件结构就加载,至于说能不能运行,则不是它负责的,那是由 Execution Engine 负责的。执行引擎也叫做解释器 (Interpreter),负责解释命令,提交操作系统执行

3. Native Interface 本地接口

本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++ 程序,Java 诞生的时候是 C/C++ 横行的时候,于是就在内存中专门开辟了一块区域处理标记为 native 的代码

4. Runtime data area 运行时数据区

运行时数据区是整个 JVM 的重点。我们所有写的程序都被加载到这里,之后才开始运行,下面会重点讲解运行时数据区。

JVM执行流程

当然不同的VM的具体实现细节也不是不一样的,现在使用的比较多的JDK8版本就是Sun HotSpot VM与BEA JRockit VM合并之后开发出的JDK版本。

下面就是一个Java文件加载并执行的流程

JVM架构

运行时数据区

运行时数据区是JVM中最为重要的部分。也是我们在调优时需要重点关注的区域。

运行时数据区分为:程序计数器Java虚拟机栈本地方法栈Java堆区方法区

其中

  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、直接内存)

JDK 1.8 和之前的版本略有不同,我们这里以 JDK 1.7 和 JDK 1.8 这两个版本为例介绍。

JDK 1.7

java-runtime-data-areas-jdk1.7

JDK 1.8

java-runtime-data-areas-jdk1.8

程序计数器

程序计数寄存器(Program Counter Register),Register 的命名源于 CPU 的寄存器,寄存器存储指令相关的线程信息,CPU 只有把数据装载到寄存器才能够运行。它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域

  1. JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。可以看作是当前线程所执行的字节码的行号指示器。解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

  2. 由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器

  3. 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。如果当前线程正在执行的是 Java 方法,程序计数器记录的是 JVM 字节码指令地址,如果是执行 native 方法,则是未指定值(undefined)

  4. 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期与线程保持一致。

jvm-pc-counter

Java虚拟机栈

每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。

1、栈的内部结构

每个栈帧(Stack Frame)中存储着:

  • 局部变量表(Local Variables):主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)

  • 操作数栈(Operand Stack):主要用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中

  • 动态链接(Dynamic Linking):指向运行时常量池的方法引用。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用,当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。这个过程也被称为 动态连接

  • 方法返回地址(Return Address):方法正常退出或异常退出的地址

PS: 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

img

2、栈的执行流程

  • JVM 直接对虚拟机栈的操作只有两个:方法调用入栈,方法执行结束出栈
  • 在线程中,同一时间只会有一个活动的栈帧,即(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧
  • 不同线程中所包含的栈帧是不允许相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧

jvm-stack-frame

3、栈的异常

  • StackOverFlowError 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。

  • OutOfMemoryError 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

本地方法栈

本地方法栈和Java虚拟机栈所发挥的作用非常相似

  • 二者区别在于: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务

  • 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

  • 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowErrorOutOfMemoryError 两种异常。

  • Hotspot JVM 中,直接将本地方法栈和虚拟机栈合二为一

Java堆区

栈是运行时的单位,而堆是存储的单位

Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享

PS:关于Java堆有很多细节可以深挖,例如堆的分代和对象的创建和回收等,后续我还会专门开一篇文章展开讲

1、堆的存储内容

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。成员变量名和值存储于堆中,其生命周期和对象的是一致的。

Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

2、堆的分区和垃圾回收

为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能):

  • 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
  • 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大
  • 元空间(JDK1.8 之前叫永久代):一些方法中的操作临时对象等,JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存

img

3、堆出现的异常

堆这里最容易出现的就是 OutOfMemoryError 错误,比如:

  • java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。

  • java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值,详见:Default Java 8 max heap sizeopen in new window)

方法区

方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域

1、方法区和永久代以及元空间是什么关系呢?

方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

method-area-implementation

2、方法区的存储内容

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。在加载类和结构到虚拟机后,就会创建对应的运行时常量池

运行时常量池(Runtime Constant Pool)是虚拟机规范中是方法区的一部分,在加载类和结构到虚拟机后,就会创建对应的运行时常量池;而字符串常量池是这个过程中常量字符串的存放位置。所以从这个角度,字符串常量池属于虚拟机规范中的方法区,它是一个逻辑上的概念;而堆区,永久代以及元空间是实际的存放位置。

3、方法区在 JDK6、7、8中的演进细节

JDK版本 是否有永久代,字符串常量池放在哪里? 方法区逻辑上规范,由哪些实际的部分实现的?
jdk1.6及之前 有永久代,运行时常量池(包括字符串常量池),静态变量存放在永久代上 这个时期方法区在HotSpot中是由永久代来实现的,以至于这个时期说方法区就是指永久代
jdk1.7 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中; 这个时期方法区在HotSpot中由永久代(类型信息、字段、方法、常量)和(字符串常量池、静态变量)共同实现
jdk1.8及之后 取消永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中 这个时期方法区在HotSpot中由本地内存的元空间(类型信息、字段、方法、常量)和(字符串常量池、静态变量)共同实现

method-area-jdk1.6

method-area-jdk1.7

参考文章

  1. 【002】十分钟搞懂Java内存结构

  2. JVM的内存分区/内存结构/内存区域/JVM内存模型

  3. JVM 基础 - JVM 内存结构

  4. Java内存区域详解(重点)

  5. 浅谈JVM整体架构与调优参数

与面试官:JVM调优,主要针对是哪一个区域?JVM内存结构是怎样的?相似的内容:

面试官:JVM调优,主要针对是哪一个区域?JVM内存结构是怎样的?

作为一个Java程序员,在日常的开发中,不必像C/C++程序员那样,为每一个内存的分配而操心,JVM会替我们进行自动的内存分配和回收,方便我们开发。但是一旦发生内存泄漏或者内存溢出,如果对Java内存结构不清楚,那将会是一件非常麻烦的事情!本文笔者将为大家详解Java内存结构。

京东面试:如何进行JVM调优?

JVM 调优是一个很大的话题,在回答“如何进行 JVM 调优?”之前,首先我们要回答一个更为关键的问题,那就是,我们为什么要进行 JVM 调优? 只有知道了为什么要进行 JVM 调优之后,你才能准确的回答出来如何进行 JVM 调优? 要进行 JVM 调优无非就是以下两种情况: 目标驱动型的 JVM

JVM调优篇:探索Java性能优化的必备种子面试题

本文将带你深入了解JVM调优的重要性、常见问题以及一些实用的调优工具和方法,助你在面试的过程中轻松应对

JVM GC配置指南

本文旨在简明扼要说明各回收器调优参数,如有疏漏欢迎指正。 #### 1、JDK版本 以下所有优化全部基于JDK8版本,强烈建议低版本升级到JDK8,并尽可能使用update_191以后版本。 #### 2、如何选择垃圾回收器 响应优先应用:面向C端对响应时间敏感的应用,堆内存8G以上建议选择G1,堆

【转帖】一道面试题:JVM老年代空间担保机制

面试问题 昨天面试的时候,面试官问的问题: 什么是老年代空间担保机制?担保的过程是什么?老年代空间担保机制是谁给谁担保?为什么要有老年代空间担保机制?或者说空间担保机制的目的是什么?如果没有老年代空间担保机制会有什么不好? 下面我们就带着这些问题去了解一下JVM老年代空间担保机制吧。 老年代空间担保

面试官:如何打破双亲委派机制?

面试连环call:1. 双亲委派机制是什么?如何打破双亲委派机制?2. JVM都有哪些类加载器?3. 如何构造一个自定义类加载器?

面试官:Java类是如何被加载到内存中的?

面试连环call Java类是如何被加载到内存中的? Java类的生命周期都有哪些阶段? JVM加载的class文件都有哪些来源? JVM在加载class文件时,何时判断class文件的格式是否符合要求? 类生命周期 一个类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期将会经历加载、验

面试官:什么是双亲委派模型?

本文已经收录进 JavaGuide(「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。) 参加过校招面试的同学,应该对这个问题不陌生。一般提问 JVM 知识点的时候,就会顺带问你双亲委派模型(别扭的翻译。。。)。 就算是不准备面试,学习双亲委派模型对于我们也非常有帮助。

一文了解JVM面试篇(上)

Java内存区域 1、如何解释 Java 堆空间及 GC? 当通过 Java 命令启动 Java 进程的时候,会为它分配内存。内存的一部分用于创建 堆空间,当程序中创建对象的时候,就从对空间中分配内存。GC 是 JVM 内部的一 个进程,回收无效对象的内存用于将来的分配。 2、JVM 的主要组成部分

【转帖】8.JVM双亲委派机制(面试常问)

目录 1.什么是双亲委派机制?2.双亲委派机制的优势3.沙箱安全机制 1.什么是双亲委派机制? 双亲委派机制工作原理:(面试) 1.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。 2.如果父类的加载器还存在其父类加载器,则进一步向上委托,依次递归,请