垃圾回收(Garbage Collection,GC )使很多人联想到java虚拟机的垃圾回收工作原理。 在C/C中,内存必须由程序员管理,程序员在使用时必须首先新建对象,并在使用完成后使用delete等关键字释放资源。 但是,java在分配和回收内存方面不需要成员的关注,一切都交给虚拟机处理,因此,这里介绍了如何了解虚拟机的工作原理和清除垃圾。
1、如何确定“垃圾”
既然是垃圾回收机制,第一步就一定要定位垃圾,知道垃圾就可以回收。 但是怎么确定垃圾? 什么是垃圾?
什么是“垃圾”
首先,为了理解什么是“垃圾”,垃圾回收的工作原理是回收堆内存中的对象,具体的内存划分如下: () )堆栈中的对象不需要考虑回收机制。 在Java中,通过堆存储器内的对象与堆栈存储器内的参照相互关联来使用。 是堆内存回收,堆内存包含引用对象的实体,因此会回收与任何引用都不关联的实体对象。
因此,“垃圾”实际上是指在java虚拟机的堆内存中没有被引用且永远不会被访问的物理对象。
引用计数法
既然知道了什么是“垃圾”,该怎么找垃圾呢? 很明显,从定义中可以看出,未被引用的对象是垃圾,因此可以通过查看对象当前被引用的数量来确定是否应该回收。
例如,如果一个引用具有相关联的实体对象,则将对象的引用数加1,取消引用将减1。 如果引用数为0,则被系统回收。 虽然看起来很简单,但效率很高,但存在——循环引用的弊端。 如果两个对象相互引用,则每个对象的引用数为1,但对象永远无法访问,如下例所示:
公共类主{
publicstaticvoidmain (字符串[ ] args ) {
我的对象对象1=new my object (;
我的对象对象2=new my object (;
object1.object=object2;
object2.object=object1;
对象1=null;
对象2=null;
}
}
class MyObject{
公共对象对象=null;
}
这两个对象最后都被分配空值,但不会被回收,因为计数不是0。 所以JAVA没有采用这个方法。 (Python采用引用计数法)
可达性分析法
从一系列称为“GC Roots”的对象开始,从这些节点开始向下搜索。 搜索通过的路径称为参照链,如果GC Roots中没有参照链的项链,则证明该对象不可用。
Java中的GC Roots对象包括虚拟机堆栈中引用的对象、方法区域类的静态属性中引用的对象、常量中引用的对象和本地方法中引用的对象。
当然,在回收过程中,如果不是被判断为不能到达的对象,则成为可以回收的对象,一般而言,如果不是标记了至少两次以上的对象,则有可能成为可以回收的对象。
常见,在能够将对象判定为回收对象情况下
)1)将显示的对象参照代入空(null )
对象obj=new object (;
obj=null;
)将显示的对一个对象的引用指向新对象
Object obj1=new Object (;
Object obj2=new Object (;
obj1=obj2;
)3)生命周期结束的对象
for(intI=0; i10; I )
{
int Object obj=new Object (;
}
)4)仅弱引用及其相关对象
weakreferencewr=newweakreference (new字符串(' world ' );
2、典型垃圾收集算法
第一步确定了什么是“垃圾”后,下一步显然是垃圾回收。 由于javavirtualmachinespecification没有明确规定如何实现垃圾回收器,所以每个供应商的virtual machine实现垃圾回收器的方式不同,因此这里有几种常见的垃圾回收协议
1 .英俊大船-Sweep (标记-过关)算法
这是最简单的算法,也是最基础最容易实现的算法。 标记算法包括两个阶段:标记阶段和清除阶段。 标记是找到并标记所有需要回收的对象。 清除阶段是回收被标记的对象占用的空间。
如上图所示,可以轻松回收内存,但也存在内存碎片易出现,无法为内存碎片较大的对象分配足够的空间,从而降低内存利用率的缺点。
2 .复制算法
针对书写明了法容易碎片化的问题,该方法提出了新的思想。
也比较容易理解。首先将可用内存空间按照大小平均分为两部分,在使用时只使用其中的一部分,另一部分不使用。当那一部分满了之后,触发收集机制,将还存活的对象复制到另一块内存上面,然后把当前内存的空间一次清理掉,这样就不容易出现内存碎片了。
具体流程:如图,(1)上半部分内存使用。(2)用满后,执行算法,存活的复制到下半部分。(3)下半部分内存使用。(4)下半部分用满后,执行算法,存活的复制到上半部分。
虽然该方法简单,且不易产生碎片,但是却付出了壮观的小懒猪的代价——将可使用的内存空间缩减到原来的一半。而且该算法的效率跟存活对象的数目多少有很大的关系,如果存活的对象很多 那么效率就会大大降低。
3.英俊的大船-Compact(标记-整理)算法
为了解决复制算法的缺陷,充分利用存储空间,提出了标记整理算法。该算法结合了标记清除和复制算法,分为两个阶段。第一阶段:同标记清楚算法,先标记出待回收的对象。第二阶段,不是直接清楚可回收对象,而是将存活对象都向一端移动,然后清理掉可回收的内存。
该方法简单易懂,结合以上两种算法的优势,但是相比之下,效率较低,而且会随着存活对象的增加而降低效率。
4.Generational Collection(分代收集)算法
目前大多数JVM的垃圾收集器采用的算法都是分代回收算法。它的核心思想是,根据对象存活的生命周期将内存划分为若干个不同的区域。因为每个对象的生命周期都是不一样的,有些对象是与业务相关的,比如线程、Scoket、Http请求中的Session等,生命周期就比较长;但是还有一些,如局部变量、临时变量等,这些的生命周期就会比较短。如果不根据存活时间进行区分,每次收集都扫描全部的对象的话,会花费较长的时间。而且对于长生命周期的对象而言,多次的这种遍历是没有效果的,他们仍然存在,导致效率低下。
因此,分代垃圾回收机制是采用了分治的思想,进行代的划分,不同的生命周期对象放在不同代上,对于不同代采用不同的最适合它的算法进行垃圾回收。
目前大部分收集器会划分成三代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。
年轻代(fkdpkq代)
该区域主要存放生命周期较短的对象,所以每次垃圾回收中都需要回收大部分对象,因此该区域采用复制(Copying)算法,也就是说复制操作少,效率不会太低。
但是在实际中,对于fkdpkq代的空间并不是1:1的划分,为了提高空间的利用率,一般将fkdpkq代划分为一块较大的pgddmj空间和两块较小的Survivor空间,每次使用pgddmj空间和一块Servivor空间,当回收时,将pgddmj和Servivor中存活的对象复制到另一块Servivor空间中,然后清理掉pgddmj和刚刚用过的Survivor空间。保证始终有一个Servivor空间是空闲的,在触发收集算法时,对于从上一个Survivor区复制来还存活的对象,将被复制到“老年代”。注意,在一块Servivor中可能同时存在从pgddmj和上一块Servivor中复制来的对象,但是只有从Servivor复制来的对象,可以被复制到老年代。同时,程序可以根据需要,配置多个(多余两个)Survivor区,这样可以增加对象在年轻代中的时间,减少被放到年老代的可能性。
年老代
年老代一般都是生命周期较长的,或者在年轻代经历了N次垃圾及回收后仍然存活的对象,就会被放到年老代中。因此该区的特点是每次回收都只有少数对象被回收,所以一般使用的是标记整理(英俊的大船-Compact)算法。
永久代(持久代)
它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如 Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
3、什么情况下触发垃圾回收
由于根据对象的生命周期进行了分代,所有不同区域的回收时间和方式是不一样的,主要有两种类型:Scavenge GC和Full GC。
Scavenge GC
这个是对fkdpkq代的回收方法,一般情况下,当fkdpkq代空间pgddmj申请失败时就会触发Scavenge GC,进行fkdpkq代的回收,执行复制算法,将存活的对象复制到Survivor区。
但是不会影响老年代,由于一般pgddmj区不少很大,所以pgddmj区的GC会频发进行。
Full GC
这个是对整个堆进行整理回收的方法,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。
有如下原因可能导致Full GC:
年老代(Tenured)被写满
持久代(Perm)被写满
System.gc()被显示调用
上一次GC之后Heap的各域分配策略动态变化
4、典型的垃圾收集器
垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。所以在此不做重点,有感兴趣的可以自己去了解。
G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。