笔记:JVM - 垃圾回收与锁机制

1. 一些琐碎的知识点:
  • 了解一下三色标记法和 SATB(Snapshot At The Beginning ,G1(以及 CMS ?)收集器采用了这项技术) 。
  • 了解一下 HSDB 工具(可以查询出某个类型的所有对象的内存地址,显示内存中的内容并对它们进行解析)。
  • 了解一下 arthas(阿里的一个 Java 诊断工具)。
  • 了解一下 MAT 和 JProfiler 工具。
  • 了解一下 MESI(缓存一致性协议)。
  • 了解一下 LockSupport、CountDownLatch、CyclicBarrier、Exchanger 的用法。
  • 了解一下压缩指针,BlockingQuene,以及 synchronized 原理。
  • JDK 的新特性一般可分为语法层面(Lambda 表达式等)、API 层面(Stream API 等)和底层优化(元空间代替永久代,GC 优化等)。

2. Java 和 C++ 相比,有内存动态分配和垃圾收集技术,即自动内存管理,降低内存泄漏和内存溢出的风险,程序员可以专心于业务开发;但另一方面也弱化了 Java 开发人员在程序出现内存溢出时定位和解决问题的能力。此外,Java 还有数组下标越界检查。

3. GC 主要关注堆和方法区,因为栈、本地方法栈和程序计数器都不会有垃圾;此外,栈、本地方法栈、程序计数器、堆、方法区这 5 部分中,只有程序计数器不会有溢出问题。
堆和方法区是线程共享的;另外 JVM 规范并没有要求一定要对方法区进行 GC ,因此堆是 GC 的工作重点。

4. 引用计数法:
  • 优点:实现简单,判定垃圾的效率高,回收没有延迟性(只要引用计数为 0 就可以回收,无需等到内存不足)。
  • 缺点:每个对象都需要额外的空间来存储引用计数器,增加了存储的开销;计数器的更新也需要时间开销;最重要的是不能解决循环引用。
  • Python 采用了引用计数法,如何解决循环引用问题?1. 手动解除循环引用;2. 采用弱引用。

5. 根可达算法(追踪性垃圾收集)中的根对象:栈变量、本地方法(JNI)栈变量、运行时常量池中的对象、方法区中的静态变量、Class 对象、已经被线程获取的锁、常驻的异常对象(空指针异常、OOM 等)、系统类加载器。如果是 MinorGC ,那么老年代中的某些对象也可以临时成为根对象。小技巧:某个不在堆内存里的指针指向了堆内存中的对象,则该指针就是一个 Root 。


6. 实际显示的堆内存大小比我们设置的值要小,这是因为运行时两个 Survivor 区总有一个是空的,而空的那个是不计入实际的大小之中的。

7. 部分收集的 GC 包括:
  • MinorGC:Eden 区满了会触发 MinorGC ,但 From 区满了并不会触发。
  • MajorGC:仅仅对老年代的垃圾回收,只有 CMS 有单独的收集老年代的行为。
  • MixedGC:收集整个新生代和部分老年代,只有 G1 有这种行为。
FullGC 是收集整个堆和方法区。

8. Hotspot VM 内存结构的变化(注意,永久代与元空间都是对方法区的实现):
  1. Java 7 之前,有永久代,静态变量的引用存放在永久代上(注意,永久代是 Hotspot 上独有的概念);
  2. Java 7 时,有永久代,但字符串常量池、静态变量的引用保存在堆中,已经开始为移除永久代做准备了;
  3. Java 8 开始,无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,而字符串常量池、静态变量的引用仍在堆中。


9. 为什么 Hotspot 在 Java 8 后用元空间替换永久代?
  1. 为了实现对 Hotspot 和 JRockit 的整合(永久代是 Hotspot VM 上独有的概念,而 JRockit VM 对方法区的实现是元空间)。
  2. 永久代的大小难以确定,太大了浪费 JVM 内存,太小了容易频繁 GC 甚至 OOM ;元空间位于本地内存,其大小基本仅受本地内存限制。
  3. 对永久代的调优比较困难。

