diff --git a/blogs/Java/口水话/JVM 相关口水话.md b/blogs/Java/口水话/JVM 相关口水话.md index 05f197e..077ca55 100644 --- a/blogs/Java/口水话/JVM 相关口水话.md +++ b/blogs/Java/口水话/JVM 相关口水话.md @@ -9,7 +9,7 @@ JVM 相关口水话 5. GC 1. 引用计数及可达性分析 2. 垃圾回收算法 - 3. 垃圾收集器 + 3. G1 及 ZGC 6. 类加载机制 7. 双亲委派模型 8. 编译器优化 @@ -66,7 +66,53 @@ Full GC/Major GC 指发生在老年代的 GC,出现 Full GC 经常会伴随着 最后是对象的访问定位,Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象,由于 reference 类型在 Java 虚拟机规范中只规定了一个指向对象的引用,并没有规定这个引用应该通过什么方式去定位和访问堆中的对象,所以对象访问方式也是取决于虚拟机实现而定。目前主流的方式有使用句柄和直接指针两种。使用句柄,就是相当于加了一个中间层,在对象移动时只会改变句柄中的实例数据的指针,reference 本身不需要改变。HotSpot 使用的是第二种,使用直接指针的方式访问的最大好处就是速度很快。 -#### 类加载机制和双亲委派模型 +#### GC + +在垃圾收集器回收对象时,先要判断对象是否已经不再使用了,有引用计数法和可达性分析两种。 + +##### 引用计数及可达性分析 + +引用计数法就是给对象添加一个引用计数器,每当有一个地方引用时就加一,引用失效时就减一。引用计数实现简单,判断效率也很高,但是 JVM 并没有采用引用计数来管理内存,其中最主要的原因使它很难解决对象之间的相互循环引用问题。可达性分析的思路是通过一系列称为 GC Roots 的对象作为起始点,从这些起始点出发向下搜索,当有一个对象到 GC Roots 没有任何引用链时,即不可达,则说明此对象是不可用的。在 Java 中,可作为 GC Roots 的对象有虚拟机栈和本地方法栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象等。 + +但这也并不是说引用计数一无是处,在 Android 的 Framework Native 层用的智能指针。智能指针就是一种能够自动维护对象引用计数的技术,它是一个对象而不是一个指针,但是它引用了一个实际使用的对象。简单来说,就是在智能指针构造时,增加它所引用的对象的引用计数;而在智能指针析构时,就减少它所引用对象的引用对象。但是它是怎样解决相互引用问题的呢?其实是通过强弱引用来实现,也就是将对象的引用计数分为强引用计数和弱引用计数两种,其中,对象的生命周期只受强引用计数控制。比如在解决对象 A 和 B 相互引用时,把 A 看成父 B 看成子,对象 A 通过强引用计数来引用 B,B 通过若引用计数来引用 A。在 A 不再使用时,由于 B 是通过弱引用来引用它的,因此 A 的生命周期是不受 B 影响的,所以 A 可以安全的释放,在释放 A 时,同时也会释放它对 B 的强引用计数,这是 B 也可以被安全的回收了。在 Android 中,是使用 sp 来表示强引用,wp 表示弱引用。 + +Java 中的引用可以分为四类,强引用、软引用、弱引用和虚引用。强引用在程序中普遍存在,类似 new 的这种操作,只要有强引用存在,即使 OOM JVM 也不会回收该对象。软引用是在内存不够用时,才回去回收,JDK 提供了 SoftReference 类来实现软引用。弱引用是在 GC 时不管内存够不够用会去回收的,可以使用 WeakReference 类来实现弱引用。虚引用对对象的生命周期没有影响,只是为了能在对象回收时收到一个系统通知,可以使用 PhantomReference 类来实现虚引用。 + +接下来就是要将垃圾回收算法了。 + +##### 垃圾回收算法 + +垃圾回收算法主要有标记清除、复制算法、标记整理。标记清除是先通过 GC Roots 标记所存活的对象,然后再统一清除未被标记的对象,它的主要问题是会产生内存碎片。老年代使用的 CMS 收集器就是基于标记清除算法。复制算法是把内存空间划分为两块,每次分配对象只在一块内存上进行分配,在这一块内存使用完时,就直接把存活的对象复制到另外一块上,然后把已使用的那块空间一次清理掉,但是这种算法的代价就是内存的使用量缩小了一半。现代虚拟机都采用复制算法回收新生代,不过是把内存划分为了一个 Eden 区和两个 Survivor 区,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor 区,也就是只有 10% 的内存会浪费掉。如果 Survivor 空间不够用,需要依赖其他内存比如老年代进行分配担保。复制算法在对象存活率比较高时效率是比较低下的,所以老年代一般不使用复制算法。标记整理算法即是在标记清除之后,把所有存活的对象都向一段移动,然后清理掉边界以外的内存区域。 + +最后就是将垃圾回收算法的具体应用了,也就是垃圾收集器。 + +##### G1 及 ZGC + +Garbage First(G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。它和 CMS 同样是一款主要面向服务端应用的垃圾收集器,不过在 JDK9 之后,CMS 就被标记为废弃了。在 G1 收集器出现之前的所有其他收集器,包括 CMS 在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),在要么就是整个 Java 堆(Full GC)。而 G1 是基于 Region 堆内存布局,虽然 G1 也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间或者老年代。收集器根据 Region 的不同角色采用不同的策略去处理。G1 会根据用户设定允许的收集停顿时间去优先处理回收价值收益最大的那些 Region 区,也就是垃圾最大的 Region 区,这就是 Garbage First 名字的由来。 + +G1 收集器的运作过程大致可划分为以下四个步骤: + +1.初始标记 + +仅仅只是标记一下 GC Roots 能直接关联到的对象,这个阶段需要停顿线程,但耗时很短。 + +2.并发标记 + +从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但是可与用户程序并发执行。 + +3.最终标记 + +对用户线程做另一个短暂的暂停,用于处理在并发标记阶段新产生的对象引用链变化。 + +4.筛选回收 + +负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。 + +G1 的目标是在可控的停顿时间内完成垃圾回收,所以进行了分区设计,但是 G1 也存在一些问题,比如停顿时间过长,通常 G1 的停顿时间在几十到几百毫秒之间,虽然这个数字其实已经非常小了,但是在用户体验有较高要求的情况下还是不能满足实际需求,而且 G1 支持的内存空间有限,不适用于超大内存的系统,特别是在内存容量高于 100GB 的系统上,会因内存过大而导致停顿时间增长。 + +ZGC 作为新一代的垃圾回收器,在设计之初就定义了三大目标:支持 TB 级内存,停顿时间控制在 10ms 之内,对程序吞吐量影响小于 15%。 + +#### 类加载机制 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java 对象,这就是虚拟机的类加载机制。 @@ -90,16 +136,5 @@ Full GC/Major GC 指发生在老年代的 GC,出现 Full GC 经常会伴随着 双亲委派模型的实现相对简单,代码都集中在 ClassLoader 的 loadClass 方法中先检查是否已经被加载过了,如果没加载则先调用父加载器的 loadClass 方法,若父加载器为空则使用默认的启动类加载器作为父加载器。如果父加载器加载失败,抛出 ClassNotFoundException 异常,然后调用自己的 findClass 方法进行加载。 -#### GC - -在垃圾收集器回收对象时,先要判断对象是否已经不再使用了,有引用计数法和可达性分析两种。 - -引用计数法就是给对象添加一个引用计数器,每当有一个地方引用时就加一,引用失效时就减一。引用计数实现简单,判断效率也很高,但是 JVM 并没有采用引用计数来管理内存,其中最主要的原因使它很难解决对象之间的相互循环引用问题。可达性分析的思路是通过一系列称为 GC Roots 的对象作为起始点,从这些起始点出发向下搜索,当有一个对象到 GC Roots 没有任何引用链时,即不可达,则说明此对象是不可用的。在 Java 中,可作为 GC Roots 的对象有虚拟机栈和本地方法栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象等。 - -但这也并不是说引用计数一无是处,在 Android 的 Framework Native 层用的智能指针。智能指针就是一种能够自动维护对象引用计数的技术,它是一个对象而不是一个指针,但是它引用了一个实际使用的对象。简单来说,就是在智能指针构造时,增加它所引用的对象的引用计数;而在智能指针析构时,就减少它所引用对象的引用对象。但是它是怎样解决相互引用问题的呢?其实是通过强弱引用来实现,也就是将对象的引用计数分为强引用计数和弱引用计数两种,其中,对象的生命周期只受强引用计数控制。比如在解决对象 A 和 B 相互引用时,把 A 看成父 B 看成子,对象 A 通过强引用计数来引用 B,B 通过若引用计数来引用 A。在 A 不再使用时,由于 B 是通过弱引用来引用它的,因此 A 的生命周期是不受 B 影响的,所以 A 可以安全的释放,在释放 A 时,同时也会释放它对 B 的强引用计数,这是 B 也可以被安全的回收了。在 Android 中,是使用 sp 来表示强引用,wp 表示弱引用。 - -Java 中的引用可以分为四类,强引用、软引用、弱引用和虚引用。强引用在程序中普遍存在,类似 new 的这种操作,只要有强引用存在,即使 OOM JVM 也不会回收该对象。软引用是在内存不够用时,才回去回收,JDK 提供了 SoftReference 类来实现软引用。弱引用是在 GC 时不管内存够不够用会去回收的,可以使用 WeakReference 类来实现弱引用。虚引用对对象的生命周期没有影响,只是为了能在对象回收时收到一个系统通知,可以使用 PhantomReference 类来实现虚引用。 - -接下来就是要将垃圾回收算法了。 +#### 编译器优化 -垃圾回收算法主要有标记清除、复制算法、标记整理。标记清除是先通过 GC Roots 标记所存活的对象,然后再统一清除未被标记的对象,它的主要问题是会产生内存碎片。老年代使用的 CMS 收集器就是基于标记清除算法。复制算法是把内存空间划分为两块,每次分配对象只在一块内存上进行分配,在这一块内存使用完时,就直接把存活的对象复制到另外一块上,然后把已使用的那块空间一次清理掉,但是这种算法的代价就是内存的使用量缩小了一半。现代虚拟机都采用复制算法回收新生代,不过是把内存划分为了一个 Eden 区和两个 Survivor 区,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor 区,也就是只有 10% 的内存会浪费掉。如果 Survivor 空间不够用,需要依赖其他内存比如老年代进行分配担保。复制算法在对象存活率比较高时效率是比较低下的,所以老年代一般不使用复制算法。标记整理算法即是在标记清除之后,把所有存活的对象都向一段移动,然后清理掉边界以外的内存区域。 \ No newline at end of file