Java与C++之间有一堵由内存动态分配和垃圾收集技术所围城的 "高墙" ,墙外的人想进去,墙里面的人却想出来. ---<深入理解JVM>
在jvm运行时的五大内存区域, 其中 程序计数器,虚拟机栈,本地方法栈,随线程而生,随线程而灭. 栈中的 栈贞随着方法的调用和执行结束也有条不絮的入栈和出栈,而每一个方法所需要的栈贞的大小在编译期就已经确定,因此除堆以外的内存空间的分配和回收都具有确定性,因此我们在讨论JVM垃圾回收机制,主要就是说堆内存.
java的垃圾收集机制,主要分为两个过程 : 1.对象已死 2.内存回收
对象已死:
引用计数法 :
这是一种比较简单的方法来判定一个对象是否已经死亡的算法,通过一个给每一个对象进行引用计数,如果有一个引用指向这个对象, 引用数+1,减少一个引用指向这个对象时,引用数-1.当这个对象的引用数为0时,就认为该对象死亡. 这种方式看似很简单,但是又一个很严重的缺陷: 很难解决对象之间相互引用的问题, 维持一个引用计数器本身是一件容易且开销较小的事情,但是这项开销将在整个程序的生命周期持续发生.
因为这些缺陷,似乎从来没有任何一款JVM采用了引用计数器的方式来判定对象是否已死.
可达性分析算法:
可达性分析算法是通过在JVM中一系列被认为是 GCRoot 的对象开始进行搜索,在搜索的过程中会形成一条引用链,当搜索完成后,如果一个对象无法从 GCRoot 对象通过引用链到达该对象,就认为此对象是不可用的, 此方式很好的解决了对象之间的相互引用的问题.
因此,当前的JVM几乎都采用该算法来判断对象是否已经死亡.
在Java中,可作为 GCRoot 的对象主要有如下几种:
- 位于JVM运行时数据区域中的 虚拟机栈 中的对象.
- 位于JVM运行时数据区域中的 本地方法栈 中的对象.
- 方法区中常量引用的对象.
- 方法区中静态属性引用的对象.
生存还是死亡:
在JVM中,判定一个对象是否正式死亡,要经过两次标记,在通过可达性分析算法对不可用的对象进行第一次标记之后, 紧接着会执行第二次标记,在执行第二次标记之前,JVM会判定该对象是否需要执行finalize方法, 如果该对象没有覆写该方法,或者JVM已经执行过该对象的finalize方法,那么JVM就会认为该对象不需要执行finalize方法. 被判定为不需要执行finalize方法的对象会马上被第二次标记.
如果一个对象需要执行finalize方法,那么JVM会将该对象放置在一个F-Queue队列当中,并稍后由JVM创建一个低优先级的finalize线程去执行该对象的finalize方法, 在执行finalize方法时, 是该对象能够逃脱死亡的最后一次机会, 对象可以在finalize方法内将自身与任何一个引用链上的对象关联, 那么在第二次标记时,该对象将会被移除,从而实现了对象的成功自救, 但是在绝大多数情况下,并不推荐这么做去自救一个对象.
内存回收
标记-清除 算法:
就如同其名字一样,该过程分为两个部分,标记和清除. 但是这两个过程的效率并不高, 而且会产生一个问题就是 标记清除后, JVM中会存在大量不连续的碎片化的内存空间,这样造成的结果是 在给新对象分配内存空间时,效率较低, 如果要分配的空间较大而无法找到足够的连续内存来存放此对象,那么久不得不提前进行一次GC操作.
停止-复制 算法:
在该算法执行时,程序会停止运行,但是这是很短的时间. 该算法会把JVM中的内存分为一块较大的Eden区域和两块较小的Survivor区域,运行时只在Eden区域和一块Survivor区域进行内存分配,当要进行回收时,会先判定哪些对象是仍然存活的,然后会把这些对象复制到另外一块Survivor区域,然后清理掉之前使用的两块内存区域.
该方式的好处是,在每次清理后,堆中的内存较连续,在进行新对象的内存分配时,效率较高, 因此 现在的JVM几乎都采用这种方式来回收新生代.
标记-整理 算法:
该算法的过程仍然和 标记-清除 一样,只是在进行标记后, 不直接进行对象的清除, 而是把可用对象进行整理,将其向一端移动,然后直接清理掉端边界以外的内存. 该方式也可以让堆中的内存较为连续.
分代回收 算法:
当前商业虚拟机都采用该种方式来进行垃圾回收. 在该方法中会把JVM中的堆划分为 新生代 和老年代, 不同年龄代中的对象存活周期不同,因此对不同年龄代中的对象采用不同的方式进行回收, 如新生代中, 对象生命周期较短,每次GC时,只有少量的对象存活,就再用 停止复制算法,对于老年代,对象生命周期较长,每次回收是,只有少量对象需要回收,如果还采用停止复制算法,那么需要复制大量对象,效率较低,因此在老年代中应该采用标记清除或者标记整理算法来进行回收,从而获得更高的效率.
关于对象年龄代的分配:
- 新生成的对象会优先分配到新生代.(Eden) 当Eden区域没有足够的内存时,会触发一次 MinorGC.
- 大对象直接进入老年代,所谓大对象,就是需要大量连续内存空间的对象.大对象对于JVM来说是一件坏事,特别是短命的大对象,因此在编程时要尽量避免.
- 长期存活的对象将进入老年代.
MinorGC 和 FullGC(MajorGC)的区别:
- 新生代的GC叫做 MinorGC,新生代的对象生命周期较短,因此MinorGC触发频率比 FullGC 要高很多.
- FullGC 是指老年代的GC, 速度较慢,一般比 MinorGC 慢 10 倍以上.