之前学习java时,笔者看到很多学习资料说,gc判断object存活与否的算法是:给对象添加一个引用计数器,每当有一处地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,当对象计数清零时,对象就会被gc回收。但等笔者开始学习jvm虚拟机后,才明白实际上gc并不是用这种算法实现的,理由如下:

package gc;

public class ReferenceCountingGC {
public Object instance = null;
private static final int occupy_1MB=*;
private byte[] bigSize = new byte[*occupy_1MB]; public static void testGC(){
ReferenceCountingGC obj1=new ReferenceCountingGC();
ReferenceCountingGC obj2=new ReferenceCountingGC();
obj1.instance=obj2;
obj2.instance=obj1; obj1=null;
obj2=null; System.gc();
} public static void main(String [] args){
ReferenceCountingGC.testGC();
}
}
[GC (System.gc()) [PSYoungGen: 10854K->480K(38400K)] 10854K->488K(125952K), 0.0014556 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 480K->0K(38400K)] [ParOldGen: 8K->402K(87552K)] 488K->402K(125952K), [Metaspace: 3072K->3072K(1056768K)], 0.0125161 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
eden space 33280K, % used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000)
from space 5120K, % used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
to space 5120K, % used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
ParOldGen total 87552K, used 402K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
object space 87552K, % used [0x0000000740000000,0x00000007400648c8,0x0000000745580000)
Metaspace used 3079K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 339K, capacity 388K, committed 512K, reserved 1048576K

可以看到,obj1和obj2互相引用,但他们还是被回收机制给回收了,可见gc底层并不是用引用计数算法实现的。

  下面介绍gc底层实现所采用的算法,可达性分析算法(Reachability Analysis),通过一系列被称为“GC Roots”的对象节点作为起始点,当GC root无法到达一个对象时,就可以判断此对象不可用,从而判断此对象可以被回收。

  GC Roots对象包括下面几种:

  1.虚拟机栈(栈帧中的本地变量表)中引用的对象。

  2.方法区中类静态属性引用的对象。

  3.方法区中常量引用的对象。

  4.本地方法栈中JNI(即一般说的Native方法)引用的对象。

  在判断对象可以被回收后,待回收的对象并不会立刻被gc回收掉,而是会进入一个名为F-queue的队列,在这里,对象有最后一次拯救自己的机会(当然也可能没有,这不好说,后面会解释),虚拟机会自动建立一个FInalize线程(此线程优先级很低),线程会调用finaliz()方法去执行F-queue中所有的对象,如果对象成功与引用链建立上联系,那么就可以免于死亡的命运。下面我写一段示意代码,用this关键字将finaliz()方法引用到对象中,使其建立联系。

package gc;

