浅谈java中的垃圾回收机制

为什么需要垃圾回收

任何的语言在运行的过程中都会创建对象,也就意味着要在内存中为这些对象分配空间,当对象使用过后需要释放掉这些内容,保证内存能给新的对象去使用,对于内存释放就是垃圾回收,也叫GC。

之前网上流传过这样一个段子,说在食堂里吃饭,吃完把餐盘端走清理的,是 C++ 程序员,吃完直接就走的,是 Java 程序员。以及这里也找了C语言和java垃圾回收区别的搞笑图片。
C语言

java
从上面两张图片和段子我们可以看出,C语言的垃圾回收是人工的,工作量大,但是可控性高。而java是自动回收,但是可控性很差,其实在很多开发的过程中对于垃圾回收我们并不是很关注,因为大多数的垃圾回收java会自动帮我们做了,所以有的时候会出现内存泄漏的问题。

判断是否需要被回收

一般垃圾回收要做的主要是两件事情 1.发现无用对象;2.回收无用对象占用的内存空间,使得该空间能被再次使用。这个过程的第一步被称为标记,标记哪些内存需要被使用,哪些不需要被使用。
判断一个对象需不需要被回收的的方法有三种:

  1. 引用计数算法
  2. 可达性分析算法
  3. 方法区回收
    下面我们就来简单的分析一下三种算法的区别。

引用计数

引用计数算法是垃圾收集器中的一个早期策略,他是通过空间保存该对象被引用的次数,如果该对象被其他的对象引用,则他的引用计数就会加一,如果删除该对象的引用,那么他的引用计数就减一,当对象的引用计数为0的时候,那么该对象就会被回收。
例如:

	String str = new String("a");
	//这个时候str就有一个a的引用
	str = null;
	//然后讲str设置为null,这个时候"a"的引用次数就等于0,那么这个时候"a"这块内容就需要被回收了。
	//虽然这个办法看起来很好,但是他有一个缺点就是无法检测出循环引用。
	Test  test = new Test();
 	Test  test2 = new Test();
	test1.value = test2;
	test2.value = test1;
	test1 = null;
	test2 = null;
	//这个时候将两个test对象设置为null,也就是test1和test2不能被访问了,
	//但是因为他们一直互相引用着对方,导致他们的计数都不为0,所以垃圾收集器就永远不会回收他们

因为引用计数解决不了java中的一个循环引用的问题,所以我们就用到了可达性分析算法。

可达性分析算法

简单来说,就对象看作一个图,选定活动的对象作为GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之间不可达,也就不存在引用,那么即可认为是可回收对象。

可达性算法根据GC Root一个个往下,把正在使用的标记出来,标记完成后Java就已经知道那些对象是正在使用的了,对于那些没有标记为正在使用的,如上图的object5没有入口可以跟踪到,这种情况,就会被回收。

可达性算法中的GC ROOT

在Java语言中,可以作为GC Root的对象包括下列4种:

  1. 虚拟机栈中正在引用的对象
  2. 本地方法栈中正在引用的对象
  3. 静态属性引用的对象
  4. 方法区常量的对象
虚拟机栈中正在引用的对象
Public class Test{
	public Test(String name){}
}
public static void testGC(){
	Test test = new Test("str");
	test = null;
}
//这个时候的test就是GC Root ,当test为空时str对象也断掉了与 GC Root 的引用链,将被回收。
本地方法栈中正在引用的对象

本地方法(Navtive 方法)是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为NAVtive方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。当线程调用Java方法时,虚拟机会创建一个栈帧并压入Java虚拟机栈,然而当它调用的时native方法时,虚拟机会保持Java虚拟机不变,也不会向Java虚拟机中压入新的栈帧,虚拟机只是简单地动态链接并直接调用只用的native方法。
本地方法栈中正在引用的对象

静态属性引用的对象

public class TestStaticProperties{
	public static TestStaticProperties staticProperties ;
	public TestStaticProperties(String name){}
}
public static void testGC(){
	TestStaticProperties test = new TestStaticProperties("str");
	test.staticProperties  =  new  TestStaticProperties("testStaticProperties");
	test = null;
	/*
	 *test 为GC Root ,test 设为null,test所指向的str对象由于无法和GC Root进行连接所以被回收,
	 *而staticProperties 是类的静态属性,也是属于GC Root,testStaticProperties对象依然
	 *和GC Root建立着连接,此时testStaticProperties对象就不会被回收。
	 */
}

方法区常量的对象