10. 为什么 Java 7 把 StringTable 从永久代移到了堆中?
实际开发中会有大量的字符串被创建。一方面,永久代只有在 FullGC 的时候才会进行垃圾回收,而 FullGC 的频率是比较低的,会导致没用的字符串不能被及时回收,导致永久代内存不足;另一方面,永久代内存不足又会导致 FullGC ,极大地降低系统的性能。因此将 StringTable 放到堆里,方便及时回收不用的字符串。

11. JVM 规范没有规定方法区必须有 GC(如 JKD 11 时期引入的 ZGC 收集器就不支持类卸载),另一方面方法区的 GC 的回收效果很难令人满意(特别是类的卸载,判断条件很苛刻),但有时又确实是必须的(不进行 GC 可能会发生错误)。

12. 方法区的 GC 主要回收废弃的常量和不再使用的类型。对于常量,只要它没有被引用,就可以被回收(类似于堆中的对象);而某个类不再被使用则需要同时满足以下 3 个条件(JVM 允许同时达成这 3 个条件的类被回收,注意,是允许而不是一定会回收):
* 堆中不存在该类及其子类的实例;
* 加载该类的类加载器已经被回收(该条件很难达成。因为类本身会引用加载它的类加载器?);
* 该类的 Class 对象没有被引用。
大量使用反射、动态代理、JSP、OSGi 的场景,都要求 JVM 具备类卸载的能力。

13. 对象的创建过程如下所示。内存规整指的是内存中一部分全是对象(假设为 P1 ),另一部分全是空闲空间。指针碰撞(Bump The Pointer)中的指针指向 P1 的末尾,而指针碰撞指的是在 P1 末尾给新对象分配了内存空间后,该指针相应地进行更新,指向新的 P1 的末尾。

14. 关于 finalization 机制:
  1. finalize() 默认是空方法,可以在该方法内完成一些资源释放和清理的工作,作用和 C++ 的析构函数相似,但又有本质上的不同;
  2. 不要主动调用 finalize() 方法,而应该让垃圾回收机制在回收垃圾时来调用。注意任意对象的 finalize()  都最多只会被垃圾回收机制调用一次;
  3. 该方法可以让已经成为垃圾的对象变成不是垃圾(复活),比如在该方法中让一个静态变量指向了本对象;
  4. finalize() 方法的执行时间是没有保障的,它完全由 GC 线程决定,极端情况下,不发生 GC 则不会执行 finalize() 方法;
  5. 一个糟糕的 finalize()  会严重影响 GC 的性能。
由于垃圾对象是可能通过 finalize() 方法复活的,因此虚拟机中对象的状态可以是下面三种之一:
  1. 可触及的:即根可达的;
  2. 可复活的:根不可达,但重写了 finalize() 方法且该方法未被调用(有机会复活);
  3. 不可触及的:根不可达,且没有重写 finalize() 方法或已经调用了 finalize() 但还是不可达,这种情况下对象才会被回收。


15. 垃圾回收算法:
  1. 标记 - 清除算法:标记的是可达的对象,而不是把垃圾标记出来;效率一般,会产生内存碎片。
  2. 复制算法:适合用于存活对象较少的情况,这和新生代大多数对象朝生夕死的特性很契合;实现简单,高效,无内存碎片,但空间浪费严重。
  3. 标记 - 压缩算法:标记 - 清除后进行内存整理;无内存碎片,空间开销少,但效率很低。

16. 增量收集算法:基础仍是标记清除和复制算法。主要思想是让垃圾回收线程和用户线程交替执行,减少单次 STW 的时间,但由于线程的频繁切换,总体上垃圾回收时间更长,系统吞吐量也下降了。

17. G1 收集器同时采用了分代和分区算法。分代算法是根据对象的存活时间对堆进行划分,分区算法是将整个堆划分成多个小区间(Region)。分区算法和增量收集算法都是为了降低单次 STW 的时间。如果允许的最大 STW 时间较小,分区算法就优先对最具回收价值的区间进行回收。

18. System.gc() 其实就是在调用 Runtime.getRuntime().gc() ,它只是建议(而不是强制) JVM 进行一次 FullGC ,开发中尽量不要调用。System.runFinalization() 则是强制让 Finalizer 调用队列中对象的 finalize() 方法。

