【Java面试】说说JVM的内存结构以及他们那些会产生内存溢出?

简介: 【Java面试】说说JVM的内存结构以及他们那些会产生内存溢出?

内存结构

对于JVM的内存结构,我将按照上面两张图配合来讲。

Java Source就是我们的Java源代码,经过JVM编译器编译之后变为Java Class也称Java字节码文件。当我们运行java命令对字节码文件进行运行的时候,将会创建一个Java虚拟机。

此时虚拟机将会创建一个名字为main的主线程来执行我们的入口方法的代码,也就是它会去找main方法。

而运行代码也是需要内存的,而他的内存从JVM Stacks虚拟机栈中获取,当然不光光是主线程,还有我们在代码中new Thread的线程也是从这里获取内存资源。

当然,想要运行main方法,会发现main方法是在一个类Main中的,而此时还没有加载这个类呢,因此我们需要对这个类进行加载,此时就会通过类加载子系统将类的原始信息加载到Method Area方法区中,加载到这个区中的信息包括类的名字,类的继承关系,类的成员变量,类中引用的其他类,以及类的方法代码等。

而当我们的方法执行到一个new操作的时候,也就是需要创建一个实例对象的时候,对象所占用的内存来自于堆Heap,会先计算这个对象需要占用多大的内存,然后去堆中找到一个这么大的内存来创建这个对象。

而指向这个对象的引用,以及方法参数,他们都是引用值,他们就不是放在堆中了,而是放在虚拟机栈JVM Stacks中。

而当调用update这种我们自己编写的方法,或者调用hashCode这种Java无法实现,必须依靠底层操作系统来实现的代码,我们都放在JVM Stacks中,当然hashCode这种方法一般是放在Native Method Stacks本地方法栈中,不过我们JVM的实现是直接将他们整合在JVM Stacks中了。

而当前线程他不可能一直占用CPU,那么当他交出CPU的使用权的时候,下一次再次轮到它之后如何直到要从哪里开始呢?

这个时候就用到了PC Register,程序计数器,他将会当前这个线程运行到的代码的位置,因此即使当前线程被切换了,那么下次切换回来的时候通过程序计数器也能直到该从哪里继续运行。

那么当我们的对象被设定为null,或者没有引用的时候,那么我们的GC垃圾回收器将会对这些对象进行回收。

同时,我们的CPU其实是不能理解字节码指令的,因此我们还需要JVM提供的解释器对字节码指令进行解释,将其翻译成CPU可以理解的机器码指令,当然对于重复的代码也是重复进行解释的。

而对于一些频繁调用的热点代码,如执行万次的循环,那么就会通过JIT进行优化,加快这些热点代码的执行,从而提高效率。

Method Area(方法区)

(1)方法区是各个线程共享的内存区域,在虚拟机启动时创建

(2)虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非

堆),目的是与Java堆区分开来

(3)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

(4)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常,此时回看装载阶段的第2步,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,如果这时候把从Class文件到装载的第(1)和(2)步合并起来理解的话,可以画个图

类的加载流程

JVM运行时数据区是一种规范,真正的实现在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space

方法区,元空间,永久代的区别

Heap(堆)

(1)Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。

(2)Java对象实例以及数组都在堆上分配。

此时回看装载阶段的第3步,在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法

区中这些数据的访问入口,此时装载(1)(2)(3)的图可以改动一下

Java Virtual Machine Stacks(虚拟机栈)

经过上面的分析,类加载机制的装载过程已经完成,后续的链接,初始化也会相应的生效。

假如目前的阶段是初始化完成了,后续做啥呢?肯定是Use使用咯,不用的话这样折腾来折腾去

有什么意义?那怎样才能被使用到?换句话说里面内容怎样才能被执行?比如通过主函数main调

用其他方法,这种方式实际上是main线程执行之后调用的方法,即要想使用里面的各种内容,得

要以线程为单位,执行相应的方法才行。那一个线程执行的状态如何维护?一个线程可以执行多

少个方法?这样的关系怎么维护呢?