public class TestMethodAreaStatic{
	public static final  TestMethodAreaStatic  testFinal = new TestMethodAreaStatic("testFinal") ;
	public TestMethodAreaStatic(String name){}
}
public static void testGC(){
	TestMethodAreaStatic test = new TestMethodAreaStatic("test");
	test = null;
//testFinal 是方法区中的常量也是GC Root ,test对象设置为null后
// testFinal对象也不会因为没有与GC Root 建立连接而被回收。

引用类型和可达性级别

引用类型

  1. 强引用(StrongReference):最常见的普通对象引用,只要还有强引用指向一个对象,就不会回收。一般Object obj = new Object();这个就是强引用。

  2. 软引用(SoftReference):JVM认为内存不足时,才会去试图回收软引用指向的对象。(可以用于缓存处理)

  3. 弱引用(WeakReference):虽然是引用,但是随时可能被回收掉。

  4. 虚引用(PhantomReference):与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。不能通过它访问对象。对象被finalize以后执行制定逻辑的机制(Cleaner)

引用流程图

Java4种引用的级别由高到低依次为:
强引用 > 软引用 > 弱引用 > 虚引用

具体可以查阅这篇博文: https://www.cnblogs.com/gudi/p/6403953.html

可达性级别

  1. 强可达(Strong Reachable):一个对象可以有一个或多个线程可以不通过各种引用访问到的情况。
  2. 软可达(softly Reachable):就是当我们只能通过软引用才能访问到的对象状态。
  3. 弱可达(weakly Reachable):只能通过弱引用访问时的状态。当弱引用被清除的时候,就符合销毁条件。
  4. 幻象可达(Phantom Reachable):不存在其他引用,并且被finalize过了,只有还乡引用指向这个对象。
  5. 不可达(unreachable):意味着对象可以被清除了。

方法区回收

主要回收废弃常量和无用的类,废弃常量包括字面量、类或接口、方法、字段的符号引用等,废弃值得是没有任何地方引用这个常量
无用的类满足的三个条件:

  1. 没有该类的任何实例存在
  2. 加载该类的ClassLoader已经被回收
  3. 该类对于的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

当满足这三个条件时,虚拟机仅仅是“可以”对这个类进行回收,而不是像对象一样,不使用了就必然回收。

垃圾回收算法

当垃圾被标记清楚后,垃圾收集器就要开始进行垃圾回收了。由于不同的虚拟机厂商会采用不同的算法,所以我们这里来讨论几种常用的垃圾算法。

标记-清除算法

算法分为标记和清除两个阶段:首先标识出所有要清除的对象,然后进行清除。
但是标记和清除的过程效率有限,有内存碎片话问题,不适合特别大的堆;收集算法基本基于标记-清除的思路进行改进。
标记-清除
标记-清除算法不仅对不存活的对象进行处理,在存活对象比较多的情况极为高效,但其存在最大的问题就是内存碎片的问题,假设上面的每个格子大小为1M,当垃圾回收结束后,我们这里现有有一个2M或者3M的数据需要往里面放入,但是因为内存区域不是连续的所以放入失败。

复制算法

划分两块同等大小的区域,收集时将活着的对象复制到另一个区域。
拷贝过程中将对象顺序放置,就可以避免内存碎片化。复制+预留内存,有一定的浪费。

复制算法
复制算法是由标记清除算法演化而来,解决了标记清除算法的碎片化的问题,他将内存容量划分成两个相等大小的区域,每次只会使用一块,当一块用完后就将对象复制到另一块上面,然后把已用过的内存空间全部清理,保证了内存连续可用,但是需要两个相同大小的空间,太浪费了。

标记-整理算法

类似于标记-清除,但为避免内存碎片化,他会在清理过程中将对象移动,以确保移动后的对象占用连续的空间
标记整理

先标记出需要清理的对象,然后将可回收的对象进行清理,清理完成后将所有存活的对象都向左移动,因此会产生较高的成本,但却解决了内存碎片的问题。

分代收集算法

分代收集是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合算法。根据对象生命周期将Java堆划分为新生代和和老年代。根据划分不同采用不同的算法,比如新生代中采用复制算法,老年代采用标记-清理或者标记-整理的算法。
堆空间

在jdk1.7版本以前,JVM把内存区分为新生代、老年代和永久代。

新生代

  1. 所有新生成的对象都放在新生代中,新生代的目标是尽可能的快速收集掉那些生命周期短的对象。
  2. 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
  3. 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
  4. 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

老年代

在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代里存放的都是生命周期比较长的对象。 此外老年代内存比新生代大很多,比例大概是2:1,当老年代满时会触发Minor GC(Full GC) ,老年代的对象存活时间比较长,因此Full GC发生的频率比较低。

永久代

用于存放静态文件,如java类、方法等。持久带对垃圾回收没有显著的影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理等,在这种时需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。

注意: 在jdk8的时候java废弃了永久代,提供了与永久代类似的叫做“元空间”的技术。由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryErroy。元空间的本质和永久代类似。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。也就是不局限与jvm可以使用系统的内存。理论上取决于32位/64位系统可虚拟的内存大小。

进行垃圾回收

垃圾回收器

JVM为我们提供了若干可供选择的回收方式,即我们称为的垃圾回收器。
新生代使用的垃圾收集器: Serial、PraNew、Parallel Scavenge
老年代收集器使用的收集器: Serial Old、Parallel Old、CMS

垃圾回收器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值