public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOCK=null; public void isAlive(){
System.out.println("I am alive XD");
} @Override
protected void finalize() throws Throwable{
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOCK=this;
} public static void main(String [] args)throws Throwable{
SAVE_HOCK=new FinalizeEscapeGC(); SAVE_HOCK=null;
System.gc();
//因为Finalize线程的优先级很低,因此笔者将线程挂起1s,等待Finalize线程的启动。
Thread.sleep(1000);
if (SAVE_HOCK!=null){
SAVE_HOCK.isAlive();
}else{
System.out.println("i am dead");
} SAVE_HOCK=null;
System.gc();
Thread.sleep(1000);
if (SAVE_HOCK!=null){
SAVE_HOCK.isAlive();
}else{
System.out.println("i am dead");
}
}
}
finalize method executed!
I am alive XD
i am dead

  可以看到,我们第一次手动启动gc后,对象依旧存活,那为什么一样的代码,第二次对象就死亡了呢?这是因为同一个对象只会执行一次finalize()方法,当第二次面临gc时,就会直接被回收掉,这是从优化内存效率的角度而作出的设计。

  虽然上面我们成功拯救了对象,但在实际情况下,我们不建议这么做。首先,有太多办法阻止对象被回收了,比如合理的使用try{};catch{},根本不需要在回收阶段再去解决这个问题。其次,Finalize线程的优先级很低,在示例代码中,我们是将线程挂起了1s来等待Finalize线程被开启,在实际开发中,这几乎不可能实现,会严重的影响系统的运行效率,再者,gc并不是一定要执行finalize()方法,因为如果有一个方法执行起来很慢,或者直接阻塞了线程,那岂不是会有内存泄露的隐患。因此,gc并没有保证一定会等待finaliz()执行完再去回收对象。

  在笔者看过的很多技术书籍中,都认为方法区(Method Are,永生代(permanent generation)概念在JDK1.8里已被Oracle移除)是没有gc机制的。但事实上,这么说是错误的。在Java虚拟机规范中,明确了虚拟机可以不在方法区内运行gc。这是因为在方法区内运行gc的效率很低,因为在jvm的新生代(Young generation)中,执行一次gc至少可以回收70~90的内存,大多数对象都是朝生夕死的,而方法区的回收效率远远低于堆的回收效率。因此,从效率考虑,一般不会在方法区内运行gc。

  但这并不意味着我们不能在方法区内回收对象,事实上,方法区中的废弃常量和无用的类还是有必要回收的(比如常见的String字符串池问题),尤其是频繁自定义类加载器的场景下,虚拟机必须要具有类卸载的功能,以保证方法区的内存不会溢出。判断常量是否废弃很简单,而判断对象是否无用的条件就有些许苛刻了,必须要同时满足三个条件:

  1.该类必须没有存活着的实例,也就是说,所有该类的实例都被gc从堆上回收了。

  2.加载该类的类加载器(ClassLoader)已经被回收了。

  3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  当类被判断为无用类后,虚拟机就可以对类执行回收了。不过可以并不意味着虚拟机一定要这么去做,事实上,HotSpot虚拟机提供了-Xnoclassgc参数来让用户控制是否回收,也可以用-verbose:class以及-XX+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose.class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,而-XX:+TraceClassUnLoading需要FastDebug版的虚拟机去支持。

  在JDK1.8里,永生代的已经被Oracle公司永久的删除了,取而代之的是类元数据(Class  MetaData),在本地的内存中进行分配,并显式管理元数据的空间。

从OS请求空间,然后分成块(piece);

类加载器从它的块中分配元数据的空间(一个块被绑定到一个特定的类加载器);

当类没有被加载到ClassLoader时,它的块就被回收,内存空间就会被释放出来以供再次使用;

元数据使用由mmap分配的空间,而不是由malloc分配的空间;

  并且Hotspot虚拟机在JDK1.8中提供给用户两个新的参数来管理内存,这两个参数是:"-XX:maxMetaspaceSize:指定类元数据区的最大内存大小","-XX:MetaspaceSize:指定类元数据区的内存阈值--超过将触发垃圾回收机制"。

  感谢周志明先生所著的深入理解Java虚拟机,笔者深受周志明先生的启发,故写下本文总结一下jvm中gc的知识。