19. 代码块中引用的对象:
public void method1(){
    {
        MyBigObject o = new MyBigObject();
    }
    
    //执行到这里 o 指向的对象不会被回收
    //因为该对象的地址仍然存在于栈帧的局部变量表的第 2 个 slot 中,第 1 个 slot 当然就是 this 了
    System.gc();
}
 
public void method2(){
    {
        MyBigObject o = new MyBigObject();
    }
    int tem = 1;
    
    //执行到这里 o 指向的对象会被回收
    //因为 slot 复用,使得栈帧中局部变量表的第 2 个 slot 已经变成了 1
    System.gc();
}

20. 
为新对象分配空间时,如果空间不够,会先 GC ,GC 完了还不够则报 OOM ;当然如果新对象所需空间比整个堆空间还大,就直接报 OOM 了。OOM 的原因可能是堆内存本身过小、内存泄漏,或存在大量生命周期较长的大对象。
存疑:当一个线程出现 OOM 时,该线程会结束,并且释放该线程所占用的堆空间,对其它线程来说相当于发生了 GC 。

21. 严格意义上的内存泄漏指的是不可达的对象无法被 GC 清除;广义上,有意或无意地延长了对象的生命周期也算是内存泄漏。举内存泄漏的例子时不要说循环引用,因为 Java 本来就没采用引用计数法,不存在循环引用导致内存泄漏的情况。正确的例子有:单例对象引用了生命周期较短的对象,导致后者不能被回收;没有调用资源的 close() 方法。

22. 可达性分析工作必须在一个能够确保一致性的快照中进行。而根对象是会动态增加/减少的,因此 STW 就是为了保证一致性。任何垃圾回收器都不能避免 STW ,只能尽量减少 STW 的时间。

23. 并行(Parallel)与并发(Concurrent):
  • 并行:多个线程在各自的 CPU 上一起执行。
  • 并发:一个 CPU 通过快速切换线程,使得这些线程看起来像是在同时执行。并发就是看起来像并行。
  • GC 中的并行:多个垃圾回收线程并行工作,而用户线程处于等待状态。
  • GC 中的并发:垃圾回收线程和用户线程同时(可能是并行或并发)执行。

24. 五种引用类型:
  1. 强引用:任何时候都不会回收强引用指向的对象,即使内存不足;是造成 Java 内存泄漏的主要原因之一;
  2. 软引用:内存充足时不会回收软引用指向的对象,但内存不足时会回收;通常用于实现内存敏感的缓存;
  3. 弱引用:发现即回收;被弱引用指向的对象,在下一次 GC 中必定会被回收;用处:WeakHashMap ,以及保存一些可有可无的缓存数据;
  4. 虚引用:获取虚引用指向的对象时总是得到空;主要用于对象回收跟踪(创建虚引用时必须指定一个队列,如果队列中有元素,说明该虚引用指向的对象已经被回收);具体用处有:管理堆外内存(虚引用 Cleaner 指向 DirectByteBuffer 对象,DirectByteBuffer 对象指向堆外内存;当 DirectByteBuffer 对象被回收时,虚引用 Cleaner 入队,GC 的某个线程负责从队列中获取 Cleaner ,然后释放该 Cleaner 对应的堆外内存)。
  5. 终结器引用:即  FinalReference ,它的作用是让被它指向的对象的 finalize() 方法被 Finalizer 线程执行。JVM 发现垃圾对象时,会创建一个终结器引用指向它;在 GC 时,终结器引用入队,然后低优先级的 Finalizer 线程通过终结器引用找到被引用对象,调用它的 finalize() 方法;在第二次 GC 时被引用的对象才有可能会被回收。
创建软/弱/虚引用时都可以指定一个队列,当软/弱/虚引用指向的对象被回收时,这个引用会被放入到那个队列中。
软/弱/虚引用本身也是一个对象,且被一个强引用所引用。

