被面试伤透了心,手撕不出来的算法题,背得零零落落的八股。最经常说的话是:对不起,我不太记得了,我不太清楚这个,我没有考虑到。然后一被问并发就回答:加锁,加锁加锁。
以下的各种图片和文字,基本来自:参考
1.JVM分区:Java 8 的内存结构
TLAB(Thread-Local Allocation Buffer) 是JVM 中堆内存管理的一种优化技术,用于减少多线程环境下对象分配的竞争,提高分配对象的效率。 它为每个线程分配一块独立的小堆空间,专门用于分配新对象,从而避免线程间的锁争用。参考
2.如何分配对象
Java 中对象地址操作主要使用 Unsafe 调用了 C 的 allocate 和 free 两个方法,分配方法有两种:
- 空闲链表(free list): 通过额外的存储记录空闲的地址,将随机 IO 变为顺序 IO,但带来了额外的空间消耗。
- 碰撞指针(bump pointer): 通过一个指针作为分界点,需要分配内存时,仅需把指针往空闲的一端移动与对象大小相等的距离,分配效率较高,但使用场景有限。参考
3.如何识别垃圾?
-
引用计数法(Reference Counting): 对每个对象的引用进行计数,每当有一个地方引用它时计数器 +1、引用失效则 -1,引用的计数放到对象头中,大于 0 的对象被认为是存活对象。虽然循环引用的问题可通过 Recycler 算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。
-
可达性分析,又称引用链法(Tracing GC)(Java采用的方法): 从 GC Root 开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。目前 Java 中主流的虚拟机均采用此算法。
所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。
GC Roots 是指:
Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中引用的对象
方法区中常量引用的对象
方法区中类静态属性引用的对象
GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。
4.垃圾收集方法
自从有自动内存管理出现之时就有的一些收集算法,不同的收集器也是在不同场景下进行组合。
- Mark-Sweep(标记-清除): 回收过程主要分为两个阶段,第一阶段为追踪(Tracing)阶段,即从 GC Root 开始遍历对象图,并标记(Mark)所遇到的每个对象,第二阶段为清除(Sweep)阶段,即回收器检查堆中每一个对象,并将所有未被标记的对象进行回收,整个过程不会发生对象移动。
- Mark-Compact (标记-整理): 这个算法的主要目的就是解决在非移动式回收器中都会存在的碎片化问题,也分为两个阶段,第一阶段与 Mark-Sweep 类似,第二阶段则会对存活对象按照整理顺序(Compaction Order)进行整理。
- Copying(复制): 将空间分为两个大小相同的 From 和 To 两个半区,同一时间只会使用其中一个,每次进行回收时将一个半区的存活对象通过复制的方式转移到另一个半区。
5.垃圾收集器
根据这篇技术文章的说明,我推测估计大部分公司都在使用CMS和G1。
目前在 Hotspot VM 中主要有分代收集和分区收集两大类,“未来会逐渐向分区收集发展。”
在美团内部,有部分业务尝试用了 ZGC(感兴趣的同学可以学习下这篇文章 新一代垃圾回收器ZGC的探索与实践),其余基本都停留在 CMS 和 G1 上。
分代收集器
- ParNew: 一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。
- CMS: 以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除,详情可见 JEP 363。
分区收集器
- G1: 一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。
- ZGC: JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。
- Shenandoah: 由 Red Hat 的一个团队负责开发,与 G1 类似,基于 Region 设计的垃圾收集器,但不需要 Remember Set 或者 Card Table 来记录跨 Region 引用,停顿时间和堆的大小没有任何关系。停顿时间与 ZGC 接近,下图为与 CMS 和 G1 等收集器的 benchmark。
6.常用工具(面试被问到了)
命令行终端
标准终端类:jps、jinfo、jstat、jstack、jmap
功能整合类:jcmd、vjtools、arthas、greys
可视化界面
简易:JConsole、JVisualvm、HA、GCHisto、GCViewer
进阶:MAT、JProfiler
7.要根据指标判断到底是不是GC问题
评判 GC 的两个核心指标:延迟(Latency)和吞吐量(Throughput)
- 延迟一般包括单向延迟(One-way Latency)和往返延迟(Round Trip Latency),实际测量时一般取往返延迟。它的单位一般是ms、s、min、h等。
- 而吞吐量一般指相当一段时间内测量出来的系统单位时间处理的任务数或事务数(TPS)。注意“相当一段时间”,不是几秒,而可能是十几分钟、半个小时、一天、几周甚至几月。它的单位一般是TPS、每单位时间写入磁盘的字节数等。
低延迟一定意味着高吞吐量吗? 如果不是,试举出反例。
假如有一个网站系统,客户端每次请求网站服务端,网络传输时间(包括往返)为 200ms,服务端处理请求为10ms。那么如果是同步请求,则延迟为210ms。此时如果提高网络传输速度,比如提高到100ms,那么延迟为110ms。这种情况减少延迟似乎确实可以一定程度提高吞吐量,原因主要在于:系统性能瓶颈不在于服务端处理速度,而在于网络传输速度。
对于图片中的公式,我实在难以理解,借助了GPT老师:
- TP9999(99.99% 分位数延迟)
TP9999表示在所有请求中,99.99% 的请求延迟应该低于某个阈值。
例如,TP9999 = 80ms,表示在一百万个请求中,只有100个请求的延迟可以超过80ms。
TP9999衡量的是系统在极端情况下的性能,确保服务即使在压力下也保持较低延迟。 - 单次GC停顿时间不得超过TP9999
GC停顿会导致请求延迟增加,从而影响TP9999指标。
如果GC停顿时间超过TP9999,就可能导致请求延迟超过TP9999阈值,进而降低系统的整体SLA(服务水平协议)。
举例:TP9999 = 80ms → GC停顿时间不应超过80ms,否则99.99%的请求延迟就可能被拉高,进而导致用户体验下降。
8.开始分析GC耗时长或者频繁GC的原因
简介:GCeasy是一款开源工具,用于帮助开发者快速理解Java垃圾回收(GC)日志。 通过GCeasy,开发者可以轻松分析GC日志,定位内存泄漏和优化垃圾回收性能。
JVM是一个计算设备规范,JIT是实时编译技术,HotSpot是Oracle Java SE的主要JVM实现,它包含解释器和自适应编译器。HotSpot通过记录方法调用次数来决定何时使用JIT编译进行优化。JIT编译和自适应编译都是动态编译,但JIT更侧重于首次执行前的编译,而自适应编译则在代码执行多次后进行。
作者给出了好多分析方法,根据发生的时间线,历史解决的概率等等,再一次佩服他们的思路(俺真的能成为这样的开发人员吗?)RT 通常指的是 Response Time(响应时间)
因为文章中指出的GC问题太多,我就截取我看得懂,和面试碰到的问题比较匹配的知识点记录(希望有机会倒逼自己全部读一遍)。
8.1服务启动开始时的频繁GC
观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。
-Xms:设置堆内存的初始大小(即 JVM 启动时分配的堆内存大小)。
-Xmx:设置堆内存的最大大小(即 JVM 在运行过程中最大可以扩展到的堆内存)。
在 JVM 的参数中 -Xms 和 -Xmx 设置的不一致,在初始化时只会初始 -Xms 大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 GC。
一般来说,我们需要保证 Java 虚拟机的堆是稳定的,确保 -Xms 和 -Xmx 设置的是一个值(即初始值和最大值一致),获得一个稳定的堆,同理在 MetaSpace 区也有类似的问题。不过在不追求停顿时间的情况下震荡的空间也是有利的,可以动态地伸缩以节省空间,例如作为富客户端的 Java 应用。
8.2堆本身不需要发生GC,却发生了GC
基本上代码中system.gc导致的,作者解释了删掉这个代码会导致内存泄漏,不删GC问题又解决不了。
此处补充一个知识点,CMS GC 共分为 Background 和 Foreground
两种模式,前者就是我们常规理解中的并发收集,可以不影响正常的业务线程运行,但 Foreground Collector
却有很大的差异,他会进行一次压缩式 GC。此压缩式 GC 使用的是跟 Serial Old GC 一样的 Lisp2 算法,其使用
Mark-Compact 来做 Full GC,一般称之为 MSC(Mark-Sweep-Compact),它收集的范围是 Java 堆的
Young 区和 Old 区以及 MetaSpace。由上面的算法章节中我们知道 compact 的代价是巨大的,那么使用
Foreground Collector 时将会带来非常长的 STW。如果在应用程序中 System.gc 被频繁调用,那就非常危险了。
解决方法:设置system.gc为Background。
JVM 还提供了 -XX:+ExplicitGCInvokesConcurrent 和 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 参数来将 System.gc 的触发类型从 Foreground 改为 Background,同时 Background 也会做 Reference Processing,这样的话就能大幅降低了 STW 开销,同时也不会发生 NIO Direct Memory OOM。
8.2Old区频繁GC,而且每次GC效果显著
原因就是Young区太小,或者对象太早就晋升到Old了。那么就对症下药,增大Young区,增大晋升所要达到的躲过GC次数。
8.3CMS Old GC 频繁
Old 区频繁的做 CMS GC,但是每次耗时不是特别长,整体最大 STW 也在可接受范围内,但由于 GC 太频繁导致吞吐下降比较多。
这种情况比较常见,基本都是一次 Young GC 完成后,负责处理 CMS GC 的一个后台线程 concurrentMarkSweepThread 会不断地轮询,使用 shouldConcurrentCollect() 方法做一次检测,判断是否达到了回收条件。如果达到条件,使用 collect_in_background() 启动一次 Background 模式 GC。轮询的判断是使用 sleepBeforeNextCycle() 方法,间隔周期为 -XX:CMSWaitDuration 决定,默认为2s。
我们这里还是拿最常见的达到回收比例这个场景来说,与过早晋升不同的是这些对象确实存活了一段时间,Survival Time 超过了 TP9999 时间,但是又达不到长期存活,如各种数据库、网络链接,带有失效时间的缓存等。
处理这种常规内存泄漏问题基本是一个思路,主要步骤如下:
Dump Diff 和 Leak Suspects 比较直观就不介绍了,这里说下其它几个关键点:
内存 Dump: 使用 jmap、arthas 等 dump 堆进行快照时记得摘掉流量,同时分别在 CMS GC 的发生前后分别 dump 一次。
分析 Top Component: 要记得按照对象、类、类加载器、包等多个维度观察 Histogram,同时使用 outgoing 和 incoming 分析关联的对象,另外就是 Soft Reference 和 Weak Reference、Finalizer 等也要看一下。
分析 Unreachable: 重点看一下这个,关注下 Shallow 和 Retained 的大小。如下图所示,笔者之前一次 GC 优化,就根据 Unreachable Objects 发现了 Hystrix 的滑动窗口问题。
9.整体的SOP
10.对于面试问题:如果遇到频繁GC,你会怎么做?
很遗憾,我读完(X)这篇技术文档,还是没办法有一个清晰的回答。
首先,确定频繁GC是因,还是果,是果的话就去解决真正的因。
是因的话,再确定(如果是分代的模型),是哪个区域,什么cause引起的GC。
然后根据这些问题,先搜索或请教别人是否在整个状况下发生过类似的情况,逐一排查。
最终的解决归宿大致就是JVM参数,代码逻辑(?)这些的修改。
11.使用什么工具进行分析。
https://blog.csdn.net/qq_45076180/article/details/108483094
命令行使用top命令查看系统CPU的占用情况
通过jstat命令监控GC情况
命令行终端
标准终端类:jps、jinfo、jstat、jstack、jmap(jdk自带的)
功能整合类:jcmd、vjtools、arthas、greys
可视化界面
简易:JConsole、JVisualvm、HA、GCHisto、GCViewer
进阶:MAT、JProfiler
JVM 的内存快照可以使用jmap 工具生成,而Dump 文件可以使用jhat 工具分析。
阿里开源的 Java 诊断神器 Arthas :Arthas 是阿里开源的一款线上 Java 诊断神器,通过全局的视角可以查看应用程序的内存、GC、线程等状态信息,并且能够在不修改代码的情况下,对业务问题进行诊断,包括查看方法的参数调用、执行时间、异常堆栈等信息,大大提升了生产环境中问题排查的效率。
JProfiler介绍 :JProfiler是一个用于分析运行JVM内部情况的专业工具。 在开发中你可以使用它,用于质量保证,也可以解决你的生产系统遇到的问题。