题目之所以是再聊,是因为以前聊过: [JVM]聊聊 CMS 收集器
最近又看了下这块的知识,打算把 CMS/标记-清除/GC Roots/引用 这些知识串起来
我依旧可能写的不是很好,降低下期待
GC 算法
CMS 是基于 标记-清除 算法来做的,那我们就先从 GC 算法开始聊
GC 算法有:
- 标记-清除
- 标记阶段: 从 GC Roots 对象开始遍历对象图,将所有可达对象进行标记
- 清除阶段: 堆中未被标记的对象进行清除,释放内存空间
- 标记-复制:
- 标记阶段: 从 GC Roots 对象开始遍历对象图,将所有可达对象进行标记
- 复制阶段: 将所有标记的对象从 From 指针指向的区域,复制到 To 指针指向的区域,同时更新对象引用
- 回收阶段: 清除未被标记的对象,同时更新 From 指针和 To 指针指向的区域,保持 From 指针指向的区域为空
- 标记-整理:
- 标记阶段: 从 GC Roots 对象开始遍历对象图,将所有可达对象进行标记
- 整理阶段: 将所有标记的对象向一端移动,然后将未被标记的内存空间释放
标记-清除算法有两个缺点: 1 执行效率不稳定:大量对象需要大量被回收时,会导致标记 & 清除效率变低; 2 内存碎片化
标记-复制算法解决了内存碎片化的问题,但它是以牺牲空间来优化的(因为有一部分空间要留出来复制用)
标记-整理算法也解决了内存碎片化的问题,但它是以牺牲时间来优化的(因为要花时间将标记对象移动到一起)
CMS 基于 标记-清除 算法来实现的,那就会有一个无法避免的缺点: 会有大量的空间碎片
CMS 进行垃圾回收时的整个过程
接下来聊一聊 CMS 进行垃圾回收时的整个过程,其实也就是 4 个过程啦:
- 初始标记: 标记 GC Roots 能直接关联到的对象,此时垃圾收集线程是独占的,不会和用户线程一起运行,会发生 stop the world 现象,不过整个过程时间很短
- 并发标记: 从 GC Roots 的直接关联对象开始遍历整个对象图,耗时比较长,不过看名字也看出来了,"并发,并发"嘛,此时的垃圾收集线程是和用户线程一起进行的
- 重新标记: 是为了修正并发标记期间产生变动的记录,这个过程相对于初始标记来说,耗时是挺长的,但是相对于并发标记来说,耗时还是挺短的,此时垃圾收集线程也是独占的,会发生 stop the world 对象
- 并发清除: 清理删除标记的对象,此时不需要移动对象,所以会和用户线程一起工作
什么是 GC Roots
在聊 GC 算法时,标记阶段都是从 GC Roots 开始的
那么,什么是 GC Roots?
要聊 GC Roots 的话,就需要来聊聊: 引用计数法
首先,垃圾收集器回收的一定是程序没有在用的对象,那它应该怎么判断这个对象有没有在用,能不能被回收呢?
可以在对象中添加一个引用计数器,如果这个对象被引用了,引用计数器就加一;如果引用失效了,引用计数器就减一
这样回收的时候,看看哪些计数器计数为 0 ,就可以回收了
此时就会有一个问题: A 引用了 B , B 引用了 A ,它俩在循环引用彼此,但除此之外,没有别的对象引用了
A,B 的引用计数器都为 1 ,就不会回收
引用计数法看来就不是很完美嘛,为了优化这个问题,就有了 GC Roots(可达性分析)
它的做法是: 一系列被称为 "GC Root"根对象作为起始节点集,根据引用关系向下探索
这样的话,虽然 A,B 在循环引用,但是没有任何一个 GC Root 可以达到它们,此时它们就会被回收
什么可以被称为 “GC Root” 呢?
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数/局部变量/临时变量等
- 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量
- 在方法区中常量引用的对象,譬如字符串常量池( String Table )里的引用
- 在本地方法栈中 JNI (即通常所说的 Native 方法)引用的对象
- Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton/OutOfMemoryError)等,还有系统类加载器
- 所有被同步锁( synchronized 关键字)持有的对象
- 反映 Java 虚拟机内部情况的 JMXBean/JVMTI 中注册的回调、本地代码缓存
别惊讶了,上面那一段是我直接从<深入理解 Java 虚拟机>这本书里,直接复制过来的
其实来看下 Java 虚拟机运行时数据区,就比较容易理解了
- 程序计数器: 当前线程所执行的字节码的行号指示器
- 虚拟机栈: 用于存储 局部变量表/操作数栈/动态连接/方法出口 等信息
- 本地方法栈: 与虚拟机栈所发挥的作用是非常相似的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则是为虚拟机使用到的本地( Native )方法服务
- 堆: 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存,垃圾收集器管理的内存区域说的就是堆了
- 方法区: 用于存储已被虚拟机加载的 类型信息/常量/静态变量/即时编译器编译后的代码缓存 等数据
- 运行时常量池: 是方法区的一部分, Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
对应着 GC Roots 来看运行时数据区,大部分就能对应上了,不外乎 虚拟机栈中引用的对象/本地方法栈中 native 方法引用的对象/方法区中类静态属性引用的对象/方法区中常量引用的对象
引用
在聊 GC Roots 时,提到了引用,这里扩展下
引用分为:
- 强引用: 只要存在,就不会回收
- 软引用: 还有用,但非必须对象,在内存溢出异常前,会被列进回收范围内,进行第二次回收
- 弱引用: 非必须对象,不管内存是否足够,都会回收
- 虚引用: 为了能在这个对象被回收时,有系统通知
以上,感谢您的阅读~