25. GC 必须等到所有用户线程都执行到安全点(Safe Point)后才能开始垃圾回收;安全点过少会导致 GC 的等待时间过长,过多可能会导致运行时的性能问题。一般选择执行时间较长的指令作为安全点,如方法调用、循环跳转、异常跳转。
保证 GC 时用户线程都执行到了安全点的方式有两种:
* 抢先式中断(目前的虚拟机已不再采用):中断所有线程,如果有线程没执行到安全点,则恢复它,让它执行到安全点;
* 主动式中断:设置一个中断标志,线程执行到安全点后检查这个标志,如果标志为真则将自己中断挂起。
有的线程可能处于阻塞或休眠状态,此时它们不能响应中断,自然也不能自己执行到安全点,这时候就需要安全区域(Safe Rigion)了。如果一段代码中,对象的引用关系不会发生变化,那么在这段代码中的任何位置开始 GC 都是安全的,此时称这段代码为安全区域。当线程执行到安全区域后,标记自己已经进入安全区域,如果期间发生 GC ,则 JVM 会忽略这些线程;线程离开安全区域后,会检查 GC 是否完成,是的话继续执行,不是的话必须等待,直到接收到可以安全离开安全区域的信号。

26. GC 收集器没有明确的规范,分类:根据垃圾收集线程的数量可分为串行和并行;根据工作模式可分为并发式和独占式;根据碎片处理方式可分为压缩式和非压缩式;根据工作的内存区间可分为新生代回收和器老年代回收器。

27. GC 中吞吐量指的是用户线程运行时间占总运行时间的比例(和垃圾收集开销互为补数),此外常用的指标还有暂停时间(单次 STW 的时间)和内存占用,三者的关系类似于 CAP ,最多只能同时满足其中两个。开发中最关注的还是吞吐量和暂停时间这两项指标,这两者是相互矛盾的,一般是在保证大吞吐量的情况下,尽量降低暂停时间
吞吐量高,则程序运行速度快,适合那些在后台运算而不需要太多交互的任务,因此在服务器环境中很常用。
暂停时间短,则用户体验好,这对交互式应用程序来说是非常重要的。
可通过 -XX:GCTimeRatio 和 -XX:MaxGCPauseMillis 参数设置期望的最大的垃圾收集开销和单次 GC 最大的停顿时间。


28. 7 个经典的垃圾收集器如下图所示。
红色虚线表示 JDK 8 中不建议使用这样的组合关系,JDK 9 中彻底取消了这种组合关系;
绿色虚线表示 JDK 14 中删除了这种组合关系/垃圾收集器;
青色虚线表示 JDK 9 中不建议使用 CMS ,JDK 14 中删除了 CMS 。


29. 可通过 -XX:+PrintCommandLineFlags 参数来打印出命令行参数(其中就包含使用了哪个垃圾收集器的信息)。当启用了某个垃圾回收器时,与它相配合的垃圾回收器也会默认启用,比如使用 -XX:UseConcMarkSweepGC 开启 CMS 收集器时,ParNew 收集器也会默认启用。

30. 垃圾收集器的比较:
  • 并行收集器更关注吞吐量,Java 8 默认的 GC 收集器。
  • CMS 和 G1 更关注暂停时间,在延迟可控的情况下尽可能提高吞吐量,适合用于服务端。
  • 串行收集器实现简单,没有切换线程的开销,因此在硬件配置较低时(如只有一个单核 CPU )或者 Client 模式下,它的效果甚至是最好的。
Serial Old 在 Client 模式下是默认的老年代收集器;在 Server 模式下则主要与 PS 配合使用或者作为 CMS 的后备垃圾收集方案。
如果多核 CPU 使用了串行的收集器,那么 GC 时只会有一个 CPU 运行垃圾收集线程,而用户线程必须暂停(即使有空闲的 CPU)。
并行收集器 GC 线程的默认数量(ParallelGCThreads):CPU 个数 count 小于 8 时,就是 count 个;大于 8 时,是 3 + [[5 * count] / 8 ] 个。
CMS 收集器 GC 线程的默认数量(ParallelCMSThreads):[(ParallelGCThreads + 3) / 4] 个。
CMS 收集器的和用户线程并发的 GC 线程的数量用 -XX:ConcGCThread 参数指定,一般是并行线程数的 1/4 。
PS 收集器和 ParNew 收集器的区别:
* 底层框架不同,因此 ParNew 可以和 CMS 配合工作,而 PS 不可以。PS 和 ParallelOld 底层框架是自成一派的。
* PS 是吞吐量优先的,并且有自适应调节策略。PS 比 ParNew 更早出现。

