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,垃圾回收总耗时

https://lddpicture.oss-cn-beijing.aliyuncs.com/picture/image-20210706085732787.png

**可达性分析算法:**通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。在Java中,可作为GC Root的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

https://lddpicture.oss-cn-beijing.aliyuncs.com/picture/image-20210706085837668.png

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异常,虽然规范规定虚拟机可以不实现垃圾收集,因为和堆的垃圾回收效率相比,方法区的回收效率实在太低,但是此部分内存区域也是可以被回收的。方法区的垃圾回收主要有两种,分别是对废弃常量的回收(常量池的回收)和对无用类的回收(类的卸载)。当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收。

方法区中类标记为无用类,需要满足以下三个条件

  1. Java堆中不存在该类的任何实例对象

  2. 加载该类的类加载器已经被回收

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

当满足上述三个条件的类才可以被回收,但是并不是一定会被回收,需要参数进行控制,例如HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收。

5. 垃圾回收算法

.1. 标记-清除算法

标记-清除算法对根集合进行扫描,对存活的对象进行标记。标记完成后,再对整个空间内未被标记的对象扫描,进行回收。

  • 优点:实现简单,不需要进行对象进行移动。
  • 缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。

https://lddpicture.oss-cn-beijing.aliyuncs.com/picture/image-20210706091434405.png

.2. 复制算法

这种收集算法解决了标记清除算法存在的效率问题。它将内存区域划分成相同的两个内存块每次仅使用一半的空间JVM生成的新对象放在一半空间中。当一半空间用完时进行GC,把可到达对象复制到另一半空间,然后把使用过的内存空间一次清理掉。

  • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
  • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

https://lddpicture.oss-cn-beijing.aliyuncs.com/picture/image-20210706091703794.png

.3. 标记-整理算法

标记-整理算法 采用和 标记-清除算法 一样的方式进行对象的标记,但后续不直接对可回收对象进行清理,而是将所有的存活对象往一端空闲空间移动,然后清理掉端边界以外的内存空间。

  • 优点:解决了标记-清理算法存在的内存碎片问题。
  • 缺点:仍需要进行局部对象移动,一定程度上降低了效率。

https://lddpicture.oss-cn-beijing.aliyuncs.com/picture/image-20210706091814472.png

.4. 分代收集算法

当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代老年代永久代,如图所示:

https://lddpicture.oss-cn-beijing.aliyuncs.com/picture/image-20210706091858163.png

新生代 中存在一个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 SurvivorTo Survivor区域。

对于老年代,因为对象存活率高,没有额外的内存空间对它进行担保。因而适合采用标记-清理算法标记-整理算法进行回收。

Resource

0%