深入理解JVM(3)——垃圾收集策略详解

Java虚拟机的内存模型分为五部分:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。

程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,也就是每个线程都拥有这三个区域,而且这三个区域会随着线程的创建而开始,随着线程的结束而销毁,那么垃圾收集器在何时清理这三个区域的问题就解决了。

堆和方法区是所有线程共享的区域,并且是在JVM开启的时候创建一直运行到JVM停止,因此它没办法根据线程的创建而创建,根据线程的结束而结束。堆中存放JVM在运行期间的所有对象,虽然每个对象的内存大小在加载该对象所属类的时候就已经确定了,但是究竟创建多少个这样的对象只能在程序运行期间才能知道;方法区中存放的类信息、静态成员变量和常量,类的加载是在程序运行过程中,当需要创建这个类的对象时才会加载这个类,JVM究竟需要加载多少个类也需要在程序运行期间才能确定。

一、堆内存的回收

  1. 如何判断那些对象需要被回收

首先判断一个对象无效的依据是这个对象不再被任何的对象或者变量所引用。

判断的方法主要有两种:引用计数法和可达性分析法;

引用计数法就是每一个对象都会有一个引用计数器,当给对象被引用一次计数器就加1,当断开一个引用计数器就减1,所以当引用计数器的值为0 的时候就表示该对象没有被任何的对象或者变量引用。

可达性分析法:所有的和GC Roots直接或者间接关联的对象都是有效对象,没有关联的都是无效对象,这里的GC Roots指的是:Java虚拟机栈中所引用的对象、本地方法区中静态属性所引用的对象、方法区中常量所引用的对象、本地方法栈中所引用的对象。

注意:引用计数法虽然简单但是存在一个严重的问题就是无法解决循环引用的问题。因此普遍使用的是可发性分析法判断一个对象是否有效。

  1. 回收无效对象的过程是什么

a)        首先判断这个对象是否覆盖了finalize()方法:如果这个对象没有覆盖这个方法,直接释放对象内存,如果该对象覆盖了这个方法并且这个方法还没有被执行,那么就将finalize()方法放到F-Queue队列中,

b)        执行F-Queue队列中的finalize()方法:虚拟机以较低的优先级执行这些finalize()方法,也不会确保所有的finalize()方法都会被执行,如果finalize()方法出现耗时情况虚拟机会直接停止执行并释放内存。

c)        对象重生或者死亡:如果执行finalize()方法的时候,this对象被别的对象或者变量引用,那么该对象就重生了,否者就会被直接释放内存。

二、方法区的内存回收

  1. 方法区中主要清楚的对象:废弃的常量和废弃的类
  2. 如何判断废弃常量:判断废弃的常量和废弃的对象是一样的,只要这个常量不再被任何的对象或者变量所引用,那么该常量就是一个废弃常量。
  3. 如何判断废弃的类:

a)        判断废弃的类要求的条件比较复杂

b)        该类多创建的所有对象都已经被回收

c)        加载该类的ClassLoader类已经被回收

d)        给类的Class类没有被任何的对象或者变量所引用。

三、垃圾收集算法

算法名称

算法的介绍

优点

缺点

引用区域

标记-清除算法

利用前面的判断方法判断出需要清除哪些数据,并给他们做上标记,然后清除被标记的数据。

该算法标记和清除的过程效率都很低,清除后会有大量的碎片空间,导致无法存储大对象,降低空间利用率。

复制算法

将内存分成两份,只在其中一块上存储数据,当需要回收垃圾时,也是首先标记出废弃的数据,然后将有用的数据复制到另一部分内存,再把第一块内存全部清除。

这种算法避免了碎片空间

但是内存被缩小一半,因为每次都要将有用的数据全部复制一遍,效率不高

新生代中使用:为了防止内存碎片化:堆内存的新生代进一步划分为:Eden区+Survior1区+Survior2区

标记-整理算法

在回收垃圾之前首先将所有被废弃的对象做上标记,然后将所有未被标记的对象移动到另一边,最后清空另一边区域即可

这是一种老年代的垃圾收集算法

分代收集算法

将内存划分为新生代和老年代。新生代中存放“朝生夕死”的对象,老年代中存放寿命较长的对象,然后再不同的区域使用不同的垃圾收集算法

四、如何解决空间利用率的问题:

在新生代中,由于大量的对象都是“朝生夕死”,也就是一次垃圾收集后只有少量对象存活,因此我们可以将内存划分成三块:Eden、Survior1、Survior2,内存大小分别是8:1:1。分配内存时,只使用Eden和一块Survior1。当发现Eden+Survior1的内存即将满时,JVM会发起一次MinorGC,清除掉废弃的对象,并将所有存活下来的对象复制到另一块Survior2中。那么,接下来就使用Survior2+Eden进行内存分配。通过这种方式,只需要浪费10%的内存空间即可实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题。

五、什么叫做分配担保:

当JVM准备为一个对象分配内存空间时,发现此时Eden+Survior中空闲的区域无法装下该对象,那么就会触发MinorGC,对该区域的废弃对象进行回收。但如果MinorGC过后只有少量对象被回收,仍然无法装下新对象,那么此时需要将Eden+Survior中的所有对象都转移到老年代中,然后再将新对象存入Eden区。这个过程就是“分配担保”。

六、老年代为什么不使用复制算法而要使用标记-整理算法:

       老年代中存放的对象一般寿命都比较长,因此每次垃圾回收之后都会有大量的对象存活,因此如果选用复制算法的话,每次需要复制大量的对象,会导致效率极低,而且在新生代中使用“复制算法”,当Eden+Survior中都装不下某个对象时,可以使用老年代的内存进行“分配担保”,而如果在老年代使用该算法,那么在老年代中如果出现Eden+Survior装不下某个对象时,没有其他区域给他作分配担保。因此,老年代中一般使用“标记-整理”算法。

七、Java中引用的种类:根据声明周期的长短,将引用分为四类:强引用、软引用、弱引用、虚引用。这四类引用的声明周期依次递减。

原文地址:https://www.cnblogs.com/BaoZiY/p/10632212.html