31. CMS 收集器:并发的标记 - 清除收集器,强调低延迟,Hotspot 中第一款真正意义上的并发收集器。主要工作流程:
  1. 初始标记:STW ,但是仅仅标记出 GC Roots 能直接关联的对象,因此暂停时间很短;
  2. 并发标记:继续遍历剩余的对象,耗时较长,但可以和用户线程并发工作;
  3. 重新标记:修正并发标记期间因用户线程没有暂停而导致标记产生变动的那一部分对象的标记记录,耗时稍长,但远比并发标记短;
  4. 并发清理:清除垃圾,由于没有进行内存整理,不需要移动对象,所以此时用户线程也可以运行。
由于最耗时的并发标记和并发清理工作都是可以和用户线程并发执行的,因此总体上程序是低延迟的。
由于 GC 线程和用户线程并发工作,因此不能等到内存不足时才 GC ,而是当堆使用率达到阈值(JDK 6 前默认 68% ,JDK 6 开始默认 92%,用 -XX:CMSInitiatingOccupancyFraction 参数指定)时,就开始 GC ,确保用户线程在 GC 期间仍然有足够的空间来支持程序的运行。
如果用户线程产生垃圾的速度大于 GC 线程回收垃圾的速度(或者老年代内存碎片过多),从而导致用户线程空间不足,就会产生一次 "Concurrent Mode Failure" ,这时候 JVM 将采用备用的 Serial Old 收集器来对老年代进行垃圾回收和内存整理,导致停顿时间变长。
CMS 回收的是老年代,此时新生代中的对象可能作为 GC Roots ,因此在重新标记阶段,需要扫描新生代,看看新生代的对象是否引用了老年代中的对象;可以通过 -XX:+CMSScavengeBeforeRemark 参数让 CMS 在重新标记前先进行 YGC ,以缩短重新标记阶段的扫描时间。
CMS 优点是低延迟、并发收集;缺点有:
* 产生内存碎片,因此在若干次 GC 后需要进行碎片整理;
* 对 CPU 资源非常敏感,并发阶段虽然不会导致用户线程暂停,但总的吞吐量是降低了的(暂停时间和吞吐量的矛盾性);
* 无法处理浮动垃圾:比如在并发标记中,某个对象已经被 CMS 标记为可达的,但如果此时用户线程取消了对该对象的引用(即该对象事实上已经变成了垃圾),就会导致该对象在此次 GC 中不会被回收。
在并发标记阶段,可能会出现两种特殊情况,注意辨别这两种情况:
1. 一个对象可能原本已经是垃圾了,但此时用户线程又重新引用了这个对象。重新标记阶段就是为了防止这样的漏标对象被误回收。
2. 一个对象可能原本已经被 CMS 标记为可达的,但此时用户线程取消了对该对象的引用。这时候该对象就是浮动垃圾。

32. G1 的优势:适用于多核 CPU 的大容量内存的服务端,在以极高概率将单次 STW 时间控制在指定范围内的同时,有很大的吞吐量。
  1. 并行(有多个垃圾回收线程)与并发(可与用户线程并发执行);
  2. 分代收集(可同时回收新生代和老年代),但不要求 Eden 、Survivor 、Old 区是连续的,也不再坚持固定大小和固定数量;
  3. 空间整合:G1 使用分区算法,各个 Region 之间是采用复制算法总体上看相当于标记 - 整理算法
  4. 可预测的停顿时间模型(或说软实时:Soft Real-time):可根据用户指定的最大延迟时间进行 GC ,优先回收最具回收价值的 Region (这也是 Garbage First 名称的由来),当然停顿时间也有可能会超过用户指定的最大值(软实时)。
