Java面试题——进阶篇

本文深入探讨了JVM调优的重要性、原则及步骤,强调了不应过早优化。介绍了垃圾回收机制,包括如何确定对象为垃圾、判断对象存活的算法以及常用的垃圾回收算法如标记-清除、标记-整理和复制算法。最后,概述了分代收集算法和各种典型的垃圾收集器,如Serial、ParNew、Parallel Scavenge、Parallel Old、CMS和G1,强调了它们的设计目标和适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.JVM调优

http://t.csdn.cn/PMqyu

1.如果使用合理的 JVM 参数配置,在大多数情况应该是不需要调优的,

JVM 经过这么多年的发展和验证,整体是非常健壮的。个人认为99%的情况下,基本用不到 JVM 调优。

通常来说,我们的 JVM 参数配置大多还是会遵循 JVM 官方的建议,例如:-XX:NewRatio=2,年轻代:老年代=1:2,堆内存设置为物理内存的3/4左右,等等

JVM 参数的默认(推荐)值都是经过 JVM 团队的反复测试和前人的充分验证得出的比较合理的值,因此通常来说是比较靠谱和通用的,一般不会出大问题。

当然,更重要的是,大部分的应用数据量不到几万,这种低压环境下,想让 JVM 出问题,说实话也挺难的。

大部分同学更常遇到的应该是自己的代码 bug 导致 OOM、CPU load高、GC频繁啥的,这些场景也基本都是代码修复即可,通常不需要动 JVM。

当然,俗话说得好,凡事无绝对,还是有一小部分场景,是可能需要用到 JVM 调优的。具体哪些场景,我们在下面介绍。

值得一提的是,我们这边所说的 JVM 调优更多的是针对自己的业务场景对 JVM 参数进行优化调整,使其更适合我们的业务,而不是指对 JVM 源码的改动。

2.忌过早优化。《计算机程序设计艺术》的作者高德纳(Donald Ervin Knuth)曾说过一句经典的话:真正的问题是,程序猿在错误的地方和错误的时间花了太多的时间担心效率问题;过早的优化是编程中所有(或者至少是大部分)罪恶的根源。

忌过早并不是说就完全不管,比较正确的做法应该是给核心服务的一些重要 JVM 指标配上监控告警,当指标出现波动或者异常时,能及时介入排查。

3.优化步骤

1.分析和定位当前系统的瓶颈

1)CPU指标

查看占用CPU最多的进程

查看占用CPU最多的线程

查看线程堆栈快照信息

分析代码执行热点

查看哪个代码占用CPU执行时间最长

查看每个方法占用CPU时间比例

2)JVM 内存指标

查看当前 JVM 堆内存参数配置是否合理

查看堆中对象的统计信息

查看堆存储快照,分析内存的占用情况

查看堆各区域的内存增长是否正常

查看是哪个区域导致的GC

查看GC后能否正常回收到内存

3)JVM GC指标

查看每分钟GC时间是否正常

查看每分钟YGC次数是否正常

查看FGC次数是否正常

查看单次FGC时间是否正常

查看单次GC各阶段详细耗时,找到耗时严重的阶段

查看对象的动态晋升年龄是否正常

2.确定优化目标

定位出系统瓶颈后,在优化前先制定好优化的目标是什么,例如:

将FGC次数从每小时1次,降低到1天1次

将每分钟的GC耗时从3s降低到500ms

将每次FGC耗时从5s降低到1s以内

3、制订优化方案

针对定位出的系统瓶颈制定相应的优化方案,常见的有:

代码bug:升级修复bug。典型的有:死循环、使用无界队列。

不合理的JVM参数配置:优化 JVM 参数配置。典型的有:年轻代内存配置过小、堆内存配置过小、元空间配置过小。

4、对比优化前后的指标,统计优化效果

5、持续观察和跟踪优化效果

6、如果还需要的话,重复以上步骤

不考虑应付面试的因素,升级垃圾回收器确实会是最有效的方式之一,例如:CMS 升级到 G1,甚至 ZGC。

2.垃圾回收机制GC

