3.JVM垃圾回收相关(标记、回收算法、回收器)
哪些内存需要回收?(什么是垃圾)
垃圾是指在 运行程序中没有任何指针指向的对象。
为什么需要垃圾回收?
- 如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至导致内存溢出。
- 有了垃圾回收,不需要开发人员手动管理,更专心专注于业务开发
回收哪些区域?
- 回收堆空间和方法区
- 主要是堆空间
- 频繁回收Young区
- 较少收集Old区
- 基本不动元空间/永久带
什么时候回收?
- minor GC:Eden区满了
- Full GC:
- 标记阶段 : 分出内存中哪些是存活对象,哪些是死亡对象
- 引用计数算法 python采用
- 可达性分析算法(Hotspot采用)两次标记
- 清除阶段 堆中分代收集
- 标记-清除算法
- 复制算法
- 标记-压缩(整理)算法
堆内存空间划分,如何回收这些内存
新生代(eden、survivor12) 老年代 元空间(很少回收)
采用了分代回收算法、增量收集算法、分区算法混合使用。—->具体就是三种 标记清除 复制 标记整理算法。
GC如何判断对象可以被回收(标记阶段的算法)
这里只问判断可以被回收 那就是两种
- 引用计数法 新增一个就 + 1 (python采用这种形式,通过手动接触)
- 可达性分析法 从GCRoot开始向下搜索,走过的路就叫引用链,要是一个对象到GC Roots没有任何引用链,则判断可回收
- GC Root对象有哪些 有哪些特例
- 两次标记则死亡的 finalize()机制
- GC必须STW,因为要在一个快照中进行,保证结果准确性
哪些元素可以作为GC Roots
GC清除阶段有哪些算法?各自优缺点
- 标记-清除算法 (空间碎片)
- 复制算法 (两份空间)
- 标记-压缩算法 (压缩空间)
标记-清除算法
两个阶段:
- 标记阶段从跟节点出发,标记所有被引用的对象(可达对象),在对象的Header中记录。
- 清除阶段,对堆内存从头到尾进行线性遍历。发现Header没有记录为可达,就回收。
复制算法
复制算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。复制算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
具体使用常见:幸存者区的交换。
标记-压缩(整理)算法
执行过程:
- 先从跟节点标记,哪些对象存活
- 然后把所有存活对象压缩到内存的一段,按顺序排放.
- 之后清理边界外的所有内存空间
使用场景:老年代的回收
三种算法的对比
垃圾收集策略?JVM怎么回收内存(分代收集算法、增量收集算法、分区算法)
Jvm方法区中会发生垃圾回收吗?
会发生垃圾回收,回收垃圾类,只是回收条件比较苛刻。
- 该类的所有对象已经从堆内存中回收了
- 加载该类的ClassLoader已经被回收了
- 该类的Class对象没有任何引用
内存溢出/内存泄露的原因。
- 内存溢出
- OOM OutOfMemoryError:没有空间内存,而且垃圾回收器也无法提供更多内存。(OOM前,会Full GC一次)
- 出现原因:堆内存设置不够,创建了大量大对象,而且长时间不能被垃圾收集器回收(存在引用)。
- 内存泄漏
- Memory Leak 对象不会被程序用到了,但是GC又不能回收他们,叫内存泄漏
- 内存泄漏不会直接导致OOM,但是会逐步蚕食内存,直到耗尽所有内存。
垃圾回收器的并发与并行/安全点与安全区域
- 并行:多条垃圾收集线程并行工作。
- 并发:指用户线程与垃圾回收线程同时执行。(用户程序继续运行,而垃圾收集器运行在另一个CPU上。
程序执行只有在特定位置才能停顿下来开始GC,这些位置就是安全点。我们现在用主动式中断(主动挂起线程)停到安全点。
安全区域是指一段代码中,对象的引用关系不会发生改变,所以在这个区域任何位置开始GC都是安全的。
Java 强软弱虚引用
软 弱引用适合保存那些可有可无的缓存数据。
弱引用实例:ThreadLocal中的ThreadLocals这个Map的Key就是指向ThreadLocal的弱引用。
虚引用:对象被回收时收到一个系统通知,所以我们可以用虚引用跟踪对象回收时间。
深浅拷贝
- 浅拷贝 对于基本数据类型,会将属性值复制给新的对象;而对于引用数据类型,会复制引用给新的对象。
- 深拷贝 基本数据类型的属性和引用数据类型的属性都会复制。
- 构造方法中new新对象
- 实现Cloneable接口,重载clone方法
- 实现Serializable接口,先将对象序列化,再反序列化
怎么选择垃圾回收器?
JVM有哪些垃圾回收器,各自有什么特点
新生代回收器:Serial GC串行回收 / ParNew GC 并行回收 / Parallel Scavenge GC 并行回收 吞吐量优先
老年代回收器:CMS GC 并发回收 低延迟 / Serial Old GC 串行回收/Parallel Old GC 并行回收
G1回收器:区域化分代式 并发回收
Shenandoah GC 低停顿时间(红帽开发的,受到了Oracle的排挤)
ZGC:任意堆内存大小下都可以将垃圾收集停顿时间限制在10毫秒以内的低延迟。区域化分代式 垃圾优先
AliGC 基于G1,面向大堆应用场景的。
CMS回收器 低延迟
- JDK 5 Concurrent-Mark-Sweep 并发标记清除 —所以空闲列表
- 第一款真正意义上的并发收集器,第一次实现了垃圾收集线程和用户线程同时工作
- 尽可能缩短垃圾收集时用户线程的停顿时间(低延迟),适合重视服务相应速度的系统,良好的交互体验。
- 当堆内存达到一定阈值,我们就执行CMS,要是Concurrent Mode Failure, Serial Old来救场
过程:
- 初始标记 STW 仅仅只标记出GC Roots直接能关联的对象 很快嗷!
- 并发标记 从GC Roots直接关联对象开始遍历整个对象图的过程,过程耗时较长但是不需要停顿用户线程(并发过程)
重新标记 修正并发标记期间 因为用户线程继续运行导致标记产生变动的那一部分对象的标记记录 停顿时间稍长于初始标记
CMS会内置记录在并发标记期间那些被新建的对象或者有变动的对象,因此重新标记阶段不需要再重新标记所有对象,只对并发标记阶段改动过的对象做标记即可。
并发清除 清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。(因为不需要移动存活对象,所以可以跟用户线程并发)
- 重置线程
- 优点:并发收集、低延迟
- 缺点:
- 内存碎片
- 对CPU资源敏感,占用一部分线程导致吞吐量降低。
- 无法处理浮动垃圾
CMS参数
- -XX:+UseConcMarkSweepGC 手动指定使用CMS ( ParNew+CMS+Serial Old组合)
- -XX:+CMSInitiatingOccupanyFractioon 设置堆内存使用率的阈值(JDK5以前默认68,JDK6默认92,如果内存增长缓慢可以调大,反之小点好/越大越可能FullGC,越小越频繁CMS)
- -XX:CMSFullGCsBeforeCompaction 设置多少次Full GC后堆内存空间进行压缩整理
- -XX:ParallelCMSThreads 设置CMS的线程数量 (CPU + 3) / 4
关于重新标记阶段的一些问题
G1回收器 区域化分代式 垃圾优先
- 设定:在延迟可控的情况下获得尽可能高的吞吐量。(JDK 7)
- 是一款面向服务端应用的垃圾收集器,针对配备多核CPU及大容量内存的机器。JDK9默认
- 具体使用:把堆内存分割为多个不相关区域(Region),用不同Region表示E、S1、S0、old等。G1跟踪各个Region,在后台维护一个优先列表,根据允许的收集时间,优先回收价值最大的Region
- 优点:
- 并行与并发
- 分代收集
- 空间整合 Region之间是复制算法、整体上实际可以看做标记-压缩算法。
- 可预测的停顿时间模型
- 缺点:
- 小内存表现没有CMS好,平衡点在6-8GB之间
- 在用户程序运行过程中,G1为了垃圾收集而产生的内存占用/额外执行负载 都要比CMS高
G1回收器垃圾回收过程
- 年轻代GC Young GC
- 扫描根 根是什么?GC Roots + RSet 作为扫描存活对象的入口
- 更新RSet 处理脏卡表(保存对象引用信息的card),更新RSet
- 处理RSet 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的
- 复制对象 回收过程 存活去S区,S区+1,晋升等
- 处理引用 处理软弱引用
- 老年代并发标记过程 ConcurrentMarking
- 初始标记阶段 CMS第一阶段 只标记GCRoot直接可达
- 根区域扫描 在YGC前,标记可以直达老年代的对象,标记被引用的对象
- 并发标记
- 再次标记
- 独占清理
- 并发清除阶段
- 混合回收 Mixed GC
- 如果需要、单线程、独占式、高强度的Full GC还是继续存在,他针对GC的评估失败提供了一种失败保护机制
概念:Remembered Set 记忆集 解决分代引用问题(区域化收集 会临时加入GC Roots)
怎么知道引用来自不同Regiono? 写入对象的时候会产生一个写屏障Write Barrier暂时中断操作 (类似于AOP,写前)
ZGC
其他回收器
- Serial 串行回收 采用复制算法
- Serial Old 标记-压缩算法 用途:
- 与Parallel Scavenge配合使用
- 作为CMS收集器的后备方案
- ParNew Serial收集器的多线程版本 多CPU下更好,单CPU不如Serial
- Parallel Scavenge 吞吐量优先 自适应调节策略
-XX:+UseAdaptiveSizePolicy
复制算法 并行回收 JDK8默认 - Parallel Old 采用了标记-压缩算法
4.JVM性能监控与调优相关
关于调优篇 由于我是应届生,没有调优经验,所以目前就是背下八股文,参数之类。
推荐 https://www.jianshu.com/p/d9642d575d80
https://blog.csdn.net/jiuxin_jiuxin/article/details/108625827
简述JVM优化,调优思路
对JVM内存调优主要目的是减少GC频率和Full GC次数。
调优思路:我自己总结的调优方面的可能性,
- 针对JVM堆大小-Xms -Xmm 防止扩容影响效率,设置相同值
- 针对年轻代老年代比例 -NewRadio 年轻代大普通GC时间长,年轻代小GC频繁。
- 针对Survivor比例和晋升次数
- 针对不同使用场景用不同收集算法: 多核大内存 Parallel 并行高吞吐/ 单核小内存 Serial串行 / 响应速度优先?CMS/G1 吞吐优先Parallel
- 线程堆栈的设置:-Xss每个线程默认开启1M + 8*2k的栈大小,有的时候256k就够了
- 针对一些并行收集算法,对收集线程的数量设定
常见评估GC的性能指标
主要是三点:吞吐量、暂停时间、内存占用
现在的标准:在最大吞吐量优先的情况下,降低停顿时间
常用调优工具
jconsole:用于对 JVM 中的内存、线程和类等进行监控;
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
1)监控GC的状态,使用各种JVM工具,查看当前日志,并且分析当前堆内存快照和gc日志,根据实际的情况看是否需要优化。
2)通过JMX的MBean或者Java的jmap生成当前的Heap信息,并使用Visual VM或者Eclipse自带的Mat分析dump文件
3)如果参数设置合理,没有超时日志,GC频率GC耗时都不高则没有GC优化的必要,如果GC时间超过1秒或者频繁GC,则必须优化
4)调整GC类型和内存分配,使用1台和多台机器进行测试,进行性能的对比。再做修改,最后通过不断的试验和试错,分析并找到最合适的参数
常用的 JVM 调优的参数都有哪些?
我这里总结的不够多,其实很多参数都需要背一下。
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
OOM说一下?怎么排查?哪些会导致OOM? OOM出现在什么时候
OOM,全称“Out Of Memory”,官方说明:当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。
(没有空闲内存,并且垃圾收集器也无法提供更多内存。)
怎么排查? 首先可以查看服务器运行日志以及项目记录的日志,捕捉到内存溢出异常。
核心系统日志文件
java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。 可以通过虚拟机参数-Xms,-Xmx等修改。
(1)java永久代溢出,即方法区溢出了,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见 ,尤其是在运行时存在大量动态类型生成的场合;(JDK 8 已经没有方法区了,改为元数据区)
(2)JAVA虚拟机栈溢出,不会抛OOM error,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
(3)直接内存不足,也会导致 OOM
如何判断是否有内存泄露?
泄露可以对比不同时间点内存分配,一般看用户类型的分配情况,什么在增加。具体,比如用jmap -histo:live 多次快照,然后对比差异,或者用jmc之类profiling工具,都可以进行,对比会更加流畅一些
定位 Full GC 发生的原因,有哪些方式?
1,首先通过printgcdetail 查看fullgc频率以及时长
2,通过dump 查看内存中哪些对象多,这些可能是引起fullgc的原因,看是否能优化
3,如果堆大或者是生产环境,可以开起jmc 飞行一段时间,查看这期间的相关数据来订位问题
5.其他
美团问过,直接内存
执行引擎的作用什么?
将字节码指令 解释或编译为对应平台上的本地及其指令。(执行什么字节码指令依赖程序计数器)
简单来说,JVM中执行引擎充当了将高级语言翻译为机器语言的译者。
什么是解释器?什么是JIT编译器?
HotSpot内嵌了两个JIT编译器 ,分别是C1 客户端模式下/C2 服务器模式下
为什么说Java是半编译半解释型语言
为什么需要解释器?为什么需要编译器?
顺便可以跟面试官扯扯冷机热机
甚至可以告诉面试官 -Xint 完全采用解释器 -Xcomp完全采用即时编译器
如何判断热点代码探测 进行栈上替换
顺便说一下热度衰减:超过一定时间限度如果方法调用次数仍然不足以让他提交给JIT,那就砍一半。
再复习下Java代码的编译执行过程。
这个颜色的部分是javac前端编译器来执行,最终目的是遍历语法树,生成 线性的字节码指令流
中间这条是逐行解释执行的过程。
下面是传统编译原理中,程序代码到目标机器代码的生成过程。
橙色部分是javac前端编译器做的,跟JVM虚拟机没有关系
绿色部分和蓝色部分是JVM虚拟机要考虑的问题
小知识点:
字符串常量池最小长度1009 jdk7设置的60013
字符串常量与常量拼接结果在常量池,原理是编译期优化
只要计算过程中出现了变量,就不等
String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
System.out.println(s1 == s2); //true
System.out.println(s1.equals(s2)); //true
new String(“ab”)会创建几个对象?
看字节码就知道是两个。
一个对象是通过new关键字在堆空间创建的,另一个对象是:字符串常量池中的对象“ab”(如果要是声明过就不造了)
new String(“a”) + new String(“b”)呢?
对象1 : new StringBuilder() 涉及到字符串拼接(+)
对象2 : new String(“a“)
对象3 : 常量池的“a“
对象4 : new String(“b“)
对象5 : 常量池的“b“
深入剖析:对象6: 字节码指令中把右边的字符串通过String方法返回(StringBuilder的toString方法的时候,帮我们返回一个String)
强调一下:toString()的调用,在字符串常量池中,没有生成“ab”