JavaJVM_GC
java运行时,其中程序计数器,虚拟堆栈,本地方法栈三个区域随线程而生,随线程而灭,但时java堆和方法区则不一样,这部分内存分配和回收都是动态的。
1. 判断对象是否存活的方法
- **引用计数算法:**给对象中添加一个引用计数器,每当有一个地方引用它,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再利用的。该方法实现简单,判定效率高,但很难解决对象之间相互循环引用的问题。
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的唯一意义就是占点内存, 以便能在GC日志中看清楚是否有回收过
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
testGC();
}
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假设在这行发生GC, objA和objB是否能被回收?
System.gc();
}
}
相互引用下却已经置为null的两个对象,是否会被GC回收。如果只是按照引用计数器算法来看,那么这两个对象的计数标识不会为0,也就不能被回收。但到底有没有被回收呢?
- 从运行结果可以看出内存回收日志,Full GC 进行了回收。
- 也可以看出JVM并不是依赖引用计数器的方式,判断对象是否存活。
- S0C、S1C,第一个和第二个幸存区大小
- S0U、S1U,第一个和第二个幸存区使用大小
- EC、EU,伊甸园的大小和使用
- OC、OU,老年代的大小和使用
- MC、MU,方法区的大小和使用
- CCSC、CCSU,压缩类空间大小和使用
- YGC、YGCT,年轻代垃圾回收次数和耗时
- FGC、FGCT,老年代垃圾回收次数和耗时
- GCT,垃圾回收总耗时
**可达性分析算法:**通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。在Java中,可作为GC Root的对象包括以下几种:
- 虚拟机栈(
栈帧中的本地变量表)中引用的对象
。 - 方法区中
类静态属性引用的对象
。 - 方法区中
常量引用的对象
。 - 本地方法栈中
JNI(即一般说的Native方法)引用的对象
。
2. 四种引用
- **强引用:**在程序代码中普遍存在的,如
Object obj = new Object()
这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
。 - **软引用:**有用但非必须的对象。当系统将要
发生内存溢出异常前
,将会把这些对象列进回收范围进行二次回收。若这次回收还没有足够的内存,才会抛出内存溢出异常。 - **弱引用:**被弱引用关联的对象
只能生存到下一次垃圾收集之前
,当垃圾收集器工作时,无论内存是否足够,都会回收对象。 - **虚引用:**唯一目的就是在
该对象被回收时收到一个系统通知
。
3. 对象的真正死亡
要宣告一个对象真正死亡,
至少需要经过两次标记过程
:如果对象在进行可达性分析后
发现没有与GC Roots相连接的引用链,那么它将会被第一次标记
,并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法
。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过(finalize()方法只执行一次),则认为其没有必要执行。当覆盖了finalize(),并且将其与引用链的任何一个对象关联,即可实现自救。 不推荐使用,可以使用 try-finally 进行替代。
package ccc;
/*此代码演示了两点
* 对象可以在GC时自我拯救
* 这种自救只会有一次,因为一个对象的finalize方法只会被自动调用一次
执行finalize方法
yes我还活着
no我死了
* */
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK=null;
public void isAlive(){
System.out.println("yes我还活着");
}
public void finalize() throws Throwable{
super.finalize();
System.out.println("执行finalize方法");
FinalizeEscapeGC.SAVE_HOOK=this;//自救
}
public static void main(String[] args) throws InterruptedException{
SAVE_HOOK=new FinalizeEscapeGC();
//对象的第一次回收
SAVE_HOOK=null;
System.gc();
//因为finalize方法的优先级很低所以暂停0.5秒等它
Thread.sleep(500);
if(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no我死了");
}
//下面的代码和上面的一样,但是这次自救却失败了
//对象的第一次回收
SAVE_HOOK=null;
System.gc();
Thread.sleep(500);
if(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no我死了");
}
}
}
4. 回收方法区
方法区和堆一样,都是线程共享的内存区域,被用于存储已被虚拟机加载的类信息(字段等)、即时编译后的代码(方法字节码)、静态变量和常量等数据。根据Java虚拟机规范的规定,方法区无法满足内存分配需求时,也会抛出OutOfMemoryError异常,虽然规范规定虚拟机可以不实现垃圾收集,因为和堆的垃圾回收效率相比,方法区的回收效率实在太低,但是此部分内存区域也是可以被回收的。方法区的垃圾回收主要有两种,分别是对废弃常量的回收(常量池的回收)和对无用类的回收(类的卸载)。当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收。
方法区中类标记为无用类,需要满足以下三个条件
-
Java堆中不存在该类的任何实例对象
; -
加载该
类的类加载器已经被回收
; -
该类
对应的java.lang.Class对象不在任何地方被引用
,且无法在任何地方通过反射访问该类的方法
。
当满足上述三个条件的类才可以被回收,但是并不是一定会被回收,需要参数进行控制,例如HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收。
5. 垃圾回收算法
.1. 标记-清除算法
标记-清除算法对根集合进行扫描,对存活的对象进行标记。标记完成后,再对整个空间内未被标记的对象扫描,进行回收。
- 优点:实现简单,不需要进行对象进行移动。
- 缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
.2. 复制算法
这种收集算法解决了标记清除算法存在的效率问题。它将内存区域划分成相同的两个内存块。每次仅使用一半的空间
,JVM
生成的新对象放在一半空间中。当一半空间用完时进行GC
,把可到达对象复制到另一半空间,然后把使用过的内存空间一次清理掉。
- 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
- 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
.3. 标记-整理算法
标记-整理算法 采用和 标记-清除算法 一样的方式进行对象的标记,但后续不直接对可回收对象进行清理,而是将所有的存活对象往一端空闲空间移动,然后清理掉端边界以外的内存空间。
- 优点:解决了标记-清理算法存在的内存碎片问题。
- 缺点:仍需要进行局部对象移动,一定程度上降低了效率。
.4. 分代收集算法
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代,如图所示:
新生代 中存在一个
Eden
区和两个Survivor
区。新对象会首先分配在Eden
中(如果新对象过大,会直接分配在老年代中)。在GC
中,Eden
中的对象会被移动到Survivor
中,直至对象满足一定的年纪(定义为熬过GC
的次数),会被移动到老年代。可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数
-XX:NewRatio
设置老年代与新生代的比例。例如-XX:NewRatio=8
指定 老年代/新生代 为8/1
. 老年代 占堆大小的7/8
,新生代 占堆大小的1/8
(默认即是1/8
)。老年代: 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的
GC
要比新生代要少得多。对象从老年代中消失的过程,可以称之为major GC
(或者full GC
)。像一些类的层级信息,方法数据 和方法信息(如字节码,栈 和 变量大小),运行时常量池(
JDK7
之后移出永久代),已确定的符号引用和虚方法表等等。它们几乎都是静态的并且很少被卸载和回收,在JDK8
之前的HotSpot
虚拟机中,类的这些**“永久的”** 数据存放在一个叫做永久代的区域。永久代一段连续的内存空间,我们在
JVM
启动之前可以通过设置-XX:MaxPermSize
的值来控制永久代的大小。但是JDK8
之后取消了永久代,这些元数据被移到了一个与堆不相连的称为元空间 (Metaspace
) 的本地内存区域。
JDK8
堆内存一般是划分为年轻代和老年代,不同年代 根据自身特性采用不同的垃圾收集算法。对于新生代,每次
GC
时都有大量的对象死亡,只有少量对象存活。考虑到复制成本低,适合采用复制算法。因此有了From Survivor
和To Survivor
区域。对于老年代,因为对象存活率高,没有额外的内存空间对它进行担保。因而适合采用标记-清理算法和标记-整理算法进行回收。