JVM的垃圾回收机制

JVM为什么要进行垃圾回收?如果不进行垃圾回收,一些用过的变量就需要进行手工释放,这就很容易遗漏出问题,占用内存,影响效率,JVM的垃圾回收机制简化了回收垃圾的方法,让程序变得更简洁。
思考问题:
1.哪些“垃圾”需要回收?
2.怎么回收?
3.谁负责回收?
4.什么时候回收?

了解了这几个问题,java的垃圾回收也就基本差不多了

1.哪些“垃圾”需要回收?

我们都知道(其实我是刚刚知道)JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。
在这里插入图片描述
其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都是具有确定性,就不需要过多考虑回收的往年提,因为方法结束或者线程结束时,内存自然就随着回收了。而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正式垃圾收集器所需要关注的东西。
1.1.1说一下什么是java堆
堆内存是用来存放有new创建的对象实例和数组(重点)
java堆是所有线程共享的一块内存区域在虚拟机启动是创建,此内存区域的唯一目的就是存放对象实例
Java堆是垃圾收集器管理的主要区域。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续的即可。在实现上,既可以实现固定大小的,也可以是扩展的。如果堆中没有内存完成实例分配,并且堆也无法完成扩展时,将会抛出OutOfMemoryError异常。
什么是方法区(方法区在堆内存里面)
与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。垃圾收集行为在这个区域比较少出现。
在这里插入图片描述1.1.2说一下什么是java栈
在栈内存中保存的是堆内存空间的访问地址,或者说栈中的变量指向堆内存中的变量(java中的指针)(重点)
java栈是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈用来存储局部变量表、操作数栈、动态连接、方法出口等。每个方法从调用直至执行完成的过程就对应着一个栈帧在虚拟机中入栈和出栈的过程
什么是本地方法栈
本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
在这里插入图片描述

		Foo foo = new Foo(); 
		foo.f();

以上代码的内存实现原理为:
  1.Foo类首先被装载到JVM的方法区,其中包括类的信息,包括方法和构造等
  2.在栈内存中分配引用变量foo
  3.在堆内存中按照Foo类型信息分配实例变量内存空间;然后,将栈中引用foo指向foo对象堆内存的首地址
  4.使用引用foo调用方法,根据foo引用的类型Foo调用f方法
1.1.3程序计数器
每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。该内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM(内存溢出:OutOfMemoryError)情况的区域。
1.2判断哪些垃圾需要回收,这些垃圾回收是机器做不是人做,不能像扔垃圾一个告诉我每天需要扔什么样的垃圾,这就需要算法的支持,根据算法判断出哪些是符合期望需要回收垃圾。
算法有两种:
1.引用计数算法
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
在这里插入图片描述
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
所以这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
2.可达性算法
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
在这里插入图片描述
 在Java语言中,可作为GC Roots的对象包括下面几种:
  1.虚拟机栈中引用的对象(栈帧中的本地变量表);
  2.方法区中类静态属性引用的对象;
  3.方法区中常量引用的对象;
  4.本地方法栈中JNI(Native方法)引用的对象。
在谈“引用”
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。
强引用
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。类似 Object obj = new Object() 这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用
用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,**在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。**如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用
也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用
也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。
虚引用主要用来跟踪对象被垃圾回收的活动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
什么是内存泄漏:
一般来说内存泄漏有两种情况。一是在堆中分配的内存,在没有将其释放掉的时候,就将所有能访问这块内存的方式都删掉,从而导致这块内存一直没有被释放;****另一种情况则是在内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)第一种情况,在Java中已经由于垃圾回收机制的引入,得到了很好的解决。所以,Java中的内存泄漏,主要指的是第二种情况。

2.怎么回收?

常用的垃圾收集算法
1.标记-清除算法
采用从跟集合(GC Roots)进行扫描,对存活的对象进行标记,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于该算法直接回收不存活的对象,因此会造成内存碎片
B被清除后留下了碎片
在这里插入图片描述
2.复制算法
该算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成一个对象面多个空闲面,程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从跟集合(GC Roots)中扫描活动对象,并将对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存
在这里插入图片描述
3.标记-整理算法
该算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针,标记整理算法是在标记-清楚算法的基础上,又进行了对象的移动,因此成本更高,但是解决了内存碎片的问题。
在这里插入图片描述
!!4.分代收集算法!!
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象的存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是持久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量的对象被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最合适的收集算法。分代GC在新生代的算法:采用了GC的复制算法,速度快,因为新生代一般是新对象,都是瞬态的用了可能很快被释放的对象。分代GC在年老代的算法 标记/整理算法,GC后会执行压缩,整理到一个连续的空间,这样就维护着下一次分配对象的指针,下一次对象分配就可以采用碰撞指针技术,将新对象分配在第一个空闲的区域。
在这里插入图片描述
新生代(Young Generation)的回收算法
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
新生代内存按照8:1:1的比例分为一个eden区和两个servivor(servivor0,servivor1)区。大部分的对象在eden区生成回收时先将eden区存活对象复制到一个servivor0区,然后清空eden区,当这个servivor0区也存放满了是,则将eden区和servivor0区存活的对象复制到另一个servivor1区,然后清空eden和servivor0,此时servivor0区是空的,然后将servivor0和servivor1交换,即保持servivor1区为空,如此反复。
当servivor1区不足以存放eden和servivor0的存活对象时,就将存活对象直接放到老年代。若是老年代也满了就会触发一次fullGC,也就是新生代、老年代都进行回收。
新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
新生代(年轻代)需要记住的几点是:
1.从eden(伊甸园)区进行minor GC的时候采用的是复制算法
2.两个servivor区的大小是相等的
3.两个servivor区一定有一个是空的
老年代(Tenured Generation)回收算法
在年轻代经历了N次垃圾回收后仍存活的对象,就会被放在老年代。因此,可以认为老年代存放的都是生命周期较长的对象。比如:字符串、数组
内存比新生代也大很多,通常是一倍,当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代(Permanet Generation)的回收算法
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。

3.谁负责回收?

下面一张图是HotSpot虚拟机包含的所有收集器,图是借用过来滴:
在这里插入图片描述
在这里插入图片描述

4.什么时候回收?

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC(gabage collection)有两种类型Scavenge GC和Full GC
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC(gabage collection),对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到survivor区。然后整理survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时eden区不会分配的很大,所以eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。导致Full GC的原因:
1.年老代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显示调用
4.上一次GC之后Heap的各域分配策略动态变化

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值