前言:java和C++之间有着内存自动管理和垃圾回收技术的高墙,墙里面的人想出来,墙外面的想进去。(引用自周大师)
上一篇主要介绍了jvm的内存分配,没看过的同学可以移步上篇博客jvm内存分配。程序猿比较关注的内存的堆栈应用,这篇主要讲jvm中内存最大的一块区域,堆内存如何实现自动管理和垃圾回收。
如何判断对象已死?
平时在开发时,我们只需new对象就可以自动分配内存空间,而不用去管理内存的销毁回收,都是jvm帮我们做了这些工作。那么,有个问题:在C程序中,都是程序猿去手动销毁不使用的对象,释放内存,在java中,jvm如何知道哪些是不需要使用的可以回收的对象呢?也就是怎么判断内存中对象已死?
1、引用计数
jvm会给内存中的对象增加一个计数器,每当对象增加一个引用,计数器就加一,当引用失效计数器就减一,计数器为零就代表这个对象是可以回收的。
缺点:无法解决循环引用的问题。如下图中,ObjectA和ObjectB相互引用,ObjectA和ObjectB都无法回收。
2、可达性分析算法
从gc root对象开始,寻找引用对象,沿引用链,可以达到的对象都是存活的;无法达到的对象都是死亡的,可以回收的。
上图中,虽然对象D、E、F之间有引用,但和GC Root的引用链没有交集,所以D、E、F对象是可以回收的对象,而对象A、B、C是活动的对象。
目前java中可作为GC Root的对象有:
1. 虚拟机栈中引用的对象(本地变量表)
2. 方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈中引用的对象(Native对象)
3种垃圾回收算法
1、标记清除(Markup And Sweep)
分为两个阶段,第一阶段标记阶段,使用引用计数或者可达性分析标记对象是否可以回收;第二阶段清除阶段,将可以回收对象的内存回收。
缺点:会出现内存碎片,当创建大对象而找不到足够大的连续的内存空间时会触发Full GC,会影响整个应用程序性能。
2、复制算法(Copying)
将堆内存分为大小相等的两个部分,比如A、B,当A内存不足时,将存活的对象拷贝到B内存区域,然后回收整个A内存区域。
缺点:内存使用率只有一半,十分浪费内存;
3、标记整理
这个是针对复制算法做的改进,解决内存利用率低的问题,也解决存活对象较多时,复制算法需要大量复制对象导致的效率问题。将内存中存活的对象移动到内存区域的一端,全部移动结束后,回收分界线另一端的整个内存区域。
分代回收
问1:jvm何时触发gc?
答:
Minor Gc(新生代回收)的条件比较简单,只要Eden内存不足就会触发。
Full Gc(老年代回收)有几种触发条件:
- 老年代空间不足。
- PermSpace空间不足。
- 统计得到Minor Gc后晋升到老年代的平均空间大于老年代的剩余空间。
问2:jvm如何分代?
答:
查看上图,jvm分代主要有年轻代(young),老年代(Tenured),和非堆内存(永久代)。
年轻代:年轻代细分为Eden、Survivor1、Survivor2,这么细分的作用后面讲,一般新建立的对象进入年轻代,年轻代对象的特点是朝生夕死,年轻代中的对象有年龄的概念,年轻代中的gc称为Minor Gc,每经过一次Minor Gc,对象年龄就会更加一岁,默认年龄达到15岁,对象就会进入老年代中,还有其他情况对象年龄没有达到15岁也会进入老年代,这里不细说了。
老年代:老年代对象的大部分是存活时间很长。
非堆内存(永久代):又称方法区,主要存放类信息、静态变量、常量信息。
问3:为什么要分代回收?
答:分代回收当然是为了提升jvm回收的效率,减少因为gc导致的应用程序停顿。年轻代的对象朝生夕死,老年代的对象存活时间很长,针对不同的特点应用不同的回收算法,年轻代应用复制算法,因为存活对象较少,老年代因为对象存活时间较长,只能使用标记清扫或者标记整理算法,具体要看虚拟机产商,不同的虚拟机产商有不同的实现。
1、年轻代
年轻代细分为Eden、Survivor1、Survivor2,采用复制算法,因为对象朝生夕死,没有必须按照复制算法的一半内存划分,默认的划分比例Eden:Survivor1:Survivor2=8:1:1,为什么分为两个Survivor?当Eden内存不够时,触发Minor Gc,将存活的对象复制到Survivor2,然后清空Eden和Survivor1的空间,下次Minor Gc时会将存活对象复制到Survivor1,然后清空Eden和Survivor2的空间,设置两个Survivor是为了来回腾挪存活对象使用的。
2、老年代
老年代的gc称为Major Gc或者Full Gc,Full Gc非常消耗性能,影响应用程序,可能导致应用程序长时间停顿,jvm应该尽量避免Full Gc。避免Full Gc这涉及jvm的性能优化,后续有时间再整理分享。