文章目录
Java虚拟机运行时内存区域: https://blog.csdn.net/DreamsArchitects/article/details/114855532
一、介绍
垃圾收集
(Garbage Collection 简称GC),在Java内存运行时区域
中,程序计数器、虚拟机栈、本地方法栈 3个区域随线程而生,随线程而灭。因此这几个区域的内存分配和回收都具备确定性,但是Java堆
和方法区
这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样。只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。
二、如何确定垃圾?
在Java堆里几乎存放着Java世界所有的对象实例,垃圾收集器在对Java堆进行回收前,第一件事就是要确定这些对象之中哪些还存活着,哪些已经死去了。
2.1 引用计数法
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单 的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,如果一个对象被引用了了,那么计数+1,如果被释放则-1。如果引用计数为 0,那么这个对象就是可回收对象。
但是在Java领域里,都没有选用引用计数法来管理内存,主要原因就是这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,比如单纯的引用计数法就无法解决对象之间相互循环引用的问题。
2.2 可达性分析法
当前主流的商用程序语言的内存管理都是通过可达性分析算法来判断对象是否存活的。通过一系列的被称为“GC roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径成为"引用链",如果某个对象到GC Root 间没有任何引用链,则证明这个对象是不可能在被使用的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记 过程。两次标记后仍然是可回收对象,则将面临回收。
在Java1.2之后,将引用分为强引用
、软引用
、弱引用
、虚引用
四种,这四种引用强度一次逐渐减弱。强引用是最传统的”引用“的定义,类似 "Object obj = new
Object()"这种引用关系,无论任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。
即使在可达性分析算法中判定为不可达对象,也不是必须回收的,真正要回收一个对象,至少要经历两次标记的过程:如果对象在进行可达性分析时发现没有与GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()
方法。假如对象内有覆盖finalize()
方法或者finalize()
方法已经被虚拟机调用过了,那么虚拟机将这两种情况都视为 没有必要执行。
如果这个对象被判定为确有必要执行finalize()
方法,那么该对象将会被放置在一个名为F-Queue
的队列之中,并在虚拟机自动建立的低调度优先级的Finalizer
线程去执行它们的finalize()
方法。
三、垃圾收集算法
3.1 分代收集理论
当前商业虚拟机的垃圾收集器,大多数都遵循了 分代收集
的理论进行设计的,它建立在两个分代假说之上:
- 弱分代假说:绝大多数的对象都是朝生夕灭的。
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
这两个假说共同确定了垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储,显而易见,如果一个区域中绝大多数对象都是朝生夕灭,难以熬过垃圾回收过程的话,那么把它们几种放在一起,每次回收时只关注如何保留少量存活,而不是去标记那些大量要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那么把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存空间有效利用。
设计者至少会把Java堆划分为新生代
(Young Generation)和老年代
(Old Generation)两个区域,顾名思义,在新生代中,每次垃圾收集时都有大批对象死去,而每次收集后存活的少量对象,将会逐步晋升到老年代中去存放。
根据 新生代
朝生夕灭
的特点:新生代中的对象有98%熬不过第一轮收集。把新生代
分为较大的一块Eden
空间和两块较小的Survivor
空间,每次分配内存只使用Eden
和其中的一块Survivor
空间。发生垃圾收集时,将Eden
和Survivor
中仍然存活的对象一次性复制到另外一块Survivor
空间上,然后直接清理调Eden
和已使用过的那块Survivor
空间。HotSpot虚拟机默认的Eden
和Survivor
空间的大小比例是8:1
,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%)。
在Java堆划分了不同的区域之后,垃圾收集器才能针对不同的区域安排与里面存储的对象死亡特征相匹配的垃圾收集算法,因而发展出了 标记-复制算法
、标记-清楚算法
、标记-整理算法
等针对性的垃圾收集算法。
3.2 标记-清除算法(Mark-Sweep)
标记清除算法
,最基础的垃圾回收算法,分为两个阶段:标记和清除。标记阶段标记出所有需要回收的对象,标记的过程就是对象是否属于垃圾的判定过程。清除阶段回收被标记的对象所占用的空间。
它的主要缺点有两个:
第一个是执行效率不稳定
,如果Java堆中包含大量对象,而且其中大部分是需要回收的,这是必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
第二个就是内存空间的碎片化
问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致当以后程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.3 标记-复制算法(copying)(新生代)
标记-复制算法
常被简称为复制算法
,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这块内存使用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次性清理掉。
如果内存中对数对象都是存活的,这种算法将会产生大量的内存间复制开销,但对于对数对象都是可回收的情况,算法复制的就是占少数存活的对象,而且每次都是针对每个半区进行回收,分配内存时,也就不用考虑空间碎片的复杂情况。这样实现简单,运行高效,不过其缺点也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费多。
现在的商用Java虚拟机大多都优先采用了这种收集算法回收新生代
。
标记-复制算法
在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以老年代一般不能直接选用这种算法。
3.4 标记-整理算法(Mark-Compact)(老年代)
针对老年代对象的存亡特征,提出了一种针对性的标记-整理算法
,其中标记的过程仍与标记-清除算法一样,但后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式。
是否移动回收后的存活对象是一项优缺点并存的风险决策:
如果移动存活对象,尤其是在老年代这种每次回收都要大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全称暂停用户应用程序才能进行。
但如果跟标记-清除算法那样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。
基于以上两点,是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。