From 0257a01f3213bcfc5f27e05dd25951e776bb1619 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 29 Dec 2021 20:58:12 +0800 Subject: [PATCH] =?UTF-8?q?[docs=20uodate]=E6=B7=BB=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E5=AF=B9=20finalize=20=E6=96=B9=E6=B3=95=E7=9A=84=E7=AE=80?= =?UTF-8?q?=E5=8D=95=E4=BB=8B=E7=BB=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/jvm/jvm-garbage-collection.md | 88 ++++++++++++++----------- docs/java/jvm/jvm-intro.md | 2 +- 2 files changed, 51 insertions(+), 39 deletions(-) diff --git a/docs/java/jvm/jvm-garbage-collection.md b/docs/java/jvm/jvm-garbage-collection.md index 058b2794..cb159e6f 100644 --- a/docs/java/jvm/jvm-garbage-collection.md +++ b/docs/java/jvm/jvm-garbage-collection.md @@ -40,7 +40,7 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(G 上图所示的 Eden 区、From Survivor0("From") 区、To Survivor1("To") 区都属于新生代,Old Memory 区属于老年代。 -大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为大于 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置默认值,这个值会在虚拟机运行过程中进行调整,可以通过`-XX:+PrintTenuringDistribution`来打印出当次GC后的Threshold。 +大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为大于 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置默认值,这个值会在虚拟机运行过程中进行调整,可以通过`-XX:+PrintTenuringDistribution`来打印出当次 GC 后的 Threshold。 > **🐛 修正(参见:[issue552](https://github.com/Snailclimb/JavaGuide/issues/552))**:“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。 > @@ -48,29 +48,30 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(G > > ```c++ > uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { -> //survivor_capacity是survivor空间的大小 -> size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100); -> size_t total = 0; -> uint age = 1; -> while (age < table_size) { -> //sizes数组是每个年龄段对象大小 -> total += sizes[age]; -> if (total > desired_survivor_size) { -> break; -> } -> age++; -> } -> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; -> ... +> //survivor_capacity是survivor空间的大小 +> size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100); +> size_t total = 0; +> uint age = 1; +> while (age < table_size) { +> //sizes数组是每个年龄段对象大小 +> total += sizes[age]; +> if (total > desired_survivor_size) { +> break; +> } +> age++; +> } +> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; +> ... > } > > ``` -经过这次 GC 后,Eden 区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To"。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,在这个过程中,有可能当次Minor GC后,Survivor 的"From"区域空间不够用,有一些还达不到进入老年代条件的实例放不下,则放不下的部分会提前进入老年代。 +经过这次 GC 后,Eden 区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To"。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,在这个过程中,有可能当次 Minor GC 后,Survivor 的"From"区域空间不够用,有一些还达不到进入老年代条件的实例放不下,则放不下的部分会提前进入老年代。 接下来我们提供一个调试脚本来测试这个过程。 **调试代码参数如下** + ``` -verbose:gc -Xmx200M @@ -85,7 +86,9 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(G -XX:+UseConcMarkSweepGC -XX:+UseParNewGC ``` + **示例代码如下:** + ```java /* * 本实例用于java GC以后,新生代survivor区域的变化,以及晋升到老年代的时间和方式的测试代码。需要自行分步注释不需要的代码进行反复测试对比 @@ -132,15 +135,16 @@ public class JavaGcTest { ``` -注意:如下输出结果中老年代的信息为 `concurrent mark-sweep generation` 和以前版本略有不同。另外,还列出了某次GC后是否重新生成了threshold,以及各个年龄占用空间的大小。 +注意:如下输出结果中老年代的信息为 `concurrent mark-sweep generation` 和以前版本略有不同。另外,还列出了某次 GC 后是否重新生成了 threshold,以及各个年龄占用空间的大小。 + ```bash 2021-07-01T10:41:32.257+0800: [GC (Allocation Failure) 2021-07-01T10:41:32.257+0800: [ParNew Desired survivor size 3145728 bytes, new threshold 1 (max 3) - age 1: 3739264 bytes, 3739264 total -: 40345K->3674K(46080K), 0.0014584 secs] 40345K->3674K(199680K), 0.0015063 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] +: 40345K->3674K(46080K), 0.0014584 secs] 40345K->3674K(199680K), 0.0015063 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2021-07-01T10:41:32.259+0800: [GC (Allocation Failure) 2021-07-01T10:41:32.259+0800: [ParNew Desired survivor size 3145728 bytes, new threshold 3 (max 3) -: 13914K->0K(46080K), 0.0046596 secs] 13914K->13895K(199680K), 0.0046873 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] +: 13914K->0K(46080K), 0.0046596 secs] 13914K->13895K(199680K), 0.0046873 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap par new generation total 46080K, used 35225K [0x05000000, 0x08200000, 0x08200000) eden space 40960K, 86% used [0x05000000, 0x072667f0, 0x07800000) @@ -150,6 +154,7 @@ Heap Metaspace used 153K, capacity 2280K, committed 2368K, reserved 4480K ``` + ![堆内存常见分配策略 ](./pictures/jvm垃圾回收/堆内存.png) ### 1.1 对象优先在 eden 区分配 @@ -226,7 +231,7 @@ public class GCTest { > 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的 50% 时(默认值是 50%,可以通过 `-XX:TargetSurvivorRatio=percent` 来设置,参见 [issue1199](https://github.com/Snailclimb/JavaGuide/issues/1199) ),取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。 > -> jdk8官方文档引用 :https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 。 +> jdk8 官方文档引用 :https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 。 > > ![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/image-20210523201742303.png) > @@ -234,20 +239,20 @@ public class GCTest { > > ```c++ > uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { -> //survivor_capacity是survivor空间的大小 -> size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100); -> size_t total = 0; -> uint age = 1; -> while (age < table_size) { -> //sizes数组是每个年龄段对象大小 -> total += sizes[age]; -> if (total > desired_survivor_size) { -> break; -> } -> age++; -> } -> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; -> ... +> //survivor_capacity是survivor空间的大小 +> size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100); +> size_t total = 0; +> uint age = 1; +> while (age < table_size) { +> //sizes数组是每个年龄段对象大小 +> total += sizes[age]; +> if (total > desired_survivor_size) { +> break; +> } +> age++; +> } +> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; +> ... > } > > ``` @@ -283,11 +288,11 @@ public class GCTest { 空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。 -《深入理解Java虚拟机》第三章对于空间分配担保的描述如下: +《深入理解 Java 虚拟机》第三章对于空间分配担保的描述如下: -> JDK 6 Update 24 之前,在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次 Minor GC 可以确保是安全的。如果不成立,则虚拟机会先查看 `-XX:HandlePromotionFailure` 参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 `-XX: HandlePromotionFailure` 设置不允许冒险,那这时就要改为进行一次 Full GC。 +> JDK 6 Update 24 之前,在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次 Minor GC 可以确保是安全的。如果不成立,则虚拟机会先查看 `-XX:HandlePromotionFailure` 参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 `-XX: HandlePromotionFailure` 设置不允许冒险,那这时就要改为进行一次 Full GC。 > -> JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。 +> JDK 6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。 ## 2 对象已经死亡? @@ -366,10 +371,17 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 ### 2.4 不可达的对象并非“非死不可” -即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 +即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 `finalize` 方法。当对象没有覆盖 `finalize` 方法,或 `finalize` 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。 +> `Object` 类中的 `finalize` 方法一直被认为是一个糟糕的设计,成为了 Java 语言的负担,影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 `finalize` 方法会被逐渐弃用移除。忘掉它的存在吧! +> +> 参考: +> +> - [JEP 421: Deprecate Finalization for Removal](https://openjdk.java.net/jeps/421) +> - [是时候忘掉 finalize 方法了](https://mp.weixin.qq.com/s/LW-paZAMD08DP_3-XCUxmg) + ### 2.5 如何判断一个常量是废弃常量? 运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢? diff --git a/docs/java/jvm/jvm-intro.md b/docs/java/jvm/jvm-intro.md index 87cd046a..d89ab731 100644 --- a/docs/java/jvm/jvm-intro.md +++ b/docs/java/jvm/jvm-intro.md @@ -246,7 +246,7 @@ JVM内存会划分为堆内存和非堆内存,堆内存中也会划分为**年 finalize()是Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用。 -补充一句:并不提倡在程序中调用finalize()来进行自救。建议忘掉Java程序中该方法的存在。因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用)。在Java9中已经被标记为 **deprecated** ,且java.lang.ref.Cleaner(也就是强、软、弱、幻象引用的那一套)中已经逐步替换掉它,会比finalize来的更加的轻量及可靠。 +补充一句:并不提倡在程序中调用finalize()来进行自救。建议忘掉Java程序中该方法的存在。因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用)。在Java9中已经被标记为 **deprecated** ,且 `java.lang.ref.Cleaner`(也就是强、软、弱、幻象引用的那一套)中已经逐步替换掉它,会比 `finalize` 来的更加的轻量及可靠。    ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/c807dab33f8b42329c1910d609e7ed21-new-image565aeab2-6d3e-4c2c-80f6-7a7b0f629fda.png)