JVM常见面试题
jvm的内存模型以及分区情况和作用
堆、栈、方法区、程序计数器。
其中堆区和方法区是线程共有区域,其他三个区域是线程私有区域。
方法区
用于存储虚拟机加载的类信息,常量,静态变量等数据,
堆
存放对象实例,所有的对象和数组都要在堆上分配。是JVM 所管理的内存中最大的一块区域。
虚拟机栈(java方法栈)
Java方法执行的内存模型: 存储局部变量表,操作数栈,动态链接,方法返回地址等信息生命周期与线程相同。
虚拟机栈存的是java方法调用过程的栈帧。
本地方法栈
native修饰的方法在这块区域执行。
本地方法栈存的是本地方法调用过程中的栈帧。
程序计数器
程序计数器用来选取下一条需要执行的字节码指令。
jvm对象创建步骤流程是什么?
JVM创建对象的步骤主要包括类加载检查、分配内存、初始化零值、设置对象头、执行init()方法。
类加载检查:在实例化一个对象时,JVM首先会检查目标对象是否已经被加载并初始化。如果尚未加载,JVM会加载目标类并进行初始化,包括对静态变量、成员变量、静态代码块的初始化。
分配内存:类加载后,JVM根据对象的大小在堆内存中为其分配内存空间。内存分配的方式有两种:指针碰撞和空闲列表。指针碰撞适用于堆内存规整的情况,而空闲列表适用于堆内存不规整的情况。此外,为了提高并发性能,JVM还提供了TLAB(Thread Local Allocation Buffer)机制,每个线程在Java堆中预先分配一小块内存,以减少同步开销。
初始化零值:分配内存后,JVM将对象的成员变量初始化为零值,如int类型初始化为0,对象类型初始化为null。这一步确保了对象实例的字段在Java代码中可以不赋初始值就可以直接使用。
设置对象头:包括对象的类元数据信息、哈希码、GC分代年龄等。
执行init()方法:这是由程序员提供的初始化方法,用于对对象进行必要的设置。
这一系列步骤共同完成了在JVM中对象的创建过程,确保了对象的正确初始化和使用。
如何找到JVM中有哪些垃圾对象
第一种:引用计数法
第二种:可达性分析法
引用计数法:每个对象都保存一个引用计数器属性(在对象头中),用于记录对象被引用的次数。
优点:实现简单,计数器为0则表示是垃圾对象。
缺点:1.需要额外的空间来存储引用计数。2.以及需要额外的时间来维护引用计数。3.还有一个严重问题,就是无法处理循环引用的问题。
这种方式一般不使用。
可达性分析:可达性分析会以GC Roots作为起始点,然后一层一层找到锁引用的对象,被找到的对象就是存活对象,那么那些不可达的对象就是垃圾对象。
垃圾回收算法有几种类型?他们对应的优缺点又是什么?
下面主要讲的是,我们找到垃圾对象,如何进行回收,涉及到的几种算法。
标记-清除算法、复制算法、标记-整理算法、分代收集算法
标记-清除算法
标记一清除算法包括两个阶段:“标记”和“清除”
标记阶段:确定所有要回收的对象,并做标记
清除阶段:将标记阶段确定不可用的对象清除。
缺点:
1.标记和清除的效率都不高。
2.会产生大量的碎片,而导致频繁的回收。
复制算法
内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候把存活的对象复制到另一块上,然后把这块内存整个清理掉。后续再按照同样的流程进行垃圾回收,交换着来。
复制算法一般用在新生代的垃圾回收。
缺点:
1.需要浪费额外的内存作为复制区
2.当存活率较高时,复制算法效率会下降。
标记-整理算法
标记-整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。
缺点:
算法复杂度大,执行步骤较多
分代收集算法
目前大部分 JVM 的垃圾收集器采用的算法。根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为新生代(YoungGeneration和老年代(TenuredGeneration),永久代(PermanetGeneration)。
老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最合适的垃圾收集算法。
年轻代:存放新创建的对象,对象生命周期非常短,几乎用完可以立即回收,也叫 Eden区。
老年代: young区多次回收后存活下来的对象将被移到 tenured 区,也叫 old 区。
Perm:永久带,主要存加载的类信息,生命周期长,几乎不会被回收。
缺点:
算法复杂度大,执行步骤较多。
老年代的垃圾回收比较适合实用标记-清除或者是标记-整理算法。
类加载的过程是什么?简单描述一下每个过程都干了什么?
加载、验证、准备、解析、初始化
jvm预定义的类加载器有几种?分别有什么作用?
启动(Bootstrap)类加载器、标准扩展(Extension)类加载器、应用程序类加载器(Application)
启动(Bootstrap)类加载器:主要用来加载JAVA_HOME/jre/lib/下的jar包。比如rt.jar。
标准扩展(Extension)类加载器:主要用来加载JAVA_HOME/lib/ext/包下的jar包。
应用程序类加载器(Application):主要用来加载用户自己写的类。
什么是双亲委派模式?有什么作用?
基本定义:
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器没有找到所需的类时子加载器才会尝试去加载该类。
双亲委派的作用:
通过带有优先级的层级关系可以避免类的重复加载。
保证java程序安全稳定运行,java核心API定义类型不会被随意替换。
介绍一下jvm中的垃圾收集器有哪些? 他们的特点分别是什么?
新生代垃圾收集器
Serial收集器
特点:
Serial收集器只能使用一条线程进行垃圾收集工作,并且在进行垃圾收集的时候,所有的工作线程都需要停止工作,等待垃圾收集线程完成以后,其他线程才可以继续工作。
使用算法:复制算法
ParNew 收集器
特点:
ParNew 垃圾收集器是Serial收集器的多线程版本。为了利用 CPU 多核多线程的优势ParNew 收集器可以运行多个收集线程来进行垃圾收集工作。这样可以提高垃圾收集过程的效率。
使用算法:复制算法
Parallel Scavenge 收集器
这也是jdk1.8新生代默认的垃圾收集器。
特点:
Parallel Scavenge 收集器是一款多线程的垃圾收集器,但是它又和 ParNew 有很大的不同点。
Parallel Scavenge 收集器和其他收集器的关注点不同。其他收集器,比如 ParNew和CMS这些收集器,它们主要关注的是如何缩短垃圾收集的时间。而 Parallel Scavenge 收集器关注的是如何控制系统运行的吞吐量。这里说的吞吐量,指的是 CPU用于运行应用程序的时间和 CPU 总时间的占比,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间)。如果虚拟机运行的总的 CPU 时间是 100分钟,而用于执行垃圾收集的时间为1分钟,那么吞吐量就是 99%。
使用算法:复制算法
老年代垃圾收集器
Serial old 收集器
特点:
Serial old 收集器是 Serial收集器的老年代版本。这款收集器主要用于客户端应用程序中作为老年代的垃圾收集器,也可以作为服务端应用程序的垃圾收集器。
使用算法:标记-整理
Parallel old 收集器
特点:
Parallel 0ld 收集器是 Parallel Scavenge 收集器的老年代版本这个收集器是在 JDK1.6 版本中出现的,所以在 JDK1.6之前,新生代的 Parallel Scavenge 只能和 Serial old 这款单线程的老年代收集器配合使用。Parallel 0ld垃圾收集器和 Parallel Scavenge 收集器一样,也是一款关注吞吐量的垃圾收集器,和 Parallel Scavenge 收集器一起配合,可以实现对Java 堆内存的吞吐量优先的垃圾收集策略。这也是jdk8老年代的默认垃圾收集器。
使用算法:标记-整理
CMS 收集器
特点:
CMS 收集器是目前老年代收集器中比较优秀的垃圾收集器。CMS是 Concurrent MarkSweep,从名字可以看出,这是一款使用”标记-清除”算法的并发收集器。
CMS 垃圾收集器是一款以获取最短停顿时间为目标的收集器。如下图所示
使用算法:复制+标记清除
其他
G1垃圾收集器
G1只有逻辑上的分代概念,使用复制算法将存活的对象复制到另一个空闲的区域。
每一个方块叫做region,堆内存会分为2048个region,每个region的大小等于堆内存除以2048还是分了Eden区、S0区、S1区、老年代,只不过空间可以是不连续的了Humongous区是专门用来存放大对象的(如果一个对象大小超过了一个region的50%,那么就是大对象)。
特点:
主要步骤:初始标记,并发标记,重新标记,复制清除。
使用算法:复制+标记整理
各个JDK版本默认使用的垃圾收集器
对象“对象已死”是什么概念?
对象不可能再被任何途径使用,称为对象已死。
判断对象已死的方法有:引用计数法与可达性分析算法。
JVM 数据运行区,哪些会造成 OOM 的情况?
除了数据运行区,其他区域均有可能造成 O0M 的情况:
堆溢出:java.lang.0utOfMemoryError:Java heap space
栈溢出:java.lang.StackOverflowError
永久代溢出:java.lang.0utOfMemoryError:PermGen space
详细介绍一下对象在分带内存区域的分配过程?
- JVM 会试图为相关 Java 对象在 Eden 中初始化一块内存区域。
- 当 Eden 空间足够时,内存申请结束;否则到下一步。
- JVM 试图释放在 Eden 中所有不活跃的对象(这属于1或更高级的垃圾回收)。释放后若 Eden 空间仍然不足以放入新对象,则试图将部分 Eden 中活跃对象放入Survivor 区。如果Eden区还是放不下新来的对象的话,那么这个新来的对象会被直接放到老年代中。
(注意:S0区和S1区只有一个区域是有对象存在的,另一个区域用来做倒腾用的。)
每次youngGc都会使得Survivor区中的对象进行交换,对象每交换一次,年龄就会长一岁,当长到15岁的时候,就会被晋升到老年代。 - Survivor 区被用来作为 Eden 及 0ld 的中间交换区域,当 0ld 区空间足够时Survivor 区的对象会被移到 0ld 区,否则会被保留在 Survivor 区.
- 当 Old 区空间不够时,JVM 会在 Old 区进行完全的垃圾收集。
- 完全垃圾收集后,若 Survivor 及 0ld 区仍然无法存放从 Eden 复制过来的部分对象,导致 JVM 无法在 Eden 区为新对象创建内存区域,则出现“out of memory”错误。
当然,我们可以通过设置jvm参数:-XX:PretenureSizeThreshold来控制大对象直接进入老年代。
https://www.bilibili.com/video/BV1gV4y1n7hK/?spm_id_from=333.337.search-card.all.click&vd_source=273847a809b909b44923e3af1a7ef0b1
G1与 CMS 两个垃圾收集器的对比
细节方面不同
1.G1 在压缩空间方面有优势,
2.G1 通过将内存空间分成区域(Region)的方式避免内存碎片问题。
3.Eden,survivor, Old 区不再固定、在内存使用效率上来说更灵活,
4.G1 可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。
5.G1在回收内存后会马上同时做合并空闲内存的工作、而 CMS默认是在 STW(stopthe world)的时候做。
6.G1 会在 Young Gc 中使用、而 CMS 只能在old区使用。
线上常用的 JM 参数有哪些?
数据区设置
Xms:初始堆大小
Xmx:最大堆大小
Xss:Java每个线程的Stack大小
XX:NewSize=n:设置年轻代大小
XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1/4。
XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor区有两个。如:3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的1/5。
XX:MaxPermSize=n:设置持久代大小。
收集器设置
XX:+UseSerialGc:设置串行收集器·
XX:+UseParallelGc::设置并行收集器。
XX:+UseParalledloldGc:设置并行年老代收集器
XX:+UseConcMarkSweepGc:设置并发收集器
GC日志打印设置
XX:+PrintGc:打印 GC的简要信息XX:+PrintGCDetails:打印GC详细信息
XX:+PrintGCTimeStamps:输出GC的时间戳
对象什么时候进入老年代?
对象优先在 Eden 区分配内存
当对象首次创建时,会放在新生代的 eden 区,若没有 Gc的介入,会一直在 eden区,GC后,是可能进入survivor 区或者年老代
大对象直接进入老年代
所谓的大对象是指需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配就是坏消息,尤其是一些朝生夕灭的短命大对象,写程序时应避免。
长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器,对象在 Survivor 区中每熬过一次Minor Gc,年龄就增加 1,当他的年龄增加到一定程度(默认是 15岁),就将会被晋升到老年代中。
什么是内存溢出,内存泄露?他们的区别是什么?
内存溢出 out ofmemory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
内存泄露 memoryleak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出.
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
Full GC、MajorGc、MinorGc之间区别?
Minor GC:
从新生代空间(包括 Eden和 Survivor 区域)回收内存被称为 Minor Gc。
Major GC:
清理 Tenured 区,用于回收老年代,出现 Major Gc通常会出现至少一次 Minor Gc.
Full GC:
Full GC 是针对整个新生代、老年代、元空间(metaspace,java8 以上版本取代 permgen)的全局范围的 GC。
什么时候触发 Full GC?
1.调用 System.gc时,系统建议执行 FullGC,但是不必然执行,
2.老年代空间不足。
3.方法区空间不足。(TODO具体下来看看吧)
4.通过 Minor Gc后进入老年代的平均大小大于老年代的可用内存。
5.由Eden 区、survivorspace1(FromSpace)区向survivorspace2(To Space)区复制时,对象大小大于Two Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
什么情况下会出现栈溢出
1.方法创建了一个很大的对象,如 List,Array。
2.是否产生了循环调用、死循环。
3.是否引用了较大的全局变量。
说一下强引用、软引用、弱引用、虚引用以及他们之间和 gc的关系
1.强引用:new 出的对象之类的引用,只要强引用还在,永远不会回收,
2.软引用:引用但非必须的对象,内存溢出异常之前,回收。
3.弱引用:非必须的对象,对象能生存到下一次垃圾收集发生之前。
4.虚引用:对生存时间无影响,在垃圾回收时得到通知。
Eden 和 Survivor的比例分配是什么情况?为什么?
默认比例 8:1。
大部分对象都是朝生夕死。
复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
CPU 资源占用过高
1.top 査看当前 CPU 情况,找到占用 CPU 过高的进程 PID=123。
2.top -H -p 123 找出两个 CPU 占用较高的线程,记录下来 PID=2345,3456转换为十六进制。(强调一下:jstack打印出来的线程的pid是用十六进制表示的,我们可以使用命令将这个十进制的pid转换成16进制的pid,可以使用命令:printf "%x" 2345 比方说得到的结果是1ea6) .
tip:jstack是java虚拟机自带的堆栈跟踪工具,用于生成java虚拟机当前时刻的线程快照。
3.jstack 123 > temp.txt打印出当前进程的线程栈。
4.然后我们查看temp.txt文件,查找到对应于第二步的两个线程运行栈(搜索1ea6),里面会有代码的错误信息,如果有死锁相关的文件也是会在文件的最下面进行展示的。
5.最后分析代码(看看有没有出现死锁等问题)。
补充:jstack用于生成java虚拟机当前时刻的进程的线程快照。
什么情况下会产生OOM?
1.一次性申请的对象太多了。比方说select * from 表没有加where条件。
2.内存资源耗尽没有得到释放。比方说我们频繁的创建线程不释放,进行IO操作的时候不关流,connection连接对象不关闭等等。
3.老板给的硬件资源不给力。
OOM 异常排查
使用命令: jmap -heap 进程号(你部署的应用程序)// 我们就能够看出当前进程的占用的内存情况,比方说eden区、surviror区老年代的内存占用情况。
出现oom不一定就表示我的系统会挂掉,原因是:因为有些请求是不需要去堆堆内存中申请空间的,但是会有直接挂掉的风险。
如果你的系统已经因为oom挂掉了。解决办法:
提前设置-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=
也就是说你提前把这个jvm参数设置了,系统挂掉了之后,直接查看这个堆的dump文件就可以了。这个文件的后缀是.hprof,然后我们可以使用java visualvm工具将这个文件导入进来,进行分析了。
这个时候我们就能够看到每个对象的数量以及每个对象占用的内存大小了。查看最多跟业务有关对象->找到GCRoot ->查看GCROOT的线程引用
如果你的系统因为oom还没有挂掉。解决办法:
我们可以导出dump文件件: jmap -dump:format=b,file=xushu.hprof 14660,
缺点是会进行一次fullgc,还有会造成一次STW.
最后我们将xushu.hprof文件导入到java visualvm工具中进行故障的排查(同上)。
当然也可以使用arthas工具进行故障排查。
- 使用 top 指令查询服务器系统状态。
- ps -aux|grep java 找出当前 Java 进程的 PID。
- jstat -gcutil pid interval査看当前 GC的状态。
- jmap -histo:live java进程的pid 可用统计存活对象的分布情况,从高到低查看占据内存最多的对象。
- jmap -dump:format=b,file=文件名[pid]利用 Jmap dump。
- 使用性能分析工具对上一步 dump 出来的文件进行分析,工具有 MAT 等。
关于jvm内存溢出排查的具体步骤,面试官有问到,而且问的特别细,要讲出来你在页面上具体看到了什么?
怎样进行jvm调优,线上JVM参数设置(面试有问到)
jvm一般是不需要进行调优的,一般是出现了问题之后才进行调优的,或者说系统没有出现问题,但是响应慢,我们也是需要进行调优的。
如果频繁的出现minorGC这个时候其实可以不用调优,如果想调优的话,我们可以适当的增加年轻代的空间,微调Eden区和两个s区的占比。
jvm的调优主要是减少频繁的STW,因为fullGC会造成我们程序的STW.
fullgc多久一次算正常?
其实fullgc的频率取决于多个因素,比如项目的QPS、服务器的集群配置、系统的jvm参数设置、垃圾收集器的选择等因素。正常情况下,是绝对不允许出现频繁的fullGC的。
这个值没有绝对的定义,但有一个大概的范围。根据项目的经验值:
平时业务稳定的情况下,fullgc次数每周应该不超过1-3次。
秒杀大促场景下,fullgc次数一般1-3小时一次。
fullgc的耗时一般在200-500毫秒,最多不超过一分钟。
这个时候我们就要进行问题的排查了,看看我们的程序是否写的有问题,比方说SQL语句没有加where条件、io操作之后没有进行流的关闭、代码中有没有显示的调用system.gc方法进行fullgc,或者是一些其他的程序bug导致的。当然,我们也可以使用jmap命令来监控当前进程的内存占用情况,也可以使用就是jstack命令导出一份堆的dump文件结合java的visualvm工具进行分析。
线上环境,比方说是4C8G的机子,我们一般给堆空间分4G的内存。在这个基础上会给新生代和老年代的空间比例为1:2
Eden区的两个s区的比例是8:1:1.
然后元空间占用512兆,(元空间是jdk1.8以及之后对方法区的一种新的实现)。
-Xss:每个线程占用的栈大小,比方说我们的一个项目中有300个县城就是300兆。
总的来说一共占了占了差不多5个G,剩下来的3G给操作系统用。
说一下CMS垃圾回收器的执行过程(面试有被问到)
CMS是一种低停顿的垃圾回收器,它主要通过初始标记阶段和并发标记阶段,这两个并发阶段来完成垃圾的回收。它的整体流程可以分为四个部分。
第一:初始标记,这个阶段需要stop the world,来标记哪些对象是需要回收的。这个过程只需要去标记GCRoot能够直接关联的对象,多以速度很快。对性能的影响比较小。
第二:并发标记,它会扫描整个堆中的对象,标记所有不需要回收的对象,这个阶段不需要stop the world,在应用程序运行的过程中进行标记。
第三:重新标记阶段,他是为了修正并发标记期间,应用程序同步运行,导致标记产生变动的那一部分对象,这个阶段需要stop the world.
最后一个阶段是并发清除,cms会并发执行清除操作,同时应用程序继续运行。最大力度的减少对性能的影响。
你们jvm参数一般在哪里设置呀(面试有被问到)
https://blog.csdn.net/golove666/article/details/141928896
到时候就回答,我们jvm参数是设置在应用启动脚本中。
对于java中的内存管理你有什么实践经验
1.合理控制对象创建频率,比方说使用stringbuffer
2.明确对象的生命周期,我们一定要分清楚哪些对象是长期存活的,哪些是用完就需要立马进行回收的,也就是说合理使用单例模式。
3.选择合适的垃圾收集器。
4.尽量避免内存溢出和内存泄漏的风险。
JVM中的方法区主要存放以下内容(面试有被问到):
- 类信息:包括类的名称、修饰符、父类信息、接口信息等。类元数据还包括类的常量池,存储类中的常量值(如字符串常量、数值常量和面值),以及字面量表示的类、方法和字段的引用1。
- 字段信息:包括类中定义的字段的名称、数据类型和修饰符信息(如访问权限)。
- 方法信息:包含方法的名称、返回类型、参数信息、修饰符等。方法的字节码是将Java源代码编译成可供JVM执行的字节码。
- 静态变量:属于类的字段信息,这些变量在类加载时就会被存储在内存中,而不是在实例化对象时。
- 运行时常量池:在类加载时,将类常量池的内容复制到运行时常量池,用于支持对字面量和符号引用的动态解析。
- 元空间(Java 8及以后):在Java 8及以后的版本中,方法区的功能被移入了元空间(Metaspace)。元空间是基于本地内存的,而不是在堆内存中,这意味着可以使用系统的本地内存来存储类的元数据12。