如何判断对象是否存活的
引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的
主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单 的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数 就很难解决对象之间相互循环引用的问题
可达性分析算法 (JVM使用的)
通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过 程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的
GCRoots 是什么
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
publicclass Test {
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
public class Test {
public static Test s;
public static void main(String[] args) {
Test a = new Test();
a.s = new Test();
a = null;
}
}
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
public class Test {
public static final Test s = new Test();
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
Java中对象的引用类型有什么?垃圾回收下的表现是什么样的?
Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
-
强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
-
软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出 异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
-
弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下 一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对 象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
-
虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。
如果对象不可达,Jvm会立即回收这个对象吗
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段, 要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”
如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之 中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法
方法区有垃圾回收吗
《Java虚 拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或未能完整 实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载),方法区垃圾收集 的“性价比”通常也是比较低的:在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常 可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回 收成果往往远低于此
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
垃圾收集算法
分代收集理论
收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区 域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那 么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对 象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有 效利用
在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域 ——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算 法”“标记-整理算法”等针对性的垃圾收集算法
标记-清除算法
算法分为“标记”和“清除”两个阶段:
-
标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。 最基础的收集算法,是因为后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。
-
它的主要缺点有两个:
- 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
- 第二个是 内存空间的碎片化问题,标记、清除之后会产生 大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时 无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
标记-复制算法
常被简称为复制算法。为了解决标记-清除 算法面对大量可回收对象时执行效率低的问题,1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集 算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象 复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
- 内存中多数对象都是存活的,这种算法将会产生大量 的内存间复制的开销,
- 对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
- 这样实现简单, 运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点
现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代,IBM公 司曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。
HotSpot虚拟机的Serial、 ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局。具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空 间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden 和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接 清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor 的大小比例是8∶1,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实 际上大多就是老年代)进行分配担保
标记-整理算法
标记-复制算法在对象存活率 较高时就要进行较多的复制操 作,效率将会降低。更关键的 是,如果不想浪费50%的空间, 就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情 况,所以在老年代一般不能直接选用这种算法
标记-整 理(Mark-Compact)算法, 其中的标记过程仍然与“标记清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。
- 是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来 看,移动对象会更划算。