Jvm垃圾采集器和垃圾采集算法
优采云 发布时间: 2020-08-07 09:11概述:
目前,内存的动态分配和内存恢复技术已经相当成熟. 一切似乎都进入了“自动化”时代. 为什么我们需要了解GC和内存分配?原因很简单: 当有必要解决各种内存泄漏和内存溢出问题时,当垃圾采集器成为系统实现更高并发性的瓶颈时,我们需要对这些“自动化”技术进行必要的监视和调整.
上一个博客讨论了Java虚拟机的运行时内存的各个部分. 其中,程序计数器,虚拟机堆栈和本地方法堆栈由线程生成,并由线程终止. 堆栈中的堆栈框架遵循该方法. 进入和退出执行推入和弹出操作,因此当方法结束或线程结束时,内存自然会跟随恢复. Java堆与方法块不同. 我们只知道在运行时将创建哪些对象. 这部分内存和恢复是动态的,垃圾回收器也与此部分内存有关. 稍后提到的内存分配和恢复也仅涉及内存的这一部分.
1. 判断物体的生与死1.参考计数方法
许多教科书或开发人员回答了以下问题: “如何判断对象是否还活着?”大多数答案都是引用计数. 实现是这样的: 向该对象添加一个引用计数器;在某个地方被引用时,将添加+1;在引用变为无效时,将添加-1;并且在任何时候其计数器为0的对象将无法被引用.
客观地讲,引用计数方法易于实现并且判断效率很高,但是至少主流的Java虚拟机不使用引用计数方法来管理内存. 主要原因是很难解决对象之间的相互循环. 引用的问题.
2. 可达性分析算法
“可达性分析”是Java和C#当前的主流实现,用于确定对象是否存在. 主要思想是使用一些类作为成为“ GC根目录”的起点的对象,并从这些节点向下搜索. 搜索的路径称为参考链. 如果没有针对GC根目录的对象的引用链,则证明该对象不可用.
如下所示:
Object1-Object4的参考链连接到GC根,而Object5-Oject7没有通过参考链连接,因此被判断为可回收对象
在Java中,有四种类型可以用作GC根节点
3. 再谈报价
在JDK1.2之前,Java中的引用定义为: 如果将引用类型的数据存储为代表另一个内存块的起始地址的值,则可以说该内存块表示一个引用. 在JDK1.2之后,Java扩展了引用的概念,分为以下四种类型:
4. 生存还是死亡
即使在可达性分析算法中无法达到的对象也不是“必须死亡”的. 目前,他们处于“试用”阶段. 要声明一个对象已死亡,至少需要两个标记过程.
以下代码一次显示对象的自救:
1 //代码演示了两点
2 //1.对象可以实现自救
3 //2.这种自救只能一次,因为finalize方法只会被执行一次
4
5 public class jvmtest1 {
6 public static jvmtest1 obj = null;
7
8 public void isAlive() {
9 System.out.println("I am alive");
10 }
11
12 @Override
13 protected void finalize() throws Throwable {
14 super.finalize();
15 System.out.println("finalize method executed!");
16 obj = this;
17 }
18
19 public static void main(String[] args) throws Exception {
20 obj = new jvmtest1();
21
22 obj = null;
23 System.gc();
24 // 因为finalize方法优先级很低,所以暂停0.5s执行
25 Thread.sleep(500);
26 if (obj != null) {
27 obj.isAlive();
28 } else {
29 System.out.println("I am dead");
30 }
31
32 // 与上面的代码完全相同,但是自救失败了
33 obj = null;
34 System.gc();
35 // 因为finalize方法优先级很低,所以暂停0.5s执行
36 Thread.sleep(500);
37 if (obj != null) {
38 obj.isAlive();
39 } else {
40 System.out.println("I am dead");
41 }
42 }
43 }
程序输出为:
可以看出obj的finalize()方法确实是由GC触发的,并且成功地保存了一次. 应该注意的是,下部的代码与上部的代码相同,但是自助失败. 这是因为对象的finalize()方法只能被系统自动调用一次. 如果对象面临第二次恢复,则不会调用其finalize(The)方法,因此第二次自助操作将失败.
5. 恢复方法区域
方法区域中的垃圾回收具有“成本效益”. 在堆中,特别是在新一代中,常规应用程序的垃圾回收通常可以回收70%-95%的空间,而方法区域(在HatSport虚拟机中)永久生成的恢复效率远远低于这个.
永久代中有两种主要的垃圾采集类型: 过时的常量和无用的类
(1)此类的所有实例均已回收,也就是说,堆中没有此类的实例
(2)加载了此类的ClassLoader已被回收
(3)与此类对应的java.lang.Class对象在任何地方都没有引用,并且该类的方法无法在任何地方通过反射来访问.
满足以上三个条件的类也可以被回收,但不一定. 在经常使用ByteCode框架(例如反射,动态代理,CGLib等)并且经常定义ClassLoader(例如动态生成的JSP和OSGi)的场景中,虚拟机需要类卸载功能,以确保永久生成不会溢出.
2. 垃圾采集算法1.打标算法
标记扫描算法是最基本的采集算法. 首先标记需要回收的对象,并在标记完成后统一采集所有标记的对象.
主要缺点有两个:
过程如下:
2. 复制算法
为了解决效率问题,出现了复制算法(Copying),他将内存分为相等大小的两块,一次只使用其中之一. 当该内存块用完时,会将尚存的对象复制到另一个块,然后清除该空间块. 这样,每次都可以在整个半个区域中回收内存,并且无需考虑在内存分配过程中内存碎片的复杂性. 您只需要移动指针并按顺序分配内存即可.
这种方法的代价是将内存分成两半,这太高了.
过程如下:
当前的商用虚拟机都使用此方法来回收新一代产品. IBM的特殊研究表明,新一代对象中有98%是“生死攸关的”,因此无需划分1: 1内存空间. 另一种方法是将内存块划分为Eden空间(80%)和两个Survivor空间(10%). 每次使用Eden空间和一个Survivor空间时,在回收过程中会将幸存的对象复制到另一个Survivor空间. ,因此每次仅“浪费”了10%的空间. 当Survivor空间不足时,它需要依赖其他内存(旧代)来进行分配保证(与旧代中的分配保证机制相同).
3. 标记排序算法
当副本采集算法的对象存活率较高时,它将执行更多的复制操作,并且效率会降低. 更重要的是,如果您不希望浪费50%的空间,则需要有额外的空间用于分配保证,以应对极端情况,即已用内存中的所有对象都处于100%活动状态,因此旧一代通常无法选择这种方法. 提出了Mark-Compact算法(Mark-Compact).
标记过程与“ mark-sweep”算法相同,但是后续步骤不是直接清理可回收部分,而是将所有活动对象移到一端,然后直接清理内存在末尾.
过程如下:
4. 分代采集算法
当前的商用虚拟机都使用Generational 采集算法. 通常,堆分为新一代和旧一代,并且根据每个时代的不同特性使用最合适的算法. 新一代一般采用复制算法,只需要支付少量复制幸存对象的费用即可. 在旧的一代中,由于对象生存率很高并且没有多余的空间来分配它,因此必须使用“标记清除”或“标记组织”算法来进行恢复.
概述:
目前,内存的动态分配和内存恢复技术已经相当成熟. 一切似乎都进入了“自动化”时代. 为什么我们需要了解GC和内存分配?原因很简单: 当有必要解决各种内存泄漏和内存溢出问题时,当垃圾采集器成为系统实现更高并发性的瓶颈时,我们需要对这些“自动化”技术进行必要的监视和调整.
上一个博客讨论了Java虚拟机的运行时内存的各个部分. 其中,程序计数器,虚拟机堆栈和本地方法堆栈由线程生成,并由线程终止. 堆栈中的堆栈框架遵循该方法. 进入和退出执行推入和弹出操作,因此当方法结束或线程结束时,内存自然会跟随恢复. Java堆与方法块不同. 我们只知道在运行时将创建哪些对象. 这部分内存和恢复是动态的,垃圾回收器也与此部分内存有关. 稍后提到的内存分配和恢复也仅涉及内存的这一部分.
1. 判断物体的生与死1.参考计数方法
许多教科书或开发人员回答了以下问题: “如何判断对象是否还活着?”大多数答案都是引用计数. 实现是这样的: 向该对象添加一个引用计数器;在某个地方被引用时,将添加+1;在引用变为无效时,将添加-1;并且在任何时候其计数器为0的对象将无法被引用.
客观地讲,引用计数方法易于实现并且判断效率很高,但是至少主流的Java虚拟机不使用引用计数方法来管理内存. 主要原因是很难解决对象之间的相互循环. 引用的问题.
2. 可达性分析算法
“可达性分析”是Java和C#当前的主流实现,用于确定对象是否存在. 主要思想是使用一些类作为成为“ GC根目录”的起点的对象,并从这些节点向下搜索. 搜索的路径称为参考链. 如果没有针对GC根目录的对象的引用链,则证明该对象不可用.
如下所示:
Object1-Object4的参考链连接到GC根,而Object5-Oject7没有通过参考链连接,因此被判断为可回收对象
在Java中,有四种类型可以用作GC根节点
3. 再谈报价
在JDK1.2之前,Java中的引用定义为: 如果将引用类型的数据存储为代表另一个内存块的起始地址的值,则可以说该内存块表示一个引用. 在JDK1.2之后,Java扩展了引用的概念,分为以下四种类型:
4. 生存还是死亡
即使在可达性分析算法中无法达到的对象也不是“必须死亡”的. 目前,他们处于“试用”阶段. 要声明一个对象已死亡,至少需要两个标记过程.
以下代码一次显示对象的自救:
1 //代码演示了两点
2 //1.对象可以实现自救
3 //2.这种自救只能一次,因为finalize方法只会被执行一次
4
5 public class jvmtest1 {
6 public static jvmtest1 obj = null;
7
8 public void isAlive() {
9 System.out.println("I am alive");
10 }
11
12 @Override
13 protected void finalize() throws Throwable {
14 super.finalize();
15 System.out.println("finalize method executed!");
16 obj = this;
17 }
18
19 public static void main(String[] args) throws Exception {
20 obj = new jvmtest1();
21
22 obj = null;
23 System.gc();
24 // 因为finalize方法优先级很低,所以暂停0.5s执行
25 Thread.sleep(500);
26 if (obj != null) {
27 obj.isAlive();
28 } else {
29 System.out.println("I am dead");
30 }
31
32 // 与上面的代码完全相同,但是自救失败了
33 obj = null;
34 System.gc();
35 // 因为finalize方法优先级很低,所以暂停0.5s执行
36 Thread.sleep(500);
37 if (obj != null) {
38 obj.isAlive();
39 } else {
40 System.out.println("I am dead");
41 }
42 }
43 }
程序输出为:
可以看出obj的finalize()方法确实是由GC触发的,并且成功地保存了一次. 应该注意的是,下部的代码与上部的代码相同,但是自助失败. 这是因为对象的finalize()方法只能被系统自动调用一次. 如果对象面临第二次恢复,则不会调用其finalize(The)方法,因此第二次自助操作将失败.
5. 恢复方法区域
方法区域中的垃圾回收具有“成本效益”. 在堆中,特别是在新一代中,常规应用程序的垃圾回收通常可以回收70%-95%的空间,而方法区域(在HatSport虚拟机中)永久生成的恢复效率远远低于这个.
永久代中有两种主要的垃圾采集类型: 过时的常量和无用的类
(1)此类的所有实例均已回收,也就是说,堆中没有此类的实例
(2)加载了此类的ClassLoader已被回收
(3)与此类对应的java.lang.Class对象在任何地方都没有引用,并且该类的方法无法在任何地方通过反射来访问.
满足以上三个条件的类也可以被回收,但不一定. 在经常使用ByteCode框架(例如反射,动态代理,CGLib等)并且经常定义ClassLoader(例如动态生成的JSP和OSGi)的场景中,虚拟机需要类卸载功能,以确保永久生成不会溢出.
2. 垃圾采集算法1.打标算法
标记扫描算法是最基本的采集算法. 首先标记需要回收的对象,并在标记完成后统一采集所有标记的对象.
主要缺点有两个:
过程如下:
2. 复制算法
为了解决效率问题,出现了复制算法(Copying),他将内存分为相等大小的两块,一次只使用其中之一. 当该内存块用完时,会将尚存的对象复制到另一个块,然后清除该空间块. 这样,每次都可以在整个半个区域中回收内存,并且无需考虑在内存分配过程中内存碎片的复杂性. 您只需要移动指针并按顺序分配内存即可.
这种方法的代价是将内存分成两半,这太高了.
过程如下:
当前的商用虚拟机都使用此方法来回收新一代产品. IBM的特殊研究表明,新一代对象中有98%是“生死攸关的”,因此无需划分1: 1内存空间. 另一种方法是将内存块划分为Eden空间(80%)和两个Survivor空间(10%). 每次使用Eden空间和一个Survivor空间时,在回收过程中会将幸存的对象复制到另一个Survivor空间. ,因此每次仅“浪费”了10%的空间. 当Survivor空间不足时,它需要依赖其他内存(旧代)来进行分配保证(与旧代中的分配保证机制相同).
3. 标记排序算法
当副本采集算法的对象存活率较高时,它将执行更多的复制操作,并且效率会降低. 更重要的是,如果您不希望浪费50%的空间,则需要有额外的空间用于分配保证,以应对极端情况,即已用内存中的所有对象都处于100%活动状态,因此旧一代通常无法选择这种方法. 提出了Mark-Compact算法(Mark-Compact).
标记过程与“标记清除”算法相同,但是后续步骤不是直接清理可回收部分,而是将所有活动对象移动到一端,然后直接清理内存在末尾.
过程如下:
4. 分代采集算法
当前的商用虚拟机都使用Generational 采集算法. 通常,堆分为新一代和旧一代,并且根据每个时代的不同特性使用最合适的算法. 新一代一般采用复制算法,只需要支付少量复制幸存对象的费用即可. 在旧的一代中,由于对象生存率很高并且没有多余的空间来分配它,因此必须使用“标记清除”或“标记组织”算法来进行恢复.