Java GC详解
垃圾收集
简介
概述
垃圾收集并不是java语言的产生物.早在1960年第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生
垃圾收集机制是java的招牌能力,极大地提高了开发效率.如今垃圾收集几乎成为现代语言的标配,即时经过如此长时间的发展,java的垃圾收机制任然在不断的演进中,不同大小的设备,不同特征的应用场景,堆垃圾收集提出了新的挑战
什么是垃圾
垃圾是指运行程序中没有任何指针指向的对象,这个对象就是要被回收的垃圾.
如果不及时堆内存中的垃圾进行清理,那么,这些垃圾对象所占用的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象调用.甚至可能导致内存溢出.
为什么需要GC
对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不尽兴回收,就好像不停地生产生活垃圾而从来不打扫一样
除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片.碎片整理将所占用的内存移到堆的一端,以便jvm将整理出的内存分配给新的对象.
随着应用程序所应付的业务越来越大,复杂,用户越来越多,没用GC就不能保证应用程序的正常进行.而精彩造成stw的GC又跟不上实际需求,所以才会不断地尝试对GC进行优化
早起的垃圾回收
再早起的c/c++时代,垃圾回收基本上是手工进行的.开发人员可以使用new关键字进行内存申请,并且使用delete关键字进行内存释放.
这种方式可以灵活控制内存释放的时间,但是会给开发人员来频繁申请和释放内存的管理负担.倘若有一处内存空间由于程序员编码的问题忘记被回收,就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,知道出现内存溢出并造成应用程序崩溃.
现在主流的高级语言都是用了自动垃圾回收的思想,这也是未来发展的趋势.
java垃圾回收
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
没有垃圾收集齐,java也会和c++一样,各种垂悬指针,野指针,泄漏内存等问题让你头疼不已
自动内存管理机制,将程序员从繁重的内纯牛奶管中释放出来,可以更专心的专注于业务研发
gc主要是在方法区和堆区
垃圾回收期可以对年轻代垃圾回收,也可以对老年代回收,甚至是全堆和方法区的回收
其中java堆是垃圾收集器的工作重点
从次数上将
- 频繁收集young区
- 较少收集Old区
- 基本不动永久代(元空间)
垃圾回收算法
标记阶段
对象存活判
在堆里存放着几乎所有的java对象实例,在gc执行垃圾回收之气,首先需要分区出内存中那些事存货的对象.只有被标记为以及死亡的对象,gc才会在执行垃圾回收时,释放掉期所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段
那么在jvm中究竟是如何标记一个死亡对象的呢?简单来说,当一个对象以及不再被任何的存活对象继续引用的时候,就可以宣布以及死亡
判断对象存货有俩种方式:引用计数法和可达性分析算法
引用计数法
引用计数算法比较简单,对每个对象保存一个整型引用计数器书写.用于记录对象呗引用的情况
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器加1:当引用失效时,引用计数器就减少1.只要对象A的引用计数器指为0,则表示对象A不可能再被使用,可进行回收.
有点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟性.
缺点
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销.
- 每次赋值都需要更新计数器,潘苏子和加法和减法操作,这增加了时间开销
- 引用计数器有一个严重的问题,即无法处理循环引用的的情况.这是一条致命缺陷,导致java的垃圾回收期中没有使用这类算法.
python解决方式
手动解除:很好理解,就是在合适的实际解除引用关系
使用弱引用,weakref,weakref是python提供的标准库,目的在解决循环引用
可达性分析算法
概述
相对于引用计数法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效的解决引用计数算法循环引用的问题,防止内存泄漏发生
相较于引用计数器算法,这里可达性分析算法那就是java,c#所选择的.也叫做追踪性垃圾收集,或跟搜索算法.
思路
可达性分析算法是以跟对象集合(GC Roots)为起始点,按照从上至下的方式搜索被跟对象集合所连接的目标对象是否可达
使用可达性分析算法后,内存中的存活对象都会被跟对象集合直接或间接连接着,欧索走过的路径称为引用链
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象以及死亡,可以标记为垃圾对象
在可达性分析算法中,只有能够被跟对象集合直接或间接连接的对象才是存活的对象.
GC Roots
在java语言中,GC Roots包括以下元素
虚拟机栈中引用的对象
- 比如各个线程被调用的方法中使用到的参数,局部变量等
本地方法栈内JNI(通常说的本地方法native)引用的对象
方法区中静态属性引用对象
- java类的引用类型静态变量
方法区中常量引用的对象
- 比如:字符串常量池(String table)里的引用
所有被同步锁synchronized持有的对象
java虚拟机内部的引用
- 虚拟机数据类型对应的class对象,一些常驻的异常对象,系统类加载器
反映ava虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等.
除了这些固定的GC roots集合以外,根据用户所选用的垃圾收集器以及当前时回收的内存区域不同,还可以有其他对象临时地假如,共同完整GC Roots集合.比如分带收集和局部回收
如果只针对java堆中的某一块区域进行垃圾回收(比如典型的只针对新生代),必须考虑到内存区域是虚拟机实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集中去考虑,才能保证可达性分析的准确性
小技巧:
由于root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个root
如果要是用可达性分析算法来判断内存是否1可回收,那么分析工作必须在一个能保障一致性的快照中执行.这点不满足的话,分析的结果准确性也就无法保证.
这点也是导致GC进行时必须Stop the world的一个重要原因.
即时是号称几乎不会发生挺短的CMS收集器中,美剧根节点时,也是必须要停顿的.
对象的finalization机制
简介
java语言提供了对象种植(finalization)机制来允许开发人员提供对象被销毁之前的自定义逻辑处理
当垃圾回收期发现没有引用指向一个对象:即垃圾回收次对象之前,总会调用这个对象的finalize()方法
finalize()方法允许在子类中被重写,用于在对象呗回收时进行资源释放.通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据库连接等
注意
永远不要主动调用某个对象的finalize()方法,一个交给垃圾回收机制调用,理由如下
- 在finalize()时,可能会导致对象复活.
- finalize()方法执行的时间是没有保障的,它完全有GC线程决定我,极端情况下若不发生GC,则finalize()方法奖没有执行的机会
- 从功能上来说,finalize()方法与C++中的析构函数比较相类似,但是java采用的是给予垃圾回收期的自动内存管理机制,所以finalize()方法在本质上不同于c++中的析构函数
- 由于finalize()方法的存在,虚拟机中对象一般处于是那种可能的状态
生存还是死
如果所有的根节点都无法访问到某个对象,说明对象以及不再使用了.一般来说次对象需要被回收,但事实上,也并非是非死不可的,这时候它们暂时处于缓刑阶段.一个无法解除的对象可能在某一个条件下复活自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态.如下
- 可触及的:从根节点开始,可以到达这个对象
- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态.不可触及的对象不能再被复活,因此finalize()只会被调用一次
以上三种状态中,是由于finalize()方法的存在,进行的区分,只有在对象不可触及时才可以被回收.
具体过程
判定objA是否可回收,至少需要经历俩次标记过程
如果对象objA到GC Roots没有引用链,则进行第一次标记
进行筛选,判断次对象是否有必要执行finalize()方法
如果对象objA没有重写finalize()方法,或者finalize()方法以及被虚拟机调用过,则虚拟机视为没有必要被执行,objA被判定为不可触及的.
如果对象objA重写了finalize()方法,并且还未执行过,那么objA会被插入到F-queue队列中,有一个虚拟机自动创建的,低优先级的Finalizer线程触发其finalize()方法执行
finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-queue队列中的对象进行第二次标记.如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么第二次标记时,objA回避诶移除即将回收集合.之后对象会再出出现没有引用存在的情况.在这个情况下,finalize方法不会被再次调用各,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次.
public class CanReliveObj {
public static CanReliveObj obj;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj=this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
public static void main(String[] args)throws Exception {
obj=new CanReliveObj();//对象第一次成功拯救自己
obj=null;
System.gc();//垃圾回收器
System.out.println("第一次gc");
//因为Finalizer线程优先级很低
Thread.sleep(2000);
if (obj==null){
System.out.println("对象死亡");
}else{
System.out.println("对象存活");
}
System.out.println("第二次gc");
//下面这段代码与上面的完全相同,但是这次却自救失败了
obj=null;
System.gc();//垃圾回收器
System.out.println("第一次gc");
//因为Finalizer线程优先级很低
Thread.sleep(2000);
if (obj==null){
System.out.println("对象死亡");
}else{
System.out.println("对象存活");
}
}
}
清除阶段
标记清除算法
标记清除算法是一种非常基础和常见的垃圾收集算法,该算法被j.McCaryhy等人在1960年提出并应用于Lisp语言
执行过程
当堆中的有效内存空间被耗尽的时候,就会整个停止程序,stw,然后进行俩项工作,第一项则是标记,第二项是清除
- 标记:Collector从引用根节点开始遍历,标记所有被引用的对象.一般是在对象的Header中记录为可达对象
- 清除:Collector对堆内存从头到尾进行线性遍历,如果发现某个对象在Header中没有标记为可达对象,则将其回收
缺点
效率不算高
在进行GC的时候,需要让整个应用,导致用户体验极差
这种方式清理出来的空闲内存是不连续的
复制算法
核心思想:
将或者的内存空间分为俩快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到违背使用的内存块中,之后清除正在使用的内存块中的所有对象,交换俩个内存的角色,最后完成垃圾回收
优点
没有标记和清除过程,实现简单,运行高效
复制过去优化保证空间的连续性,不会出现碎片问题
缺点
此算法的缺点也是很明显的,就是需要俩倍的内存空间
对于G1这种拆分大量的region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不会小.
特别的:如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大或者说非常低才行.
应用场景
在新生代,堆常规应用的垃圾回收,一次通常可以回收70-99的内存空间.回收性价比很高.所以现在的商业虚拟机都是
标记压缩
标记清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后悔产生内存碎片,所以jvm设计者需要在此基础上进行改.标记压缩算法由此诞生(mark-compact)
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存一端,安顺序排放.
之后,清理边界外所有空间
标记压缩算法的最终效果等同于标记清除算法执行完成后,再近些一次内存碎片整理,因此,也可以把它称为标记-清除-压缩算法那
二者的本质诧异在于标记清除算法是一种非移动式的回收算法,标记压缩是移动式的.是否移动回收后的存活对象是一项有点缺点并存的风险决策
可以看到,标记的存活对象将会被整理,按照内存地址一次排列,而未被标记的内存会被清理掉.如此一来,当我们需要给新对象分配内存时,jvm只需要持有一个内存的起始地址即可,这笔维护一个空闲列表显然少了许多开销.
指针碰撞
如果内存空间已规整有序的方式分布,即医用和为用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当心对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞
优点
- 消除了标记清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,jvm只需要持有一个内存的起始地址即可
- 消除了复制算法当中,内存减半的高额代价
缺点
从效率上来说,标记-整理算法要低于复制算法.
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址.
移动过程中,需要全程暂停用户应用程序,即STW
标记清除(Mark-Sweep) | 标记压缩(Mark-Compact) | 复制(Copying) | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间 | 少(会堆积碎片) | 少(不堆积碎片) | 需要存活对象的2倍大小(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
分代收集算法
分代收集算法是基于这样一个事实:不同对象生命周期是不一样的。因此,不同生命周期的对象的生命周期是不一样的。因此,不同生命周期的对象可以采用不同的收集方式,以便提高效率。一般是吧java堆分为老年代和年轻代,这样就可以根据各个年亲代的特点使用不同的回收算法以提高垃圾回收效率。
目前几乎所有的GC算法都是采用分代收集算法执行垃圾回收的
在hospot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短,存活率低,回收频繁
这种情况复制算法的回收整理速度是最快的。复制算法的效率只和当前存货对象的大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不足的问题则通过hotspot中俩个survivor的设计得到缓解
老年代
老年代特点:区域较大,对象生命周期长,存活率高,回收不及年轻代频繁。
这种情况大量存活率高的对象,复制算法明显变得不合适。一般是由比较清楚或是标记清楚与标记整理的混合实现
- 标记(Mark)阶段的开销与存货对象数量成正比
- 清除(Sweep)阶段的开销与所管理区域的大小成正比
- 压缩(Compact)阶段的开销与存活对象的数据成正比
一hotspot中的cms回收期为例,cms是基于mark-sweep实现的,对于对象的回收效率很高,而对于碎片问题,cms采用基于mark-compact算法的Serial Old回收期作为补偿措施:当内存回收不佳,将采用Serial OPld执行Full GC以达到堆老年代内存的整理
增量收集
上述现有的算法,在垃圾回收过程中,应用软件处于一种stop the world的状态,在stop the world状态下,应用程序的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收的时间过长,应用程序会被挂起很久,将严重影响用户体验或系统的稳定性。为了解决则会个问题,即堆实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生
基本思想
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让了垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线只收集一小片区域的内存空间,直接切换到应用程序线程.依次反复,直到垃圾收集完成.
总的来说,增量收集算法的基础乃是传统的标记清除和复制算法.增来收集算法通过对线程间冲突的妥善处理,允许收集线程以分阶段的方式完成标记,清理或复制工作
缺点:
使用这种方式,由于垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统停顿的时间.但是,因为线程切换和上下文转换消耗,会使得垃圾收集的总体成本上升,造成系统吞吐量的下降.
分区算法
一般来说,在相同条件下,堆空间越大,一次GC所需要的时间就越长,有关GC产生的停顿也越长.为了更好控制GC产生的停顿时间,将一块打的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿.
分带收集算法将按照对象的生命周期长短划分成俩个部分,分区算法将这个对空间划分成连续不同小区间region
每一个小区间都独立使用,独立回收.这种算法的好处是可以控制一次回收多少个小区间.
相关概念
System.gc()
在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被的丢弃对象占用的内存,这是一个native方法
然而System.gc()调用附带一个免责声明,无法保证堆垃圾收集齐的调用(只是提醒jvm执行,不确定能执行)
jvm实现者可以通过System.gc()调用来决定jvm的GC行为.二一班情况下.垃圾回收应该是自动进行的,无需手动触发,否则就太过麻烦了.在一些特殊情况下,入我们正在编写一个性能基准,我们可以在运行之间调用System.gc()
当调用System.runFinalization()就会强制调用对象的finalze()方法
public class A {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize被调用");
}
public static void main(String[] args) {
new A();
System.gc();
System.runFinalization();
}
}
//运行结果finalize被调用
内存溢出与泄漏
内存溢出
内存溢出相对于内存泄漏来说,更容易被理解,但是同样的也是引发程序崩溃的罪祸首之一
由于GC一直在发展,所以一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况.
大多数情况下,GC会进行各种年龄段的回收,实在不行了就会来一次独占式的Full GC操作,这时候会回收大量内存,供应用程序继续使用
javadoc中堆OutOfMemoryError的解释是,没空闲内存,并且垃圾收集齐也无法提供更多的内存,并且垃圾收集器也无法提供更多的内存
首先说没有空闲内存的情况:说明java虚拟机的堆内存不够.原因有2
比如:可能存在内存泄漏问题;也有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定jvm堆大小或者指定数值偏小.我们可以通过参数-Xms,-Xmx来调整
代码中创建了大量对象,并且很长时间不能被垃圾收集齐收集(存在被引用)
对于老版本的Oravle JDK,因为永久代的大小是有限的,并且jvm对永久代垃圾回收(入,常量池的回收,卸载不需要的类型)非常不积极,所以我们不断添加新类型的时候,永久代出现OOM也非常多,尤其在运行存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题.对应的异常信息,会标记出来和永久带相关"java.lang.OutOfMemoryError:PermGen space"
随着元数据区的引入,方法去内存已经不再那么窘迫,所以相应的OOM也有所改观,出现OOM异常信息变成了:"java.lang.OutOfMemoryError:Metaspace"直接内存不足,也会导致OOM
这里隐含一层意思是,在炮锤OutOfMemoryError之前,通常垃圾收集齐会被触发,尽其所能区清理空间
例如在引用机制分析中,涉及到jvm会尝试回收软引用指向的内存对象等.
在java.nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间.
当然不是说任何情况下垃圾收集齐都会被触发的.比如我们去分配一个超大独享,类似一个超大数组超过堆的最大值,jvm可以判断出垃圾收集器并不能解决这个问题,所以直接抛出OutOfMemoryError
内存泄漏
介绍
严格说,只有对象不会再被程序用到了,但GC又不能回收他们的情况,才叫内存泄漏.
但实际情况很多时候一些不太好的实践或舒服会导致对象得生命周期变得很长很长甚至导致OOM,也可以叫做宽泛意义上的内存泄漏
机关内存泄漏并不会立即引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutOfMemory异常,导致程序崩溃.
举例
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有堆外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的诞生.
一些提供close的资源关闭导致内存泄漏
数据库连接,和网络连接和io必须手动close否则不能被回收
stop the world
简称STW,指的是GC事件发生过程中,会产生应用程序的停顿,停顿产生时,整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉
,这个停顿称为STW.
可达性分析算法中枚举根节点(GC Roots)会导致所有java执行线程丁顿
- 分析工作必须能在一个能确保一致性的快照中执行
- 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
- 如果出现分析过程中对象引用的关系还在不断发生变化,则分析结果的准确性无法保证
被stw终端的应用程序会在完成GC之后恢复,频繁那中断会让用户感觉像是网速不快造成电影卡带一样,所以我们要减少STW的发生
STW时间和采用哪款GC无关,所有的GC都有这个事件
STW是jvm在后端自动发起和自动完成的,在用户不可见的情况下,把用户造成的工作线程全部停掉
开发中要用System.gc();会导致stw的发生
并行与并发
并发
在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行
并发布时真正意义上的同事运行,只是CPU把一个时间段华丰城几个时间段(时间区间),然后在这几个时间区间之间来回切换,由于cpu处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在运行.
并行
当系统有一个以上的cpu时每当一个cpu执行一个进程时,另一个cpu可以执行另一个程序,俩个进程互不抢占cpu资源,可以同时进行,我们称之为并行
其实决定并行的因素不是cpu的数量,而是cpu的核心数量,比如一个cpu多个核也可以并行
适合科学计算,后台处理等弱交互场景
对比
并发,指的是多个事情,在同一时间内同时发生了.
并行,指的是多个事情,在同一时间点上同时发生了.
并发的多个任务之间是互相抢占资源的
并行的多个任务是不互相抢占资源的
只有在多cpu或者一个cpu多核的情况中,才会发生并行.否则看似同时发生的事情,其实都是并发执行的
垃圾回收的并发与并行
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态.
- 如ParNew,Parallel Scavenge,Parallel Old
串行:Serial
- 相较于并行的概念,单线程执行.
- 如果内存不够,则程序暂停,启动jvm垃圾回收期进行垃圾回收.回收完,再启动程序的线程
并发:
指用户线程余垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行不会停顿用户程序的运行.
- 用户程序在继续运行,而垃圾收集程序线程运行与另一个cpu上
- 如cms,G1
安全点与安全区域
安全点
程序在执行时,并非在所有的地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点(SafePoint)
safe point的选择很重要,如果太少可能导致GC等待时间太长,如果太频繁,可能导致运行时的性能问题,大部分指令的执行时间都非常短暂,通常会根据"是否具有让程序长时间执行的特征"为准.比如方法调用,循环跳转和异常跳转等
如何在发生GC时,检查所有线程都跑到最近的安全点停下来呢
抢先式中断
- 首先终端所有线程.如果还有线程不在安全点,就恢复线程,让线程跑到安全点
主动式中断
设置一个中断标致各个线程运行到safe point的时候主动轮询这个标致,如果中断标致为真,则将自己中断挂起.
安全区域
safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的safepoint.但是程序不执行的时候呢?例如线程处于sleep状态或Blocked状态,这时候线程无法响应jvm的中断请求,走到安全点区中断挂起,jvm不太可能等待线程被唤醒,对于这种情况,就需要安全区域safe region来解决
安全区域是指在一段代码中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的.我们也可以吧safe Region看作是被扩展了的safepoint
实际执行时:
当线程运行到safe Region的代码时,首先标识已经进入了safe Region,如果这段时间发生GC JVM会忽略表示为Safe Region状态的线程
当线程即将离开Safe Region时,会检查jvm是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止
引用
在jdk1.2之后,java堆引用的概念进行了扩充,将引用分为
- 强引用:Strong Reference,最传统的引用定义,是指在程序代码中普遍存在的引用赋值,即类似"Object o=new Object();"这种引用关系,无论在任何情况下,只要强引用关系还存在,垃圾收集齐就永远不会回收掉被引用的对象.
- 软引用:soft Reference,在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中二次回收,如果这次回收后还没有足够的内存,才会跑出内存溢出异常.
- 弱引用:weak Reference:被弱引用关联的对象只能生存到垃圾收集之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象.
- 虚引用:Phantom Refrence:一个对象是否有虚引用的存在,完全不对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例.为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时,收到一个系统通知.
强引用
在程序中,罪常见的类型就是强引用,也是默认的引用类型.强引用的对象是可触及的,垃圾收集齐就永远不会回收掉被引用的对象
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用于或者显式地将相应强引用赋值为null,就可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略.
相对的,软引用,弱引用和虚引用的对象是软可触及,弱可触及,虚可触及的,在一定条件下,都是可以被回收的,所以强引用是造成java内存泄漏的主要原因之一
- 强引用可以直接访问目标对象
- 强引用所指向的对象在任何时候,都不会被系统回收,虚拟机宁愿抛出oom异常也不会回收引用所指向对象
- 强引用可能导致内存泄漏
软能用
软引用是用来描述一些还有用,但非必须的对象,纸杯软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够内存,才会跑出内存溢出异常.
软引用通常用来实现敏感的缓存,比如告诉缓存就有用到软引用.如果还有空闲内存,就可以暂时保留缓存,当内存不足时,清理掉,这样就保证了是哟并缓存的同时不会耗尽内存.
垃圾胡思后期在某个时刻决定回收软可达对象的时候,回清理软引用,并可选地把引用存放到一个软引用队列(Reference Queue)
类似弱引用,只不过java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理.
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
public class A {
public static void main(String[] args) {
Object obj=new Object();//声明强引用
SoftReference <Object>sf=new SoftReference<Object>(obj);
obj=null;//销毁强引用
System.out.println(sf.get());//能打印出对象
}
}
弱引用
弱引用也是来描述那些非必须的对象,纸杯弱引用关联的对象只能生存到下一次垃圾收集发生为止,在系统gc时,只要发现弱引用,不管系统空间使用是否充足,都会回收掉纸杯弱引用关联的对象.
但是由于垃圾回收期的线程优先级很低,并不一定能很快地发现持有弱引用的对象,在这种情况下,弱引用对象可以存在较长的时间.
弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象呗回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况.
软引用,弱引用都非常适合来保存那些可有可无的缓存数据.如果这么做,当系统内存不足时,这些缓存数据都会被回收,不会导致内存溢出.而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class A {
public static void main(String[] args) {
Object obj=new Object();//声明强引用
WeakReference<Object> wk=new WeakReference<Object>(obj);
obj=null;//销毁强引用
System.out.println(wk.get());//java.lang.Object@1540e19d
System.gc();//如果调用了这行代码,就会输出为null,因为gc会直接回收弱引用
System.out.println(wk.get());//null
}
}
弱引用于软引用最大的不同就在于,当GC进行回收时,需要通过算法检查对象是否回收软引用对象,而弱引用对象,GC总是进行回收.弱引用对象更容易,更快被GC回收.
使用weakHashMap存储数据,能让数据及时的进行回收,防止内存溢出
虚引用
也被称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个
一个对象是否有虚引用的存在,完全不会决定对象的生命周期.如果一个对象仅只有虚引用,那么它和没有引用几乎是一样的,随时都可能会被垃圾收集齐回收
它不能单独使用,也无法通过虚引用来获取被引用的对象.当试图通过虚引用的get方法获取对象时,总是null
为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程.比如:能在这个对象呗垃圾收集回收的时候收到一个系统通知.
虚能用必须和引用队列一起使用.虚引用在创建时,必须提供一个引用队列作为参数.当垃圾回收期准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用假如引用队列,以通知应用程序对象的回收情况.
由于虚引用可以跟踪对象的回收时间,因此也可以将一些资源释放的操作放置在虚引用中执行和记录
import java.lang.ref.*;
public class A {
public static void main(String[] args) throws InterruptedException {
Object obj=new Object();//声明强引用
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
PhantomReference<Object> wk=new PhantomReference<Object>(obj,referenceQueue);
obj=null;//销毁强引用
System.out.println(wk.get());//java.lang.Object@1540e19d
System.gc();//如果调用了这行代码,就会输出为null,因为gc会直接回收弱引用
Thread.sleep(1000);//睡眠一会保证确实进行了gc
System.out.println(referenceQueue.poll());
}
}