java中的引用的定义很传统:如果reference(引用)类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表这一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只被引用或者没有被引用两种状态,对于如何描述一些处于判刑中又或者我们想扔又不舍得的对象就显得无能为力。
我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
那么其实我们可以想到,我们的对象的生命周期中一定会很多种的状态,然后我们会在各个状态都给出一个相应的描述
在java中,对象的生命周期包括以下几个阶段
1创建阶段
2应用阶段
3不可见阶段
4不可达阶段
5收集阶段
6终结阶段
7对象空间重分配阶段
在聊这个之前,我觉得我们应该先聊一聊对象的引用,一般来说,我们的引用有四种
在java中最常见的就是强引用,也是我们在开发过程中经常会使用到引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收,即使该对象以后永远都不会被用到jvm也不会回收。因此强引用是造成内存泄漏的主要原因之一。
软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统空间内存不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用需要用到WeakReference类实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管jvm的内存空间是否足够,总会回收该对象占用的内存。
虚引用需要PhantomReference类实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
其实我们在探讨类加载的时候就已经探讨了一部分对象创建的情况
为对象分配存储空间
开始构造对象
从超类到子类对static成员进行初始化
超类成员变量按照顺序初始化,递归调用超类的构造方法
子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
对象至少被一个强引用持有着。
当一个对象处于不可见阶段时,说明程序本身不在持有该对象的任何强引用,虽然这些引用仍然是存在着的。简单来说就是程序的执行已经超出了该对象的作用域了。
对象处于不可达阶段是指该对象不再被任何强引用持有
与不可见阶段相比,不可见阶段是指程序不再持有该对象的任何强引用,这种情况下,该对象仍然可能被jvm系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为GC root。存在着这些GC root会导致对象的内存泄漏情况,无法被回收。
当垃圾回收器发现该对象已经处于不可达阶段并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入收集阶段。如果该对象已经重写了finalize()方法,则会去执行方法的终端操作
这里要特别说明一下:不要重载finazlie()方法原因有两点
在分配该对象时,jvm需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗cpu时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。
在finalize()方法中,如果有其他的强引用再次持有该对象,则导致对象的状态由收集阶段又重新变为应用阶段。这个已经破坏了java对象的生命周期进程,且复活的对象不利用后续的代码管理。
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收
垃圾回收器对该对象的所占用内存空间进行回收或者再分配,则该对象彻底消失了,称之为对象空间重新分配阶段。
GC是由jvm自动完成的,根据jvm系统环境而定,所以时机是不确定的。当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知jvm进行一次垃圾回收,但是具体什么时刻运行也无法控制,也就是说System.gc()只是通知要回收,什么时候回收由jvm决定。但是不建议手动调用该方法,因为GC消耗的资源比较大。
(1)当Eden区或者S区不够用了
(2)老年代空间不够用了
(3)方法区空间不够用了
(4)System.gc()
已经能够确定一个对象为垃圾之后,接下来要考虑就是要回收,怎么回收呢?,得要有对应的算法,下面介绍常见的垃圾回收算法。
标记-清除
标记
找出内存中需要回收的对象,并且把它们标记出来
此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
清除掉被标记需要回收的对象,释放出对应的内存空间
缺点
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作
(1)标记和清除两个过程都比较耗时,效率不高
(2)会产生大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另外一次垃圾回收。
将内存划分为两块相等的区域,每次只使用其中一块,如下图
当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉
缺点:空间利用率降低
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况,所以老年代一般不能直接选用这种算法。
标记过程仍然与标记清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
其实上述过程相对复制算法来讲,少了一个保留区
让所有存活得对象都向一端移动,清理掉边界以外的内存
既然上面介绍了3种垃圾收集算法,那么在堆内存中到底用哪一个呢?
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没有必要,不如做个标记再清理)