G1 并不是全方面优于 CMS ,G1 会有更高的内存占用(比如需要维护 Remember Set)和额外执行负载。一般来说,内存在 6 到 8 GB 之间时,两者性能差不多;内存比 6 GB 小时 CMS 更有优势,内存比 8GB 大则 G1 有优势。
G1 的 Region 大小可以用 -XX:G1HeapRegionSize 参数来设置,必须是 2 的 N 次幂,且在 1 到 32 MB 之间。
而 Region 的个数相对而言比较固定,G1 会尽量将堆内存划分成 2048 个 Region ,因此 Region 的大小默认是堆内存的 1 / 2000。
所有 Region(除了 Humongous )的大小相同,且在 JVM 生命周期内大小不会改变。对象大于 0.5 个 Region 时会被分配到 Humongous 区;如果 1 个 H 区还放不下,则分配到连续的 1 块 H 区。由于 H 区存放的是大对象,因此 H 区一般也可以看作是老年代的一部分。G1 不会拷贝大对象,GC 时优先考虑大对象;当老年代不再引用某个大对象时,这个大对象会在下次 YGC 时被回收。
G1 的设计原则是简化 JVM 性能调优,一般我们只需要设置堆的最大大小和允许的最大停顿时间即可,其余的交给 G1 来调整。
当 G1 的 GC 线程较慢时,G1 还可以调用应用线程来承担后台运行的 GC 工作,加速垃圾回收。

33. G1 的垃圾回收模式:YoungGC ,MixedGC ,FullGC 。G1 的垃圾回收过程有 4 个环节,最主要的是前 3 个:
  1. YoungGC:Eden 区用尽时开始 YoungGC ,使用一个并行的独占式的收集器来完成垃圾回收。
  2. Concurrent Marking:当堆使用率达到阈值(用 -XX:InitiatingHeapOcuppancyPercent 指定,默认 45% ;JDK 9 前,阈值设置完就固定下来了;JDK 9 开始,这个参数设置的只是初始值,允许 G1 对阈值进行动态调整,从而尽量避免退化成 FullGC )时,开始老年代并发标记。
  3. MixedGC:并发标记完成后,G1 会将最具回收价值的老年代 Region 中存活的对象复制到空闲区中,后者变成了老年代的一部分,而前者变回空闲区;除了老年代,新生代在该阶段也会被回收,这也是"混合回收"这个名字的由来。
  4. FullGC:是备用的、独占式的、单线程的、高强度的垃圾回收方案,主要是作为一种失败保护机制。


34. 什么是记忆集(Remember Set,或 Rset)?
问题引入:位于 E 区的对象 A 是可能被位于 O 区的对象引用的,那么,进行 YoungGC 时,为了确定 A 是不是垃圾,我们还得把所有 O 区扫描一遍,即全局扫描,这会大大影响效率(这和我们之前提到的老年代的对象可能临时性地成为根对象相对应)。在各种分代收集器中,这种现象是普遍存在的,在 G1 尤其突出(因为 Region 多,且堆内存大)。
记忆集就是用来避免全局扫描的。每个 Region 都有一个记忆集,如果 Region 2 上的对象 B 引用了 Region 1 上的对象 A ,那么 Region 1 的记忆集就会记录:Region 2 上的对象 B 引用了我上面的对象(应该不用记录下来具体引用的是我上面的哪个对象)。

35. G1 YoungGC的步骤(新生代的垃圾回收在 G1 的垃圾回收过程的 4 个环节都是会发生的):
  1. 扫描根:根对象和 Rset 中记录的外部引用是扫描存活对象的入口;
  2. 通过 Dirty Card Queue 更新 Rset :更新完成后,Rset 可以准确地反映老年代对该 Region 中对象的引用;
  3. 处理 Rset(从而确定某个对象是不是垃圾);
  4. 使用复制算法移动对象:从 Eden 区复制到 Survivor 区,或从新生代复制到老年代等;
  5. 处理引用(如软弱虚引用)。
一个 Region 又会被分成很多个卡,如果老年代的卡上的对象引用了新生代上的对象,则该卡被标记为脏卡。
进行 YGC 时,脏卡上的对象作为 GC Roots ,而无需扫描整个老年代。
执行了 son.father = father; 之类的语句时,需要更新 Rset ,但对 Rset 的处理需要线程同步;因此,为了提高性能,JVM 仅仅是把相关的信息放到 Dirty Card Queue 中(其中还会使用到 Post-write Barrier);在进行 YoungGC 时,G1 才会对该队列中所有 Card 进行处理以更新 Rset 。