垃圾回收(GC)是由 Java 虚拟机(JVM)垃圾回收器提供的一种对内存回收的一种机制,它一般会在内存空闲或者内存占用过高的时候对那些没有任何引用的对象不定时地进行回收。

1.如何确定某个对象是垃圾

对于Java对象来讲,如果说这个对象没有被其他对象所引用该对象就是无用的,此对象就被称为垃圾,其占用的内存也就要被销毁;

2.JVM判断对象是否存活的算法

  • 引用计数算法

一个对象被创建之后,系统会给这个对象初始化一个引用计数器,当这个对象被引用了,则计数器 +1,而当该引用失效后,计数器便 -1,直到计数器为 0,意味着该对象不再被使用了,则可以将其进行回收了。

这种算法其实很好用,判定比较简单,效率也很高,但是却有一个很致命的缺点,就是它无法避免循环引用,即两个对象之间循环引用的时候,各自的计数器始终不会变成 0,所以 引用计数算法 只出现在了早期的 JVM 中,现在基本不再使用了。

  • 可达性分析算法

通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

3.垃圾回收算法

1.标记 - 清除算法(Tracing Collector)
标记-清除 算法是最基础的收集算法,它是由 标记 和 清除 两个步骤组成的。

标记的过程其实就是上面的 可达性算法(根搜索) 所标记的不可达对象,当所有的待回收的“垃圾对象”标记完成之后,便进行第二个步骤:统一清除。

该算法的优点是当存活对象比较多的时候,性能比较高,因为该算法只需要处理待回收的对象,而不需要处理存活的对象。

但是缺点也很明显,就是在执行完 标记-整理 之后,由于将“垃圾对象”回收掉了,所以原本连续使用的内存块便会变得不连续,这样会导致内存块上面会出现很多小单元的内存区域,这些小单元的内存区域只能够存放比较小的对象,而比较大的对象是无法直接存储的。

2.标记 - 整理算法(Compacting Collector)
上述的 标记-清除 算法会产生内存区域使用的间断,所以为了将内存区域尽可能地连续使用, 标记-整理 算法应运而生。

标记-整理 算法也是由两步组成,标记 和 整理。

第一步的 标记 动作也是使用的 根搜索算法,但是在标记完成之后的动作却和 标记-清除算法 天壤之别,该算法并不会直接清除掉可回收对象 ,而是让所有的对象都向一端移动,然后将端边界以外的内存全部清理掉。

该算法所带来的最大的优势便是使得内存上面不会再有碎片问题,并且新对象的分配只需要通过简单的指针碰撞便可完成。

3.复制算法(Copying Collector)
无论是标记-清除算法还是垃圾-整理算法,都会涉及句柄的开销或是面对碎片化的内存回收,所以,复制算法 出现了。

复制算法将内存区域均分为了两块(记为S0和S1),而每次在创建对象的时候,只使用其中的一块区域(例如S0),当S0使用完之后,便将S0上面存活的对象全部复制到S1上面去,然后将S0全部清理掉。

复制算法的优势是:① 不会产生内存碎片;② 标记和复制可以同时进行;③ 复制时也只需要移动栈顶指针即可,按顺序分配内存,简单高效;④ 每次只需要回收一块内存区域即可,而不用回收整块内存区域,所以性能会相对高效一点。

但是缺点也是很明显的:可用的内存减小了一半,存在内存浪费的情况。

所以 复制算法 一般会用于对象存活时间比较短的区域,例如 年轻代,而存活时间比较长的 老年代 是不适合的,因为老年代存在大量存活时间长的对象,采用复制算法的时候会要求复制的对象较多,效率也就急剧下降,所以老年代一般会使用上文提到的 标记-整理算法。

 

4.Generational Collection(分代收集)算法
        分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

        目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

        而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

        注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

4.典型的垃圾收集器
        垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面介绍一下HotSpot(JDK 7)虚拟机提供的几种垃圾收集器,用户可以根据自己的需求组合出各个年代使用的收集器。

1、Serial/Serial Old
        Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

2、ParNew
        ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

3、Parallel Scavenge
        Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

4、Parallel Old
        Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

5、CMS
        CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

6、G1
        G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值