关于gc中对象回收算法的认识的更多相关文章

  1. JVM总括二-垃圾回收:GC Roots、回收算法、回收器

    JVM总括二-垃圾回收:GC Roots.回收算法.回收器 目录:JVM总括:目录 一.判断对象是否存活 为了判断对象是否存活引入GC Roots,如果一个对象与GC Roots没有直接或间接的引用关 ...

  2. JVM垃圾回收机制之对象回收算法

    前言 在前面的文章中,介绍了JVM内存模型分为:堆区.虚拟机栈.方法区.本地方法区和程序计数器,其中堆区是JVM中最大的一块内存区域,在Java中的所有对象实例都保存在此区域,它能被所有线程共享. 在 ...

  3. 浅谈PHP5中垃圾回收算法

    原文链接:http://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源 ...

  4. JVM中垃圾回收算法

    GC 算法与种类 GC的概念 Garbage Collection 垃圾收集1960年 List 使用了GCJava中,GC的对象是堆空间和永久区 引用计数法 老牌垃圾回收算法通过引用计算来回收垃圾使 ...

  5. 小师妹学JVM之:GC的垃圾回收算法

    目录 简介 对象的生命周期 垃圾回收算法 Mark and sweep Concurrent mark sweep (CMS) Serial garbage collection Parallel g ...

  6. java面试一日一题:java中垃圾回收算法有哪些

    问题:请讲下在java中有哪些垃圾回收算法 分析:该问题主要考察对java中垃圾回收的算法以及使用场景 回答要点: 主要从以下几点去考虑, 1.GC回收算法有哪些 2.每种算法的使用场景 3.基于垃圾 ...

  7. 【C# .Net GC】垃圾回收算法 应用程序线程运行时,

    触发垃圾回收算法的条件 触发垃圾回收的条件 当满足以下条件之一时将发生垃圾回收: 操作系统报告低内存请看(将触发第2代垃圾回收). 这是通过 OS 的内存不足通知或主机指示的内存不足检测出来. 由托管 ...

  8. java中垃圾回收算法讲解

      判断对象是否存活的方法: 一.引用计数算法(Reference Counting) 介绍:给对象添加一个引用计数器,每当一个地方引用它时,数据器加1:当引用失效时,计数器减1:计数器为0的即可被回 ...

  9. JVM GC-----垃圾回收算法

    说到Java,一定绕不开GC,尽管不是Java首创的,但Java一定是使用GC的代表.GC就是垃圾回收,更直接点说就是内存回收.是对内存进行整理,从而使内存的使用尽可能大的被复用. 一直想好好写一篇关 ...

随机推荐

  1. AFNetworking实现表单(multipart)形式上传图片

    最近遇到个问题,就是上传图片到服务器,后台说用表单形式... 由于没弄过这种上传,所以搜了大堆资料,但也没解决问题. 最后通过请教一位大神才得以解决这个简单的问题... 现在将此方法做个笔记... & ...

  2. 【BZOJ4237】稻草人

    题意 给定平面上 \(N\) 个关键点,询问有多少个矩形满足左下和右上各有一个关键点,且矩形中间没有关键点. \(N\le 2\cdot 10^5\) . 题解 我们按 \(x\) 排序分治,对于左右 ...

  3. phpStudy配置站点解决各种不能访问问题(本地可www.xx.com访问)

    1.配置站点:打开phpStudy->其他选项菜单->站点域名管理 2.配置站点:打开phpStudy->其他选项菜单->打开hosts(www访问重点) 3.在apache的 ...

  4. NumPy 数组迭代

    章节 Numpy 介绍 Numpy 安装 NumPy ndarray NumPy 数据类型 NumPy 数组创建 NumPy 基于已有数据创建数组 NumPy 基于数值区间创建数组 NumPy 数组切 ...

  5. 本地的jar包导入到maven仓库

    需要引入本地jar,然后百度跟着教程实现了,做个记录加深印象.https://www.cnblogs.com/lixuwu/p/5855031.html 1首先找到要传入maven的jar包(放在一个 ...

  6. 【Vue中的坑】Vue中的修改变量没有效果?

    使用箭头函数 this.$forceUpdate();

  7. 如何生成 SSH keys, 并在 Github 或 Gitlab 等上添加密钥

    1 打开 Git Bash $ 2 输入 dir, 确认当前文件夹,并切换到想存密钥文件即pub文件的路径 $ dir 3 生成 密钥命令 ssh-keygen -t rsa -C "{ y ...

  8. uboot 学习笔记

    ram 初始化: 在 start.S 中, bl cpu_init_crit 这句,在 tq2440 中是直接调用,在韦东山里面是通过和 TEXT_BASE 进行比较,如果从 RAM 中运行就不进行 ...

  9. ADC分辨率

    转载:http://www.rationmcu.com/elecjc/1874.html 今天给大家简单介绍一下ADC器件的常识. ADC,模数转换器,功能是把模拟电压转换成数字量. 概念听的模糊,说 ...

  10. [SDOI2016]游戏(树剖+李超树)

    趁着我把李超树忘个一干二净的时候来复习一下吧,毕竟马上NOI了. 题解:看着那个dis就很不爽,直接把它转换成深度问题,然后一条直线x->y,假设其lca为z,可以拆分成x->z和z-&g ...