36. G1 并发标记过程:

  1. 初始标记(同 CMS ):在进行 YGC 时进行初始标记;
  2. 根区域扫描(Root Region Scanning):并发扫描 Survivor 区,详细操作不太明确,该步骤必须在 YoungGC 前完成;
  3. 并发标记(Concurrent Marking),类似于 CMS 的并发标记。不同的是该过程可以被 YoungGC 中断,而且该过程会计算各区域的对象活性,即存活对象的比例,从而确定该区域的回收价值。特别地,如果发现某个 Region 全是垃圾,则直接进行回收;
  4. 再次标记(Remark),类似于 CMS 的重新标记,但 G1 采用了更快的初始快照算法 SATB( Snapshot-at-the-beginning );
  5. 独占清理(Cleanup):计算各区域存活对象和 GC 回收比例并排序,识别可以混合回收的区域,为下阶段做铺垫,实际并没有回收垃圾;
  6. 并发清理:识别并清理完全空闲的区域。
G1 是通过 SATB 算法来解决漏标问题的,涉及到 pre-write barrier 和 satb_mark_queue ,具体可自行了解。
并发标记后,G1 还可以知道哪些类不再被使用了,从而卸载这些类。通过 -XX:ClassUnloadingWithConcurrentMark 参数启用(默认启用)。

37. 混合回收(MixedGC)过程(回收新生代和老年代,其中对老年代的回收,不是针对整个老年代,而是部分有价值回收的 O 区):


38. G1 的初衷就是避免 FullGC ,只有在以上 3 种方式都无法正常工作时才使用 FullGC 。FullGC 是单线程的,因此性能很差。FullGC 一般是由堆内存太小导致的,如:回收阶段(Evacuation)时没有足够的 to-space 存放晋升的对象、并发处理过程完成前空间耗尽。
JDK 9 对 G1 进行了很多的增强,G1 也成为了 JDK 9 的默认垃圾回收器。

39. 垃圾收集器的使用建议/调优建议:
  1. 优先调整堆的大小,让 JVM 自适应调整(如新生代、老年代大小);
  2. 根据不同需求(低延迟,高吞吐量等)选择最合适的垃圾收集器,比如,内存小、配置低的计算机使用串行收集器就最合适;
  3. 使用 G1 时,避免手动设置新生代大小(这会覆盖我们设定的暂停时间目标),而且设置的暂停时间目标不要过于严苛。

40. GC 调优相关(官方文档:Hotspot VM GC Tuning Guide):
  1. 最快的 GC 是不发生 GC。不要创建多余的对象,对象不要太臃肿,检查是否有内存泄漏。查数据库时需要什么,就只查询什么。
  2. 新生代的死亡对象的回收代价是零;官方建议新生代大小在堆内存的 25% 到 50% 之间。
  3. 理想的新生代应该可以容纳:单次请求响应产生的全部数据 * 并发量。
  4. Survivor 区应该大到能够保存所有当前活跃的对象和需要晋升的对象。
  5. 选择合适的晋升阈值,让长时间存活的对象及时晋升到老年代;-XX:+PrintTenuringDistribution 参数可以打印各年龄段的对象大小总和。
  6. 对于 CMS ,老年代越大越好;优先对新生代调优;用 -XX:CMSInitiatingOccupancyFraction 设置合适的阈值(75% 到 80% 较合适)。
如果 MinorGC 和 FullGC 频繁,则可能是新生代过小导致的;新生代过小导致 MinorGC 频繁,同时也会让一些生存周期短的对象晋升到老年代,从而又导致 FullGC 频繁。CMS 停顿时间长,则可能是因为重新标记阶段需要搜索的对象过多;此时可以在重新标记前先进行 YGC ,从而减少需要搜索的对象。JDK 8 之前,如果老年代空间充裕,但发生了 FullGC ,则是因为永久代空间不足。

41. ZGC:基于 Region 内存布局的,(暂时)不设分代的,使用读屏障、染色指针和内存多重映射等技术来实现可并发的标记 - 整理算法的,以低延迟为首要目标的一款垃圾收集器。ZGC 在工作时,除了初始标记需要 STW 停顿很短时间之外,几乎所有操作都是并发执行的。

