一. 什么叫做垃圾回收
垃圾回收(Garbage Collection, GC)
简单来说,就是把不在使用的对象清除掉,释放内存。给其他新生儿腾地方。
Java会对内存进行自动分配与回收管理,使上层业务更加安全,方便地使用内存实现程序逻辑
在不同的JVM实现及不同的回收机制中,堆内存的划分方式是不一样的
二.静态/动态内存分配与回收
正所谓不了解内存的分配与回收不足以了解java堆内存的回收
2.1静态内存分配和回收
定义: 在程序开始运行时由编译器分配的内存
在被编译时就已经能够确定需要的空间,当程序被加载时系统把内存一次性分配给它,这些内存不会在程序执行时发生变化,直到程序执行结束时才回收内存.
-
包括原生数据类型及对象的引用
-
这些静态内存空间在栈上分配,方法运行结束,对应的栈帧撤销,内存空间被回收.
-
每个栈帧中的本地变量表都是在类被加载的时候就确定的,每一个栈帧中分配多少内存,基本上是在类结构确定时就已知了,因此这几块区域内存分配和回收都具备确定性,就不需要过多考虑回收问题了
2.2动态内存分配和回收
在程序执行时才知道要分配的存储空间大小,对象何时被回收也是不确定的,只有等到该对象不再使用才会被回收.
堆和方法区的内存回收具有不确定性,因此垃圾收集器在回收堆和方法区内存的时候花了一点心思.
三.java堆内存回收
3.1 如何判断对象要被回收呢?
GC是如何判断对象是否可以被回收的呢?
为了判断对象是否存活,JVM引入了GC Roots
如果一个对象与GC Roots之间没有直接或间接的引用关系,比如某个失去任何引用的对象,或者两个互相环岛状循环引用的对象等,判决这些对象“死缓”,是可以被回收的
再回收之前要做的一件事情就是,判断哪些是无效对象(一个对象不被任何对象或变量引用)
判断方式有以下2种方式:
-
引用计数法 (Reference Counting) 每个对象都有一个整型的计数器,当这个对象被一个变量或对象引用时,该计数器加一;当该引用失效时,计数器值减一.当计数器为0时,就认为该对象是无效对象.
-
可达性分析法 (Reachability Analysis) 所有和GC Roots直接或间接关联的对象都是有效对象,和GC Roots没有关联的对象就是无效对象.说白了点就是和GC Roots有关系,你就是有效的,没关系,那么对不起你就是个无效的,我就要收拾你
两者对比
引用计数法虽然简单,但存在无法解决对象之间相互循环引用的严重问题,且伴随加减法操作的性能影响.
因此,目前主流语言均使用可达性分析方法来判断对象是否有效.
此处应该做下GC Roots
的解释,上菜~
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈JNI(native方法)引用的对象
GC Roots并不包括堆中对象所引用的对象!这样就不会出现循环引用.
四.无效对象回收-过程
聊完了什么样的对象会被回收,该聊聊回收的过程是怎么样的了
刚刚说了2种判断方式,那么java采用的是可达性分析法,对于用可达性分析法筛选出来的无效对象,并不是立即清除的,而是给了他一次改过自新的机会
在JVM的垃圾回收器来看。堆区中的每个对象都肯能处于以下三个状态之一:
可触及状态:当一个对象被创建后,只要程序中还有引用变量引用该对象,那么它就始终处于可触及状态。
可复活状态:当程序不再有任何引用变量引用对象时,它就进入可复活状态。该状态的对象,垃圾回收器会准备释放它占用的内存,在释放前,会调用它的finalize()方法,这些finalize()方法有可能使对象重新转到可触及状态。
不可触及状态:当JVM执行完所有的可复活状态的finalize()方法后,假如这些方法都没有使对象转到可触及状态。那么该对象就进入不可触及状态。只有当对象处于不可触及状态时,垃圾回收器才会真正回收它占用的内存。
垃圾回收的时间
当一个对象处于可复活状态时,垃圾回收线程执行它的finalize()方法,任何使它转到不可触及状态,任何回收它占用的内存,这对于程序来说都是透明的。程序只能决定一个对象任何不再被任何引用变量引用,使得它成为可以被回收的垃圾。
五.垃圾回收算法
5.1 标记 - 清除
- 标记需要回收的内存
-
将上述标记的对象进行统一回收
image.png
劣势:
- 执行效率不稳定:
从上述也看出来,这种算法是分2步的,先标记后清除,假如对于有大量对象要回收时,该算法会随着对象的增加效率降低
2.内存碎片化:
从图片中可以看到,当对象被回收后,未使用的区域呈现无规则随机分布,那么伴随而来的就是内存碎片化。同时当有大对象要实例化时,由于内存碎片严重无法分配连续大内存空间,就会在一次导致垃圾回收。
5.2 标记 - 整理
由于标记 - 清除
方式会造成内存碎片,继而提出了另一种算法 -- 标记 - 整理
大概意思为:
1.标记需要回收的对象
- 将存活的对象向空间内存的一段移动【此时界限就很明确了,同时内存是连续的】
-
清理掉无效标记对象
image.png
劣势:
标记整理算法,在老年代区域中每次回收都有大量的对象存活,并且移动对象时需要更新所有对象的引用,所以会造成比较大的系统开销,而且对象移动操作必须全程暂停用户应用程序(Stop The Word)才能进行。
5.3 复制算法
为了能够并行地标记和整理将空间分为两块,每次只激活其中一块,垃圾回收时只需把存活的对象复制到另一块未激活空间上,将未激活空间标记为已激活,将已激活空间标记为未激活,然后清除原空间中的原对象
将内存分成大小相等两份,只将数据存储在其中一块上
1.当需要回收时,首先标记废弃数据
-
然后将有用数据复制到另一块内存
-
最后将第一块内存空间全部清除
image.png
劣势:
- 将内存分为2部分后,内存空间减少,空间利用率缩减
- 如果对象存活太多,复制过程中大量存活对象的复制会造成效率低下,开销大;相反,存活对象较少,不失为一种very good的算法
对于以上2点,其实该算法做了比较完善的优化的
优化
堆内存空间分为较大的Eden
和两块较小的Survivor
,每次只使用Eden和Survivor区的一块。这种情形下的“ Mark-Copy"减少了内存空间的浪费。“Mark-Copy”现作为主流的YGC算法进行新生代的垃圾回收。
在新生代中,由于大量对象都是"朝生夕死",也就是一次垃圾收集后只有少量对象存活
因此我们可以将内存划分成三块
Eden、Survior1、Survior2
- 内存大小:
Eden : Survior : Survior2 = 8:1:1
分配内存时,只使用Eden和一块Survior1.
- 当发现Eden+Survior1的内存即将满时,JVM会发起一次
Minor GC
,清除掉废弃的对象, - 并将所有存活下来的对象复制到另一块Survior2中.
- 接下来就使用Survior2+Eden进行内存分配
通过这种方式,只需要浪费10%的内存空间即可实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题.
一般情况下,经过15次分配后就进入了老年代区域
5.4 分代收集算法
根据对象存活周期的不同将Java堆划分为老年代和新生代,根据各个年代的特点使用最佳的收集算法.
- 老年代中对象存活率高,无额外空间对其分配担保,必须使用"标记-清除"或"标记-压缩"算法
- 新生代中存放"朝生夕死"的对象,用复制算法,只需要付出少量存活对象的复制成本,就可完成收集
网友评论