部分内容来源:JavaGuide+二哥Java
图解JVM内存结构
内存管理快速复习
栈帧:局部变量表,动态链接(符号引用转为真实引用),操作数栈(存储中间结算结果),方法返回地址
运行时常量池:常量池表,符号引用,字面量
对象创建过程:类加载检查(类没有加载就进行类加载)+分配内存+初始化零值+设置对象头+执行对象初始化方法
类加载过程:
- 加载:通过类的全限定名获取该类的二进制字节流,存到类常量池,内存中生成Class类对象
- 连接:
-
- 验证:验证Class二进制字节流合规(例如验证魔数和版本号)
- 准备:为类对象分配内存
- 解析:符号引用转为直接引用
- 初始化:执行初始化方法
内存分配:指针碰撞(CAS重试)+空闲列表
内存分配失败:CAS配上重试机制+TLAB为每个线程预先在Eden区分配一块内存
对象:对象头(运行时数据+类型指针,GC年龄,Hash码)+实例数据
对象访问定位:使用句柄(间接访问)+直接指针访问
JVM堆内存分区:新生代(Eden+S1+S2)+老年代
对象什么时候会进入老年代:长期存活的对象,大对象,动态年龄判断,分配担保机制
逃逸分析:在栈内为对象分配内存
Stop The World :停止所有用户线程
Oop Map:记录了对象内部所有引用字段(指针)的位置
安全点:可以暂停所有线程执行特定操作的位置
常量池包括:
类常量池
运行时常量池
字符串常量池
JDK1.6:常量池在永久代
JDK1.7:运行时常量池+类常量池在永久代,字符串常量池在堆
JDK1.8:运行时常量池+类常量池在元空间,字符串常量池在堆
引用类型有哪些?有什么区别?
强引用指的就是代码中普遍存在的赋值方式,比如 A a = new A () 这种。强引用关联的对象,永远不会被 GC 回收
软引用可以用 SoftReference 来描述,指的是那些有用但是不是必须要的对象
系统在发生内存溢出前会对这类引用的对象进行回收
弱引用可以用 WeakReference 来描述,他的强度比软引用更低一点
弱引用的对象下一次 GC 的时候一定会被回收,而不管内存是否足够
虚引用也被称作幻影引用,是最弱的引用关系,可以用 PhantomReference 来描述,他必须和 ReferenceQueue 一起使用,同样的当发生 GC 的时候,虚引用也会被回收
可以用虚引用来管理堆外内存
说一下弱引用?举例子在哪里可以引用?
Java 中的弱引用是一种引用类型,它不会阻止一个对象被垃圾回收
在 Java 中,弱引用是通过 Java.lang.ref.WeakReference 类实现的
弱引用的一个主要用途是创建非强制性的对象引用,这些引用可以在内存压力大时被垃圾回收器清理,从而避免内存泄露
弱引用的使用场景:
- 缓存系统:弱引用常用于实现缓存,特别是当希望缓存项能够在内存压力下自动释放时。如果缓存的大小不受控制,可能会导致内存溢出。使用弱引用来维护缓存,可以让 JVM 在需要更多内存时自动清理这些缓存对象。
- 对象池:在对象池中,弱引用可以用来管理那些暂时不使用的对象。当对象不再被强引用时,它们可以被垃圾回收,释放内存。
- 避免内存泄露:当一个对象不应该被长期引用时,使用弱引用可以防止该对象被意外地保留,从而避免潜在的内存泄露
说一下你对内存泄露和内存溢出的了解
什么是内存泄露:
内存泄漏是指程序在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致可用内存逐渐减少
虽然在 Java 中,垃圾回收机制会自动回收不再使用的对象,但如果有对象仍被不再使用的引用持有,垃圾收集器无法回收这些内存,最终可能导致程序的内存使用不断增加
内存泄露常见原因:
- 静态集合:使用静态数据结构(如 HashMap 或 ArrayList)存储对象,且未清理。
- 事件监听:未取消对事件源的监听,导致对象持续被引用。
- 线程没被回收:未停止的线程可能持有对象引用,无法被回收。
内存溢出:
内存溢出是指 Java 虚拟机(JVM)在申请内存时,无法找到足够的内存,最终引发 OutOfMemoryError 。这通常发生在堆内存不足以存放新创建的对象时
内存溢出常见原因:
- 大量对象创建:程序中不断创建大量对象,超出 JVM 堆的限制。
- 持久引用:大型数据结构(如缓存、集合等)长时间持有对象引用,导致内存累积。
- 递归调用:深度递归导致栈溢出
JVM的内存泄露有几种溢出情况?
堆内存溢出:当出现 Java.lang.OutOfMemoryError:Java heap space 异常时,就是堆内存溢出了。原因是代码中可能存在大对象分配,或者发生了内存泄露,导致在多次 GC 之后,还是无法找到一块足够大的内存容纳当前对象
栈溢出:如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的时候失败,则会抛出 OutOfMemoryError
元空间溢出:元空间的溢出,系统会抛出 Java.lang.OutOfMemoryError: Metaspace。出现这个异常的问题的原因是系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大
直接内存内存溢出:在使用 ByteBuffer 中的 allocateDirect () 的时候会用到,很多 JavaNIO (像 netty) 的框架中被封装为其他的方法,出现该问题时会抛出 Java.lang.OutOfMemoryError: Direct buffer memory 异常
栈中存的是指针还是对象?
在 JVM 内存模型中,栈(Stack)主要用于管理线程的局部变量和方法调用的上下文
而堆(Heap)则是用于存储所有类的实例和数组
当我们在栈中讨论 “存储” 时,实际上指的是存储基本类型的数据(如 int, double 等)和对象的引用,而不是对象本身。
这里的关键点是,栈中存储的不是对象,而是对象的引用
也就是说,当你在方法中声明一个对象,比如 MyObject obj = new MyObject ();,
这里的 obj 实际上是一个存储在栈上的引用,指向堆中实际的对象实例
这个引用是一个固定大小的数据(例如在 64 位系统上是 8 字节),它指向堆中分配给对象的内存区域
说一下程序计数器的作用?为什么程序计数器是私有的?
Java 程序是支持多线程一起运行的,多个线程一起运行的时候 cpu 会有一个调动器组件给它们分配时间片,比如说会给线程 1 分给一个时间片,它在时间片内如果它的代码没有执行完,它就会把线程 1 的状态执行一个暂存,切换到线程 2 去,执行线程 2 的代码,等线程 2 的代码执行到了一定程度,线程 2 的时间片用完了,再切换回来,再继续执行线程 1 剩余部分的代码
我们考虑一下,如果在线程切换的过程中,下一条指令执行到哪里了,是不是还是会用到我们的程序计数器啊。
没个线程都有自己的程序计数器,因为它们各自执行的代码的指令地址是不一样的呀,所以每个线程都应该有自己的程序计数器
说一下方法区中方法的执行过程
当程序中通过对象或类直接调用某个方法时,主要包括以下几个步骤:
解析方法调用:JVM 会根据方法的符号引用找到实际的方法地址(如果之前没有解析过的话)
栈帧创建:在调用一个方法前,JVM 会在当前线程的 Java 虚拟机栈中为该方法分配一个新的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
执行方法:执行方法内的字节码指令,涉及的操作可能包括局部变量的读写、操作数栈的操作、跳转控制、对象创建、方法调用等。
返回处理:方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境
JVM内存中的栈和堆有什么区别?
用途
栈主要用于存储局部变量、方法调用的参数、方法返回地址以及一些临时数据。每当一个方法被调用,一个栈帧(stack frame)就会在栈中创建,用于存储该方法的信息,当方法执行完毕,栈帧也会被移除
堆用于存储对象的实例(包括类的实例和数组)。当你使用 new 关键字创建一个对象时,对象的实例就会在堆上分配空间
生命周期
栈中的数据具有确定的生命周期,当一个方法调用结束时,其对应的栈帧就会被销毁,栈中存储的局部变量也会随之消失
堆中的对象生命周期不确定,对象会在垃圾回收机制(Garbage Collection, GC)检测到对象不再被引用时才被回收
存取速度
栈的存取速度通常比堆快,因为栈遵循先进后出(LIFO, Last In First Out)的原则,操作简单快速
堆的存取速度相对较慢,因为对象在堆上的分配和回收需要更多的时间,而且垃圾回收机制的运行也会影响性能
存储空间
栈的空间相对较小,且固定,由操作系统管理
当栈溢出时,通常是因为递归过深或局部变量过大
堆的空间较大,动态扩展,由 JVM 管理
堆溢出通常是由于创建了太多的大对象或未能及时回收不再使用的对象
可见性
栈中的数据对线程是私有的,每个线程有自己的栈空间。堆中的数据对线程是共享的,所有线程都可以访问堆上的对象
static修饰的类型
未被static
修饰的基本类型:基本类型的变量(局部变量)存放在栈中,而不是堆中。例如int num = 10;
,变量num
是在方法执行时在栈中开辟空间存储的。基本类型的成员变量存放在堆中(当所属对象在堆中时) 。
被static
修饰的基本类型:被static
修饰的基本类型(静态变量)存放在方法区(在 JDK 8 及之后,方法区的实现是元空间),而不是栈中。静态变量属于类,在类加载时就会分配空间并初始化,存储在方法区供类的所有对象共享。
包装类型:包装类型属于对象类型,其对象实例确实几乎都存在堆中,但包装类型的对象在某些场景下会有缓存机制。例如Integer
在-128
到127
之间的值会被缓存,当创建这个范围内的Integer
对象时,不会在堆中重新创建,而是直接引用缓存中的对象
此外,部分包装类(如Integer
、Short
、Byte
、Character
、Long
)存在对象缓存机制。以Integer
为例,在创建-128
到127
之间的Integer
对象时,不会在堆中重新创建,而是直接引用方法区中缓存的对象;超出这个范围才会在堆中创建新对象
线程的内部有什么?
程序计数器
本机方法栈
Java虚拟机栈
说一下JVM栈的内部组成
JVM栈的内部组成
Java虚拟机栈是线程私有的
它的生命周期和线程相同
除了Native方法是调用本地方法栈实现,其他的所有方法的调用都是通过Java虚拟机栈来实现的
Java虚拟机栈的内部是由栈帧组成
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出
Java虚拟机栈的内部是由一个又一个的栈帧组成
栈帧内部:局部变量表、操作数栈、动态链接、方法返回地址
说一下栈帧的内部
局部变量表
存放了数据类型,对象引用
操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果
另外,计算过程中产生的临时变量也会放在操作数栈中
动态链接
场景:主要服务一个方法需要调用其他方法的场景
Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。
当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用
方法返回地址
就是我们方法结束后的返回地址
说一下栈帧内部有啥?
局部变量表
操作数栈
动态链接
方法返回地址
说一下JVM栈会出现的问题
tackOverFlowError错误:栈帧过多爆了
函数循环调用过多
我们这个线程递归调用的时候,我们会往栈里面压入栈帧,如果压入的栈帧过多,就会爆出
Java方法的两种返回方式
一:Returen正常返回
二:抛出异常
OutOfMemoryError:内存空间不够爆了
虚拟机动态扩展栈时,无法申请到足够的内存空间
什么是本地方法栈
为本地方法服务
(也就是和我们的操作系统有关,我们的操作系统的方法)
说一下JVM的内存区域
JVM 内存区域最粗略的划分可以分为 堆 和 栈 ,
当然,按照虚拟机规范,可以划分为以下⼏个区域:
JVM 内存分为线程私有区和线程共享区,
线程共享区:方法区和堆
线程隔离的数据区: 虚拟机栈 、本地方法栈 和 程序计数器
1)程序计数器
程序计数器(Program Counter Register)也被称为 PC 寄存器,是⼀块较⼩的内存空间。
它可以看作是当前线程所执⾏的字节码的⾏号指示器。
2)Java 虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的⽣命周期与线程相同。
Java 虚拟机栈描述的是 Java ⽅法执⾏的线程内存模型:⽅法执⾏时,JVM 会同步创建⼀个栈帧,⽤来存储局部变量表、操作数栈、动态连接等。
3)本地方法栈
本地⽅法栈(Native Method Stacks)与虚拟机栈所发挥的作⽤是⾮常相似的,其区别只是虚拟机栈为虚拟机执⾏
Java ⽅法(也就是字节码)服务,⽽本地⽅法栈则是为虚拟机使⽤到的本地(Native)⽅法服务。
Java 虚拟机规范允许本地⽅法栈被实现成固定⼤⼩的或者是根据计算动态扩展和收缩的。
4)Java 堆
对于 Java 应⽤程序来说,Java 堆(Java Heap)是虚拟机所管理的内存中最⼤的⼀块。Java 堆是被所有线程共享
的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,Java ⾥“几乎”所有的对象实例
都在这⾥分配内存。Java 堆是垃圾收集器管理的内存区域,因此⼀些资料中它也被称作“GC 堆”(Garbage Collected Heap,)。从回
收内存的⻆度看,由于现代垃圾收集器⼤部分都是基于分代收集理论设计的,所以 Java 堆中经常会出现 新⽣代 、⽼年代 、 Eden空间 、 From Survivor空间 、 To Survivor空间 等名词,需要注意的是这种划分只是根据垃圾回收机制来进⾏的划分,不是 Java 虚拟机规范本身制定的。
5)方法区
⽅法区是⽐较特别的⼀块区域,和堆类似,它也是各个线程共享的内存区域,⽤于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
它特别在 Java 虚拟机规范对它的约束⾮常宽松,所以⽅法区的具体实现历经了许多变迁,例如 jdk1.7 之前使⽤永久代作为⽅法区的实现
JVM的堆是用来干嘛的
Java 虚拟机所管理的内存中最大的一块
Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存
什么是逃逸分析
jdk1.7之后,已经默认开启逃逸分析
也就是某些方法中的对象引用没有被返回或者未被外面使用,那么就可以直接在栈上分配内存
也就是我们不用在堆给这个对象分配内存,我们在栈上给这个对象分内存就可以了