首先在java运行过程中,其实程序员并不需要去显示的调用程序来释放对象的内存,而是由虚拟机来完成的,具体来看是在jvm中有个垃圾回收线程,这个线程是个守护线程,这个线程会在虚拟机空闲或者在当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
其实jvm的垃圾回收,在我们一开始创建对象的时候,GC就会去监控这个对象的地址,大小和使用情况,而jvm在进行回收的时候主要是采用了可达性分析算法,通过这种方式确定哪些对象是"可达
的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
其实垃圾回收的优点也很明显,首先就是不用像其他语言一样需要程序员手动的去释放内存,其次垃圾回收机制可以有效的防止内存泄露,并且可以做到对内存进行更有效的利用。
对于垃圾回收的时机,虽然程序员可以通过执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
我们刚才说到jvm会自动的对内存进行回收,因此它在回收的时候就需要知道 哪些对象是存活的,不需要被回收;而哪些对象又是垃圾对象,可以被回收,那么它具体是怎么判断的呢?
其实有两种判断方式
1 引用计数法:说白了也就是会为每一个对象创建一个引用计数,有对象引用时,计数器会加1,而引用释放时计数器会减1,而当计数器为0时也就表示对象可以被回收,它有一个缺点就是不能解决循环引用的问题
2 可达性分析算法:也就是会从GC root开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
Java 堆从 GC 的角度还可以细分为: 新生代和老年代。
而新生代 又可以分为 (Eden 区、 From Survivor 区和 To Survivor 区)
一般新生代和老年代的比例约为 1:2
同时 eden区:s1区:s2区约为 8:1:1
同时对于新生代来说 JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor区域是空闲着的。
整体的流程就是 :新建的对象会首先分配在Eden区中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor S0区,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor S1 区,就是通过这样不断地复制–>清空—>互换来实现年轻代的垃圾回收即minor gc
同时年轻代采用复制算法,也解决了内存碎片化的问题
我们接着再看老年代
老年代的对象特点 一般都是长生命周期的对象,也就是比较稳定,因此 MajorGC (常常称之为 FULL GC)不会频繁执行。
同时注意在进行 FULL GC前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法
找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
FULL GC 采用标记清除算法,同时会产生内存碎片
永久代 (或者 jdk1.8元空间)
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域, 它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久
代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
这个问题 其实想一下就能理解 分代主要还是 为了提高对象内存分配和垃圾回收的效率。
试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的 GC 效率。
而有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,
静态属性、类信息等存放在永久代(或者元空间)中,
新生代中的对象存活时间短,只需要在新生代区域中频繁进行 GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收。
而新生代为什么要分为eden区和survivor区 ?
针对这个问题,思路就是可以先描述下java堆内存的划分,以及minor gc,major gc,full gc 以及他们之间的转换过程。
首先我们知道java堆可以分为 老年代 + 新生代;同时 新生代 = Eden + S0 + S1
整体的过程就是:
那么Minor GC与Full GC分别在什么时候发生?
新生代内存不够用时候发生minor gc也叫YGC;JVM内存不够的时候发生FGC
可以分为 四种,即标记-清除算法;复制算法;标记整理算法; 分代收集算法;
我们首先看标记清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
适用场合:存活对象较多的情况、适用于年老代。
优点:比较简单,不需要对象的移动
缺点:会产生大量的内存碎片;效率也比较低
复制算法:
因为标记清除算法会产生内存碎片 因此在他的基础之上引入复制算法
说白了复制算法 就是 把内存空间划为两个相等的区域,每次只使用其中一个区域。
垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
标记整理算法
刚才说了复制算法 解决了内存碎片化的问题,但是它也有一个问题,就是内存的使用率并不高,需要频繁地进行来回复制,
因此又引入了标记整理算法,说白了 就是在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。
优点是:解决了内存碎片化的问题,相比于复制算法 内存使用率也提高了
缺点:仍需要进行局部对象移动,一定程度上降低了效率
分代收集算法:
分代收集法是目前大部分 JVM 所采用的方法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代,并根据各年代特点分别采用最适当的 GC 算法
老生代的特点是每次垃圾回收时只有少量对象需要被回收,
新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,
因此可以根据不同区域选择不同的算法
新生代与复制算法
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集,当然并不是按照1:1来划分新生代,一般是按照eden区,s1区,s2区 =8:1:1 来划分
老年代与标记整理算法
因为老年代对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标记—整理” 算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。因而采用 标记整理算法。