【初步学习内存回收机制】
jvm内存的内存回收机制主要是针对堆来讲的
因为栈的内存回收机制随着一个基本类型的创建与销毁而进行创建而销毁,比较固定。
【如何判断对象是否死亡】
早期有两种算法:
第一种是【引用计数算法】:
给对象添加一个计数器,对象被引用,则+1,对象引用失效时,就-1。
任何时刻计数器为0的对象都是可以被清除的。
但是这里有一个问题,是我在后面【小狗】那一篇博文中记录的一个片段,当两个对象互相引用的时候,
他们就算都没有实际存在价值了,也无法被这种算法识别。
第二种是【可达性分析算法】:
首先确定一个【GcRoots】对象作为起始点,向下搜索,走过的路经叫做【引用链】,如果一个对象到没有与引用链相连,则 认为他是没有存在价值的一个对象,可以进行回收。
然而被认为没有存在价值的对象也不是被直接就回收掉的,他会先被标记并进行一次筛选,
筛选条件:
此对象是否有必要执行finalize()方法,当对象没有覆盖这个方法或者已经被调用过,被视为没必要执行;
此对象如果有必要执行finalize()方法,这个对象会被放在一个F-Queue的队列中,并在之后被一个虚拟机自动建立的,低优先级的Finalizer线程去执行它,所谓的执行时指虚拟机会触发这个方法,但并不承诺会等待他结束,这是为了防止发生异常造成内存崩溃。之后GC会对F-queue中对象进行第二次标记,如果对象此时与GcRoot的引用链连接上了,就逃脱了被回收的命运,反之被回收。
【一种特例对象】
在回收机制算法的背后,体现的是一个对象的状态,分为被引用和没有被引用两种。
但实际对于内存分析来说,还有一种对象,没什么实际用处,但是依然在被引用着,
是否可以有一种方式,可以让这些鸡肋的对象在内存充足时可以保留,内存不足时则被抛除。
因此jvm1.2之后,java对引用的概念进行了扩充,将引用分为了【强/软/弱/虚】四种类型:
【强引用】:new一个对象这种普遍存在的;
【软引用】:softReference类,描述的时有些用但非必需的对象;系统要发生oom异常之前,将会把对象收进回收范围内进行二次回收,若果仍然不能提供足够内存,才会报异常。
【弱引用】:强度比软引用更弱一些,弱引用的对象只能存活到下一次GC之前,由weakReference实现。
【虚引用】:需引用不会对对象的生存时间构成影响,也无法通过虚引用来创建对象实例,作用是这个对象被回收时,会获得一个系统通知,由phantomReference实现。
【垃圾收集算法】:
【复制算法】
这种算法会将内存划分为两个相等的块,每次只使用其中一块。当这块内存不够使用时,就将还存活的对象复制到另一块内存中,然后把这块内存一次清理掉。这样做的效率比较高,也避免了内存碎片。但是这样内存的可使用空间减半,是个不小的损失。
把堆均分成两个大小相同的区域,只使用其中的一个区域,直到该区域消耗完。此时垃圾回收器终端程序的执行,通过遍历把所有活动的对象复制到另一个区域,复制过程中它们是紧挨着布置的,这样也可以达到消除内存碎片的目的。复制结束后程序会继续运行,直到该区域被用完。
但是,这种方法有两个缺陷:
对于指定大小的堆,需要两倍大小的内存空间,
需要中断正在执行的程序,降低了执行效率
【标记-清除算法】
这种垃圾回收一次回收分为两个阶段:标记、清除。首先标记所有需要回收的对象,在标记完成后回收所有被标记的对象。这种回收算法会产生大量不连续的内存碎片,当要频繁分配一个大对象时,jvm在新生代中找不到足够大的连续的内存块,会导致jvm频繁进行内存回收(目前有机制,对大对象,直接分配到老年代中)
【标记-整理算法】
这是标记-清除算法的升级版。在完成标记阶段后,不是直接对可回收对象进行清理,而是让存活对象向着一端移动,然后清理掉边界以外的内存
【分代收集算法】
当前商业虚拟机都采用这种算法。
首先根据对象存活周期的不同将内存分为几块即新生代、老年代,
然后根据不同年代的特点,采用不同的收集算法。
在新生代中,每次垃圾收集时都有大量对象死去,只有少量存活,所以选择了复制算法。
而老年代中因为对象存活率比较高,所以采用标记-整理算法(或者标记-清除算法)
新生代:包括一个Eden以及两个Survivor区,分别称为s0,s1;初始化后,第一次堆中的对象,放置在Eden中,当此区域内存满了之后,进行一次GC,然后将Eden中存活的对象放到s0中,每次存货对象被GC移动至其他区的时候,内置计数器+1,【默认当计数器到了15次后,将存货对象放入老年区,此阈值可配置】当Eden第二次内存满了之后,GC查看Eden及s0区,将存活对象放入s1,并清空Eden及s0,若s1内存满了,存货对象也会进入老年区;当Eden第三次满了之后,GC会查看Eden及s1区,然后存活对象放入s0,以此类推。s0,s1交替存放存活的对象。
老年代:如果某个对象经历了几次垃圾回收之后还存活【上面提了默认8次】,就会被存放到老年代中。老年代的空间一般比新生代大。
【垃圾收集器】:
这里只作简单介绍:
Serial收集器:最基本的收集齐,单线程,在gc时,需要暂停所有其他线程的工作。emmmm
ParNew收集器:Serial的多线程版本,使用复制算法。
Parallel Scavenge收集器:与ParNew相似,但是关注点不同,其他收集器关注点是缩短GC时,减少用户线程的停顿时间,而这个收集齐的目标是i达到一个可控制的吞吐量。
SerialOld收集器,使用标记整理,单线程。
ParallelOld收集器,使用标记整理,多线程。
CMS收集器:获取最短回收停顿时间为目标的收集器。
G1收集器:最新的了,是面试的重点。
(CMS和G1我会在后续深入学习后,再写一篇相关的博客)
【几种常用的GC】
Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。
Major GC vs Full GC
大家应该注意到,目前,这些术语无论是在 JVM 规范还是在垃圾收集研究论文中都没有正式的定义。但是我们一看就知道这些在我们已经知道的基础之上做出的定义是正确的,Minor GC 清理年轻带内存应该被设计得简单:
Major GC 是清理老年代。
Full GC 是清理整个堆空间—包括年轻代和老年代。