42. 注意区分 Java 内存结构和 Java 内存模型 JMM ,JMM 定义了一套在多线程读写共享数据时,对原子性、可见性、有序性的规则和保障。

43. DCL:Double Check Lock,懒汉式单例线程安全化时就用了 DCL(两次判断是否为空);此外,使用线程安全的懒汉式单例时还要注意单例的引用声明时需要加 volatile 关键字:Java并发-懒汉式单例设计模式加volatile的原因

44. CAS 可能出现 ABA 问题,解决方式是使用版本号。

45. JVM 启动时会有一些默认的线程执行很多的 sync 代码,锁竞争比较频繁,因此偏向锁默认会在 JVM 启动后 4 秒才打开。
可以通过参数 -XX:BiasedLockingStartupDelay 来设置偏向锁在 JVM 启动多久后才打开。
在启动偏向锁功能后,新创建的对象默认处于偏向锁状态(即匿名偏向);锁膨胀的过程为:偏向锁 -> 轻量级锁 -> 重量级锁。
在调用了一个对象的 hashCode() 方法后,如果对该对象进行加锁,那么会直接用轻量级锁而不是偏向锁,因为使用偏向锁需要在对象的 MarkWord 中记录一些信息(主要是该锁偏向的那个线程的 ID ),导致该对象的哈希值没地方存放。轻量级锁不会导致哈希值的丢失,因为轻量级锁会将 LR 指针存放到锁对象的 MarkWord 中,而 LR 中记录了该对象的哈希值。
重量级锁对象的 MarkWord 存放在它对应的 Monitor 中,解锁时再将 Monitor 中的数据还原到锁对象中。
偏向锁在解锁后,其 MarkWord 依然是记录之前偏向的那个线程的 ID 而不是清除掉它。
撤销偏向锁的方式:调用锁的 hashCode() 方法,多个线程争夺该锁,调用锁的 wait()/notify() 方法(因为只有重量级锁有这些方法)。

46. LR:Lock Record ,用于记录锁对象在未被加锁时的 MarkWord ?

47. 批量重偏向:假设 MyLock 类有 lock0 到 lock25 这些对象(这些锁必须是同一个类的),线程 1 依次对这些对象进行加偏向锁和解锁,然后线程 2 再依次对这些对象进行加锁和解锁,结果发现 lock0 到 lock18 加锁后变成轻量级锁,解锁后变成不可偏向的,而 lock19 到 lock25(即从第 20 个开始,当然可以自己设置从第几个开始 在加锁时是直接偏向线程 2 而不是膨胀为轻量级锁。
JVM 发现有 19 个锁在被线程 1 获取后又被线程 2 获取,且线程 2 还要继续获取偏向于线程 1 的锁,因此直接让后面的锁直接偏向线程 2 。

48. 批量撤销:和批量重偏向类似,如果一个类的偏向锁被撤销了 39 次,那么再创建该类的实例时,新的实例默认就是不可偏向的。
注意这里说的批量重偏向和批量撤销可能有误,可以自行去详细了解。

49. 线程在执行同步代码块或同步方法时,都会到主内存中读取数据,而不是在工作内存中读取;这样的话,即使某个变量不是 volatile 的,本线程在执行同步代码时依旧能够及时得到该变量的最新值。System.out.println() 方法是同步方法,因此它打印的数据肯定是从主内存中获取到的。
全部评论

相关推荐

04-29 22:35
门头沟学院 Java
点赞 评论 收藏
分享
佛系的本杰明反对画饼:个人看法,实习经历那段是败笔,可以删掉,它和你目标岗位没什么关系,没有用到什么专业技能,甚至会降低你项目经历内容的可信度。个人技能那里可以再多写一点,去boss直聘上看别人写的岗位要求,可以把你会的整合一下,比如熟悉常规的开关电源拓扑结构(BUCK、正激、反激、LLC等),熟悉常用的通信总线协议和通信接口,如UART,IIC,SPI等。简历首先是HR看的,HR大多不懂技术,会从简历里去找关键字,你没有那些关键字他可能就把你筛掉了,所以个人技能尽量针对着岗位描述写一下。还有电赛获佳绩,获奖了就写什么奖,没获奖就把获佳绩删了吧,要不会让人感觉夸大。
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务