1. 什么情况下会发生栈内存溢出
栈是线程私有的,他的生命周期和线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
栈内存溢出是指线程请求的栈深度大于虚拟机所允许的最大深度,则会抛出StackOverflow异常。
另一个可能是栈中引用了大的变量,导致OOM异常。
2. JVM内存结构,Eden和Survivor的比例
内存结构
- 程序计数器: 当前线程执行的字节码的行号指示器,是线程私有的,也是唯一一个不会发生OOM异常的区域。
- Java虚拟机栈:也是线程私有的,保存方法调用的动态链接、操作数、局部变量等信息,方法调用就是虚拟机栈出栈入栈的过程。
- 本地方法栈:线程私有的,和虚拟机栈类似,只是保存的是对于native方法的调用。
- 堆:所有线程共享,Java虚拟机中管理的内存最大的一个区域,所有线程共享的区域,唯一目的是存放对象实例。也是垃圾回收的主要区域。
- 方法区:线程共享,存放虚拟机加载的类信息、常量、静态变量等数据。
- 运行时常量池:是方法区的一部分,存放编译器生成的各种字面量和符号引用。
- 直接内存:
并不是虚拟机运行时数据区域的一部分
,使用Native函数直接分配堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆中来回复制数据。
Eden和Survivor比例
JVM对堆进行分代,新生代分为三个部分,1个Eden区、2个Survivor区。
默认比例为8:1:1
。一般情况下,新创建的对象都会被分配到Eden区(一些大对象除外)。这些对象经过一次Minor GC后,如果仍然存在,将会被移到Survivor区。对象每次熬过一次Minor GC,年龄就会增加1岁。当年龄增加到一定程度时,将会被移动到老年代.
3. JVM为什么要分为新生代、老年代和持久代。新生代中为什么要分为Eden区域和Survivor区。
- 堆内存是虚拟机管理的内存中最大的一块,堆内存区域划分,主要是为了提高对象内存分配和回收效率。
- 新创建的对象在新生代中分配,经过多次回收仍然存活的对象放入老年代。静态变量、类信息存放在永久代中。大部分对象都是朝生夕死的,只需要在新生代中频繁执行Minor GC,老年代中对象生命周期长,内存回收的效率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行回收。
- 另外,分代以后可以根据不同的区域执行不同的垃圾回收算法。例如,新生代通常采用
标记-复制算法
, 老年代通常采用标记-清除-整理
算法。
新生代分为Eden区、From Survivor区、To Survivor区,默认比例为8:1:1。划分的目的是因为JVM采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配,当Eden区域没有足够内存时触发Minor GC,对象进入Survivor区域。
4. JVM中一次完整的GC流程是什么样,对象如何晋升到老年代,介绍几种主要的JVM参数。
完整GC流程
- GC开始时,对象只存在于Eden区和From Survivor区域,To Survivor是空白的保留区域。GC进行时,Eden中所有存活的对象会被复制到To Survivor区域,而在From Survivor区域的对象,根据年龄决定去向,如果年龄+1大于15,则晋升到老年代,否则,进入To Survivor区域。接着,清空Eden区域和From Survivor区域。接着,From Survivor和To Survivor区域交换角色,即,To Survivor变成From Survivor。
对象晋升到老年代的几种可能:
- 当对象达到成年,经过15次GC,对象晋升到老年代。
- 大的对象直接在老年代创建。
- 新生代的Survivor空间内存不足时,对象可能直接晋升到老年代。
主要的JVM参数
默认情况下,JVM初始分配的堆内存大小是物理内存的1/64,最大分配的堆内存大小是物理内存的1/4.
- -Xms:初始堆大小
- -Xmx:堆最大内存
- -Xss:栈内存
- -XX:PermSize 初始化永久代内存
- -XX:MaxPermSize 最大永久代内存
- -XX:NewSize 设置年轻代初始值
- -XX:MaxNewSize 设置年轻代最大值
5. 介绍几种垃圾收集器,各自的优缺点,重点介绍cms和G1收集器,包括原理,流程,优缺点。
- 串行收集器(Serial 收集器)
- 工作原理:串行收集器是工作在新生代的单线程垃圾收集器。只会使用一个CPU或者一个收集线程来完成垃圾回收工作,在进行垃圾收集时,会暂停所有用户线程,直到垃圾收集结束。
- 优点:简单高效,开销低
- 缺点:进行垃圾收集时需要暂停所有应用线程,对于需要高响应性的应用来说是不可接受的。
- ParNew收集器
- 工作原理:ParNew收集器是串行收集器的多线程版本;除了使用多线程外,其收集算法、STW、对象分配规则、回收策略和Serial收集器完全一样。
- 优点:提高吞吐量,减少暂停时间。能和CMS收集器配合工作,在Server模式下,是许多新生代收集器的首选。
- 缺点:单CPU环境中可能不如串行收集器,随着CPU数量增加,线程交互的开销可能成为性能瓶颈。
- Parallel Scavenge收集器
- 工作原理:Parallel Scavenge收集器也是一个使用复制算法,多线程,工作于新生代的垃圾收集器。他关注的是吞吐量,而CMS关注的是停顿时间。
- 优点:提高吞吐量,减少暂停时间。
- 缺点:不适用于对停顿时间要求极高的应用。
- CMS收集器
- 工作原理:CMS收集器是基于“标记-清除”算法实现的并发收集器。
- 工作流程:
- 初始标记:标记出和GC Roots直接相连的对象,需要暂停所有线程。
- 并发标记:和用户线程并发执行,标记出所有可达对象。
- 重新标记:修正并发标记期间因用户线程操作导致标记变动的情况,需要暂停所有用户线程。
- 并发清除:与用户线程并发执行,清除未被标记的对象。
- 优点:与用户线程并发执行,减少停顿时间。
- 缺点:基于“标记-清除”算法,收集结束时可能会产生大量空间碎片;对CPU资源敏感;无法处理浮动垃圾。
- G1收集器
- 工作原理:G1收集器是一种面向服务器的垃圾收集器,基于“标记-整理”和“复制”算法实现。他将堆空间划分为多个大小相等的独立区域,并优先收集垃圾最多的区域。
- 优点:适用于高吞吐量的应用程序,在多个处理器之间并行的进行操作,提高处理效率。
- 缺点:启动时间长,需要对整个堆空间进行分区,启动时间较长。需要额外的内存来存储标记信息和回收状态信息。
6. 垃圾回收算法的实现原理
- 标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 标记-整理算法:标记过程和上述标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理的,而是让所有存活的对象都向一端移动,然后直接清理掉边界外的内存。
- 复制算法:将可用内存按容量分为两块,每次只使用其中一块,当这一块内存用完后,将还活着的对象复制到另一块上面。然后再把已使用过的空间一次清理掉。
7. 当出现内存溢出,怎么排查错误
- 首先控制台查看错误日志。
- 使用jmap查看堆转储快照。
- 定位出内存溢出的空间:堆、栈、还是永久代。
- 如果是堆内存溢出,看是否创建了超大的对象。
- 如果是栈内存溢出,看是否创建了超大的对象或者产生了方法调用的死循环。
8. JVM内存模型相关,重排序、内存屏障、happen-before、主内存、工作内存等。
重排序:
- 重排序指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
内存屏障
- 内存屏障可以确保某些操作按照预定的顺序执行,防止编译器和处理器对指令进行重排序;内存屏障可以确保写入一个共享变量的操作在后续的读操作前对其他线程可见。
Happen-Before原则
- 指一个操作的结果对另一个操作是可见的,即一个操作的结果可以被后续操作获取或感知到。这个原则保证了跨线程的内存可见性。
主内存
- 所有线程共享的内存空间。
工作内存
- 工作内存指每个线程特有的内存空间,工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写在主内存中的变量。
9. Java的反射机制
他允许程序在运行时查询和使用任何类的信息,包括类的成员变量、方法、构造函数等。这种机制主要通过java.lang.reflect包中的类和接口实现。
10. G1和CMS区别,吞吐量优先和响应优先的垃圾收集器选择
- CMS是基于“标记-清除”实现的,主要步骤是初始标记、并行标记、重新标记、并发清除。
- G1是基于“标记-整理”实现的,主要步骤是初始标记、并发标记、最终标记和筛选回收。
CMS的缺点是对CPU要求比较高。
G1的缺点是将内存划分成很多块,所以对内存段的大小有很大的要求。
CMS是清除,所以有很多内存碎片。
G1是整理,所以碎片空间很小。
CMS和G1都是响应优先,他们的目的都是尽量控制STW时间。
G1和CMS的Full Gc都是单线程的mark sweep compact算法,直到JDK10才优化成并行的。
CMS目前只用于老年代,G1将整个Java堆划分为多个大小不等的独立区域,虽然还保留有老年代和新生代的概念,但是不在物理隔开,他们都是Region的集合。
吞吐量优先选择Parallel Scavenge收集器。
11. 解释如下JVM参数含义
- -server 服务器模式
- -Xms512m:初始堆内存大小
- -Xmx512m: 最大堆内存大小
- -Xss1024k: 栈大小
- -XX:PermSize=256m:初始永久代大小
- -XX:MaxPermSize=512m:最大永久代大小
- -XX:MaxTenuringThreshold=20:新生对象存活次数为20时,晋升到老年代
- -XX:CMSInitiatingOccupancyFraction=80 :CMS在对老年代内存占用率达到80%时,开始GC
- -XX:+UseCMSInitiatingOccupancyOnly : 只使用设定的阈值(如上80%),如果不指定,JVM仅在第一次GC时使用设定值,后续则自动调整。
12. 运行时数据中哪些区域是线程共享的,哪些是独享的
JVM内存区域中,程序计数器、虚拟机栈、本地方法栈是线程独享的。
堆、方法区是线程共享的,但是,值得注意的是,Java堆其实还为每一个线程单独分配一个TLAB(本地线程分配缓冲)。
创建对象时,内存分配过程是如何保证线程安全性?有两种解决方案:
- 对分配内存空间的动作进行同步处理,例如CAS机制,配合失败重试的方式更新操作的线程安全性。
- 每个线程在Java堆中预先分配一小块内存即线程本地分配缓冲(TLAB),然后再给对象分配内存时,在自己的分配缓冲上进行分配,当这部分空间用完后,在分配新的“私有”内存。
13. 什么是TLAB
TLAB(线程本地分配缓冲区)是Java虚拟机中的一种内存分配优化技术,特别是在使用垃圾收集器时非常常见,TLAB的目的是减少多线程环境下对象分配时的线程同步开销,提高内存分配效率。
工作原理:
- 在多线程环境下,多个线程可能会同时尝试在堆上分配内存,如果不采取任何措施,这些线程可能会因为竞争同一个内存分配点而陷入等待。这不仅会降低程序性能,还可能引发线程饥饿。为了解决这个问题,
JVM引入了TLAB,每个线程在分配内存时,都会尝试在TLAB中分配,TLAB是一块从堆中预先分配给线程的私有内存区域。如果TLAB中有足够的空间,线程就可以直接在其中分配内存,而无需进行任何同步操作,只有当TLAB耗尽时,线程才会尝试从堆中分配更多的内存来填充TLAB,这时可能需要进行同步操作。
14. Java中的数组是存储在堆上还是栈上
在Java中,数组同样是一个对象,所以对象在内存中如何存放同样适用于数组,所以,
数组的实例是保存在堆中,而数组的引用是保存在栈上的。
15. Java对象创建的过程是怎样的
- 当虚拟机遇到new指令,到常量池定位到这个类的符号引用。
- 检查符号引用代表的类是否被加载、解析、初始化过,如果没有的话,执行类加载过程。
- 虚拟机为对象分配内存,根据Java内存是否规整,分别通过“指针碰撞”和“空闲列表”来分配。
- 虚拟机将分配到的内存空间都初始化为零值。
- 虚拟机对对象进行必要的设置。
- 执行方法、成员变量进行初始化。
16. 类加载过程
Java 虚拟机类加载过程分为加载、验证、准备、解析和初始化5个阶段。类加载过程
17. 在Java中,可以作为GC Roots的对象有什么
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象。
18. JDK1.8 虚拟机内存模型变化
在JDK1.8中最大的变化是取消了永久区Perm,而是用元数据空间MetaSpace来进行替换,元空间占用的内存不是虚拟机内部的,而是本地内存空间,使用本地内存空间,类的元数据等不再受到永久代大小限制,而是和系统可用内存空间一致。
19. 频繁GC的原因
- 频繁调用System.gc()
- 设置的堆大小比较小,可以提高堆的空间。
- 构建对象非常频繁,并且有很多大对象。