java对象什么时候变成垃圾?
java对象的实列都存放于jvm的堆区中,但是堆的内存空间是有限的。在空间不足的时候,便会发抛出内存溢出的异常(OutOfMemoryError),通常在遇到这种情况的时候,我们可以选择把堆空间的内存设置到更大,方法:
第一种 环境myeclipse修改
在菜单window->preferecces.在JDK对话框中输入-Xms512m -Xmx1024m这个参数就可以了
但这终究不是个办法,空间总是有限的,这时候jvm的垃圾回收器就该出马了。
他会做些什么?
对,就是把其中一些对象当作垃圾清除掉。什么?你要把我当作垃圾清除,凭什么?jvm说会说,因为我们判定你已经死亡了啊。
怎样判定一个对象死亡?
- 一个不太成熟的方法:引用计数
大家可能会问,这是什么东西?和jvm运行时数据区域里面的程序计数器有什么联系吗(什么?你不知道程序计数器这个东东,赶紧去看看)。下面且听我慢慢道来。
在很久之前,引用计数是作为判断一个对象是否存活的算法存在的。具体实现是:
为每一个java对象都添加一个引用计数器,当有一个地方引用这个对象的时候,就会是引用计数器的值增加一,当引用失效的时候计数器的值就减一。当对象的引用计数器为零的时候,jvm就认为他凉了,内存不够时,就得把他当垃圾干掉了。但是这种方法存在一个缺陷就是,无法解决对象之间相互循环引用的问题
也许你会问,什么叫做循环引用。我之前也不知道啊,所以搜了下,那就一起看看吧。
class A {
Object instance = null;
static final int 1_M = 1024*1024;
static byte[] bigSize = new byte[2*1_M]; //占用内存用
void main() {
A a = new A();
B b = new B();
a.instanc = b;
b.instance = a;
a = null;
b = null;
System.gc;//手动回收
}
}
事实证明(这个事实需要大家手动式验证一下)我们这次手动的gc并没有导致对象a和对象b被回收啊,说明java的垃圾回收并没有使用引用计数这种算法策略了已经。那是使用了什么呢? 对,就是一种叫做根搜索的算法(到目前为止,我们已经多次提到了 引用这个词,如果还不是很理解的小伙伴,需要补补咯)。
在根搜索算法中有一个重要的点就是 “GC ROOT” 对象,稍后介绍哪些可以作为GC ROOT 对象。根搜索算法的思路就是,以GC ROO对象为起点,从这个起点开始往下搜索,搜索所走过的路线叫做引用链,当一个对象到GC ROOT 对象诶有什么引用链的时候,证明这个对象不可用,就会被纳入GC观察区了(这个名词楼主yy的,并非官方)。
说了这么多概念,来个小列子考考大家了:
以上图片中对象 5、6、7是否可用?
差点忘了很重要的一点,什么样的对象才有资格作为很牛逼的GC ROOT呢?
- 虚拟机栈(这个概念不懂又得八百字补补了)中的引用对象
- 方法区中的静态属性引用对象
- 方法区中的常量引用对象
- 本地方法栈中JNI的引用对象(这个楼主也不懂,还望哪位小伙伴指导)
好好记住上面几个区域里面的引用对象,可是决定着很多人的生死呢
生存还是死亡?
上面说完了jvm判断一个对象生死的算法。在跟搜索算法中,当GC ROOT不可达的时候,我们只说这个对象不可用,并不一定意味着他就死亡。我们可以称这些不可达的对象正处于缓刑阶段,所以还是有机会自救的。这到底是个什么情况?
在一个对象真正死亡之前,他往往需要两次标记。当一个对象与GC ROOT不可达的时候,会被进行一次标记,并且会进行一次筛选。筛选的条件是此对象是否有必要执行finalize()(一般用于资源回收,在执行GC方法之前执行)方法。当对象没有覆盖finalize() 方法或者finalize()方法已经被虚拟机调用过,则jvm会认为 “没有必要执行”。
如果 “有必要执行 ” 会怎么样呢?
如果 某个对象 被认为 有必要执行finalize() 方法,对象会被放入一个 F-Queue队列中。并且在稍后会被虚拟机建立的线程(Finalizer)去执行。这里的执行是指会有这个专门的线程执行 finalize()方法。
处于缓刑的对象的自我救赎:
对象第一次标记并被放入F-Queue队列里面,并且会执行finalize()方法。这时候对象的自我救赎机会来了。对象在finalize()方法中将自己与引用链上的任何一个对象建立连接,如果连接建立成功,将会被移出F-Queue。如果没有连接成功,那就真的离gg不远了。
接下来就来段自我救赎的代码演示了:
package json;
public class SaveAndGC {
public static SaveAndGC gg = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
Syso("finalize start---");
//开始自救表演
SaveAndGC.gg = this;
}
public void main() throws InterruptedException {
gg = new SaveAndGC();
//干掉引用链,称为缓刑者
gg = null;
//触发GC操作‘
System.gc(); //会执行finalize()方法,并完成自我救赎
Thread.sleep(500); //考个知识点:wait()和sleep()的区别
if(gg != null) {
Syso(“自救成功!!”)
}else {
Syso(“自救失败!!”)
}
gg = null;
//触发GC操作‘
System.gc(); //会执行finalize()方法,并完成自我救赎
Thread.sleep(500); //考个知识点:wait()和sleep()的区别
if(gg != null) {
Syso(“自救成功!!”)
}else {
Syso(“自救失败!!”)
}
}
}
上面进行了两次GC操作,第一次 自救成功 第二次 自救失败。大家可以结合前面的知识分析原因。
垃圾收集算法
前面的知识应该说都是铺垫,从这里开始,才是真正的好戏
- 标记清除算法
这个算法是最基础的算法,后续算法都是基于此来改造的。
核心分为两步:
- 标记。正如前面提到的标记一样,这里就是标记哪些对象为可以回收的对象
- 清除。移除掉那些被标记的对象
缺点:
- 效率太低
- 空间浪费,由于对象的分布问题,会导致很多碎片化的空间,在分配较大内存对象的时候很难找到合适的连续内存空间
效果图如下所示:
- 复制算法
通过对标记清除算法的了解,我们知道标记清除在效率和空间问题上存在一定的缺陷。于是,大牛们又想出了一种叫做复制的垃圾回收算法。
将内存按容量分为大小相等的两份,每次分配内存只使用其中的一份,当一份用完的时候,就将这份上面还存活着的对象一次性复制到另一份上面,然后把已经使用过的那一份内存全部清理掉。
优点:
不用考虑内存碎片等复杂情况,只需要移动堆指针,按顺序分配内存即可,实现简单,运行高效。
缺点:
将内存缩小到原来的一半
运行效果图如下:
在现在的商用虚拟机中,常常会将内存分为新生代内存区域和老年待内存区域,复制算法就是被大量应用在新生代内存区域中。下面会详细介绍这两种内存区域:
复制算法常常被用在新生代区域,但是由于新生代对象具有朝生夕死的特性,所以在新生代中,使用复制算法的时候,区域不用按照1:1的比例来划分。在新生代区域中,内存被划分为一块Eden区域和两块Survivor区域。Eden和其中的一块Survivor区域被用来作为存放对象的区域,还有一个Survivor区域被用来作为拷贝区域使用。
同时,在新生代区域中。Eden和Survivor区域是按照8:1的比例划分的。列入新生代被分配到10M内存,其中Eden占有8M,每个Survivor占有1M。
如上图所示:其中Edene、Survivor_01被用来存储新产生的对象,Survivor_02被用来作为拷贝区域。
现在假设一种情况:
- 整个新生代内存区域为100M
- 所以:Edene = 80M,Survivor_01 = 10M,Survivor_02 = 10M
所以现在新生代可以容纳的最大新生对象空间为90M(暂时不考虑某一个对象空间大于90M的情况),拷贝区域可以容纳的对象空间为10M。假设现在进行一次新生代垃圾回收(垃圾回收分为新生代和老年待垃圾回收),死亡的对象空间为20M,所以剩下的存活对象为70M,如果要进行基于复制算法的垃圾回收,Survivor_02的10M内存空间显然是不够的。这时候就需呀老年代进行空间的分配担保了。
这里稍微补充一点关于新生代和老年代的知识:
Java堆中是JVM管理的最大一块内存空间。主要存放对象实例。
在JAVA中堆被分为两块区域:新生代(young)、老年代(old)。
堆大小=新生代+老年代;(新生代占堆空间的1/3、老年代占堆空间2/3)
默认创建的对象都是先放在新生代,当gc收集发生之后,若该对象没有没回收,并且达到了老年代的年龄,就被转移到老年代
结合前面的知识,我们来介绍另一种专门应用于老年代的垃圾收集算法:标记整理算法。
如下图所示:核心在于让存活的对象向一端游走
分代收集算法
核心便是:将内存分为新生代和老年代,然后在新生代中使用复制算法,在老年代中使用标记整理算法。
相信如果是第一次接触jvm相关知识的小伙伴,一定对这篇笔记还有很多疑惑。不用担心,我将在下一篇笔记中为大家带来一个对象的出生过程,详细讲解在最初对象是怎样在内存中划分的。
好了,关于垃圾回收的部分就介绍到这里,如有错误还望多多指教。