天天看点

Java基础篇——GC垃圾回收机制

Java垃圾回收机制概念

       new的本质为malloc,即在内存中(注意是在内存中!)申请一段空间,申请之后必须释放,否则会产生大量未被回收的内存碎片,进而导致软件崩溃。在C语言中存在有free()函数,C++中存在有析构函数,这两种都为回收这样的内存碎片提供了工具,但Java中有一样更为NB的机制,也被js,GO等超灵活超优雅的程序语言争相模仿,即大名鼎鼎的垃圾回收机制,又称GC(Garbage Collection)。刚刚入门的基础Coder知道有这么个东西就好,等你摸清了Java的基本使用发方法之后可以尝试看一看GO语言实现JVM内核的方法。

        说句题外话,这在我心里一直是一项非常非常有价值的技术,他将回收垃圾时要考虑的超多问题以某种基础机制自动解决,足以体现程序语言设计团队的博大人文情怀,而且其中涉及的知识、代码结构、算法远非常人可及,所以作为一种“工具”,使用时的注意事项就尤为重要,毕竟有利就有弊,就算是GC也不可能完美。

GC的执行时机

      目前有这么几种说法: 当new出的实例对象使用结束,方法中局部变量、局部对象在该方法被调用结束时可能会执行的方法(为什么会是可能,下文会解释)和 当内存警告与程序即将结束时执行,不频繁,所以占用cpu时间片段不多。这之前科普一下finalize方法,每个对象都有这样一个方法,使用时只需覆盖即可。这是被gc调用的留给程序员的最后一根指挥棒,用来控制程序最后的动作。

       下面给一段代码:

public class PreClass {
	private int num1;
	
	public PreClass() {
	}
	
	public void showStr(String str) {
		int length = str.length();
		System.out.println("这句话拥有[" + length + "]个字," + str);
	}
	
	public PreClass add(int num2) {
		this.num1 += num2;
		System.out.println(this.num1);
		
		return this;
	}

	@Override
	protected void finalize() throws Throwable {
		Thread.sleep(3000);
		System.out.println(this + "已被回收");
	}
}
           

        最后添加了一个3s的延时,如果该finalize被使用时就会有一个3s的延迟,之后便会输出“xxx已被回收”的语句。

public class Demo {
	public static void main(String[] args) {
		PreClass pre = new PreClass();
		
		pre.add(12).add(15);
		pre.showStr("这居然是非常正好的十五字你敢信");
	}
}
           

        下面为输出:

12
27
这句话拥有[15]个字,这居然是非常正好的十五字你敢信
           

        (注意:下面是我之前的看法)可见,"xxx被回收"的语句并未出现,这并不是说明不是所有的程序在运行之后变量都会被回收。而是在GC之后,Java的console窗口一样被回收,导致并不是没被回收,而是回收了你也看不到。若想看到结果需要满足一定条件,下面改一下Demo:

public class Demo {
	public static void main(String[] args) {
		PreClass pre = new PreClass();
		
		pre.add(12).add(15);
		pre.showStr("这居然是非常正好的十五字你敢信");
		System.out.println(pre.num1);
		pre = null;                        //人为将pre空间回收
		System.gc();
		
		try {
			Thread.sleep(6000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println("6s后");        //在GC之后,看他会不会执行
	}
}
           
12
27
这句话拥有[15]个字,这居然是非常正好的十五字你敢信
[email protected]已被回收
6s后
           

        显而易见,我之前的想法是错误的,并不是console也一样被回收了,那么又是怎么一回事呢?经过研究查找资料后我打算这样验证:

private static void fun1() {
		PreClass pre = new PreClass();                //不解除对pre的引用
		pre.showStr("就是为了消除警告");        
		System.gc();
	}
	
	private static void fun2() {
		PreClass pre = new PreClass();
		pre.showStr("就是为了消除警告");
		
		pre = null;                                    //解除对pre引用
		System.gc();
	}
	
	private static void fun3() {
		{
			PreClass pre = new PreClass();
			pre.showStr("就是为了消除警告");        //经过一个{}后pre的寿命已经结束,如果此时释放GC说明GC不是对应堆
		}
		
		System.gc();
	}
	
	private static void fun4() {
		{
			PreClass pre = new PreClass();        
			pre.showStr("就是为了消除警告");
		}
		
		int a = 2;                                    //pre生命周期结束后被a的赋值替代,如果GC失败说明栈帧中不存在覆盖关系,即
		System.gc();                                  //pre的生命周期结束后栈帧负责它的区域仍存活且仍旧被pre占据
	}
	
	private static void fun5() {
		fun1();                                        //进行对fun1()的调用,观察栈帧寿命与什么有关
                
		System.gc();
	}
           

结果

fun1();			//GC失败
		fun2();			//GC
		fun3();			//GC失败
		fun4();			//GC
		fun5();			//GC
           

       需要介绍一下 栈帧的概念:每当Java开启一个线程时,JVM会为其开辟出一个Java栈,每当其调用一个方法时就向该栈压入一个栈帧,即栈帧是Java栈的基本组成单位。熟悉栈这种数据结构的朋友都知道,栈,先入后出,存在栈顶指针ebp和栈底指针

esp,栈顶为高地址,每当需要释放空间时将栈顶指针上移即可,栈帧也是如此释放。

        栈帧中存在三部分,局部变量帧,操作数栈和帧数据区。局部变量帧负责存储局部变量,按字节存储,即根据变量的声明方式判断其数据类型并压入对应Java栈。操作数栈负责存储临时数据,如int a = 2等。帧数据区负责存储用来进行常量池解析、方法返回及异常派发等的基本数据。

        fun1中存在了对PreClass的引用,就需要在栈帧的局部变量帧压入该变量且后期没有释放,便一直占据,导致GC失败,毕竟GC的本质是回收没用的空间,栈还在用着他当然不会回收。fun2在结束了pre的引用后将其赋值为null,即人为回收,GC判定无用,收回。所以这里做两点结论:

                1.变量不用后将其赋值为null是最为安全的回收方法,无论GC不GC;

                2.GC的一种实现方法应该就是判定当前引用是否为null。

        fun3由于在引用后并未释放pre导致GC失败,所以说明该栈仍被pre数据块使用。fun4释放成功是因为在pre数据块运行结束后出现了新的int赋值,导致覆盖了当前数据块,联想栈帧结构,操作数栈可以覆盖局部变量区。fun5中调用fun1方法,fun1方法使用后寿命结束,无返回值出栈,空间无用,所以GC成功。