共计 2304 个字符,预计需要花费 6 分钟才能阅读完成。
程序计数器. 虚拟机栈. 本地方法栈随线程而生随线程而灭,栈帧分配多少内存在类结构确定后就确定了。垃圾回收针对的是 Java 堆和方法区。
一:对象已死吗
在垃圾收集器进行回收前,第一件事就是确定这些对象哪些还存活,哪些已经死去。
1. 引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加 1;当引用失效时,计数器减 1;其中计数器为 0 的对象是不可能再被使用的已死对象。
当两个对象相互引用时,这两个对象就不会被回收 引用计数算法, 不被主流虚拟机采用,主要原因是它很难解决对象之间相互循环引用的问题。
2 可达性分析算法
通过一系列的称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所经过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(在图论中称为对象不可达)时,这个对象就是不可用的。如图 object5,6,7 虽然有关联,但是到 gc roots 是不可达的所以是可回收对象。
在 java 中,可作为 GC Roots 的对象包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI 引用的对象
3 引用的分类
强引用:是指在程序代码中直接存在的引用,譬如引用 new 操作符创建的对象。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用:还有用但是并非必需的引用,早系统将要发生内存溢出异常之前会把这些对象列进回收范围中进行二次回收,若还是没有足够的内存,才会抛出内存溢出异常。
弱引用:非必需的对象,只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否够用都将回收这些对象。
虚引用:一个对象是否有虚引用的存在完全不会对他的生存时间构成影响,也无法通过虚引用来取得一个对象实例。
4 宣告一个对象死亡的过程
要真正宣告一个对象死亡,至少要经历两次标记过程:
若对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,会被 第一次标记 并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize() 方法(如当对象没有重写 finalize() 方法或者 finalize() 方法已经被虚拟机调用过则认为没有必要执行)。
如果有必要执行则将该对象放置在 F -Queue 队列中,并在稍后由一个由虚拟机自己建立的、低优先级的 Finalizer 线程去执行它;稍后 GC 将对 F -Queue 中的对象进行第二次标记,如果对象还是没有被引用,则会被回收。
但是作者不建议通过 finalize() 方法“拯救”对象,因为它运行代价高、不确定性大、无法保证各个对象的调用顺序。
5 回收方法区
永久代(方法区)的垃圾收集主要回收两部分内容:废弃常量和无用的类
废弃常量:假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个 String 对象是叫做“abc”的,换句话说,就是没有任何 String 对象引用常量池中的“abc”常量,也没有其他 地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。
无用的类:同时满足下面 3 个条件的类(实例、类加载器被回收,java.lang.Class 对象没有被引用)。①该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。②加载该类的 ClassLoader 已经被回收。③该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
二:垃圾收集算法
1. 标记清除算法
分为两个阶段:标记和清除
标记:首先标记所有需要回收的对象(标记过程在上文宣告一个对象死亡过程中提及)
清除:在标记完成后统一回收所有被标记的对象
不足地方:①效率问题, 标记和清除过程效率都不高;②空间问题, 垃圾回收后较多不不连续的内存碎片,导致分配较大对象时找不到足够的连续内存而不得不提前触发又一次垃圾回收动作.
2. 复制算法(新生代)
将可用内存按容量分为两个块,每次只用其中之一。当这一块内存用完之后,将还存活的对象复制到另一边去,然后清除所有已经使用过的部分。
优点: 每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点: 代价是将内存缩小为了原来的一半,未免太高了一点。
解决方法: 新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。
在 HotSpot 里,考虑到大部分对象存活时间很短将内存分为 Eden 和两块 Survivor,默认比例为 8:1:1。代价是存在部分内存空间浪费,适合在新生代使用。
备注:不保证每次不多于 10% 的对象存活,survivor 空间不够时, 依赖其他内存分配担保 (老年代)
3 标记 - 整理算法(老年代算法)(Mark-Compact)
对象存活率太高情况下,进行较多复制操作效率将会变低, 应对诸如所以对象都存活情况下, 所有老年代不采用复制算法.
标记过程仍然与“标记 - 清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4. 分代收集算法
当前商用虚拟机都采用了这种算法,根据对象的存活周期将内存划分为几块,一般是把 Java 堆分为新生代和老生代,根据各个年代采用适当的收集算法。
新生代一般采用复制算法 (Copying)。
老生代一搬采用 标记 - 清理 (Mark-Sweep) 或者标记 - 整理 (Mark-Compact) 进行回收。