(1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程

的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创

建。

(2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。

调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

void a(){
b();
}
void b(){
c();
}
void c(){
}

栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。

每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A

reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。

局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中

局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使

用。

操作数栈:以压栈和出栈的方式存储操作数的

动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调

用过程中的动态连接(Dynamic Linking)。

方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇

见异常,并且这个异常没有在方法体内得到处理。

结合字节码指令理解栈帧
javap -c Person.class > Person.txt
Compiled from "Person.java"
class Person {
...
public static int calc(int, int);
Code:
0: iconst_3 //将int类型常量3压入[操作数栈]
1: istore_0 //将int类型值存入[局部变量0]
2: iload_0 //从[局部变量0]中装载int类型值入栈
3: iload_1 //从[局部变量1]中装载int类型值入栈
4: iadd //将栈顶元素弹出栈,执行int类型的加法,结果入栈
5: istore_2 //将栈顶int类型值保存到[局部变量2]中
6: iload_2 //从[局部变量2]中装载int类型值入栈
7: ireturn //从方法中返回int类型的数据
...
}

The Program Counter Register(程序计数器)

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据

CPU调度来的。

假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获

得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置。

如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;

如果正在执行的是Native方法,则这个计数器为空,Native方法由本地OS提供,如hashCode()。

Native Method Stacks(本地方法栈)

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。

那如果在Java方法执行的时候调用native的方法呢?

内存溢出的产生

上面的结构中,除了程序计数器不会发生内存溢出,其他的都会发生内存溢出。

出现 OutOfMemoryError的情况

  • 堆内存耗尽–对象越来越多,又一直在使用,不能被垃圾回收
  • 方法区内存耗尽–加载的类越来越多,很多框架都会在运行期间动态产生新的类
  • 虚拟机栈累积–每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁时出现

StackOverflowError的区域

  • 虚拟机栈内部–方法调用次数过多,每一次调用方法都会从线程分配到的内存中拿走一点去做函数调用,如果调用太多,那么内存不够了,就会溢出


相关文章
|
6月前
|
Web App开发 缓存 监控
内存溢出与内存泄漏:解析与解决方案
本文深入解析内存溢出与内存泄漏的区别及成因,结合Java代码示例展示典型问题场景,剖析静态集合滥用、资源未释放等常见原因,并提供使用分析工具、优化内存配置、分批处理数据等实用解决方案,助力提升程序稳定性与性能。
1811 1
|
11月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
947 55
|
监控 Java Unix
6个Java 工具,轻松分析定位 JVM 问题 !
本文介绍了如何使用 JDK 自带工具查看和分析 JVM 的运行情况。通过编写一段测试代码(启动 10 个死循环线程,分配大量内存),结合常用工具如 `jps`、`jinfo`、`jstat`、`jstack`、`jvisualvm` 和 `jcmd` 等,详细展示了 JVM 参数配置、内存使用、线程状态及 GC 情况的监控方法。同时指出了一些常见问题,例如参数设置错误导致的内存异常,并通过实例说明了如何排查和解决。最后附上了官方文档链接,方便进一步学习。
2849 4
|
8月前
|
安全 Oracle Java
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
607 0
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
|
11月前
|
存储 监控 算法
Java程序员必学:JVM架构完全解读
Java 虚拟机(JVM)是 Java 编程的核心,深入理解其架构对开发者意义重大。本文详细解读 JVM 架构,涵盖类加载器子系统、运行时数据区等核心组件,剖析类加载机制,包括加载阶段、双亲委派模型等内容。阐述内存管理原理,介绍垃圾回收算法与常见回收器,并结合案例讲解调优策略。还分享 JVM 性能瓶颈识别与调优方法,分析 Java 语言特性对性能的影响,给出数据结构选择、I/O 操作及并发同步处理的优化技巧,同时探讨 JVM 安全模型与错误处理机制,助力开发者提升编程能力与程序性能。
Java程序员必学:JVM架构完全解读
|
9月前
|
存储 运维 Kubernetes
Java启动参数JVM_OPTS="-Xms512m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError"
本文介绍了Java虚拟机(JVM)常用启动参数配置,包括设置初始堆内存(-Xms512m)、最大堆内存(-Xmx1024m)及内存溢出时生成堆转储文件(-XX:+HeapDumpOnOutOfMemoryError),用于性能调优与故障排查。
936 0
|
9月前
|
存储 Java 编译器
深入理解Java虚拟机--类文件结构
本内容介绍了Java虚拟机与Class文件的关系及其内部结构。Class文件是一种与语言无关的二进制格式,包含JVM指令集、符号表等信息。无论使用何种语言,只要能生成符合规范的Class文件,即可在JVM上运行。文章详细解析了Class文件的组成,包括魔数、版本号、常量池、访问标志、类索引、字段表、方法表和属性表等,并说明其在Java编译与运行过程中的作用。
273 0
|
存储 监控 算法
Java JVM 面试题
Java JVM(虚拟机)相关基础面试题
367 4
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?