JVM-java垃圾回收
垃圾回收机制特点:
1.垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源。
2.程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。对象永久性地失去引用后,系统将会在合适的时候回收它的内存。
3.在垃圾回收机制回收任何对象之前,总会先调用它的finallize()方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。
1.垃圾回收主要关注 Java 堆
Java 内存运行时区域中的程序计数器、虚拟机栈、本地方法栈随线程而生灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由 JIT 编译器进行一些优化),因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而 Java 堆不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
2.判断哪些对象需要被回收
有以下两种方法:
-
引用计数法
给对象添加一引用计数器,被引用一次计数器值就加 1;当引用失效时,计数器值就减 1;计数器为 0 时,对象就是不可能再被使用的,简单高效,缺点是无法解决对象之间相互循环引用的问题。 -
可达性分析算法
通过一系列的称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。此算法解决了上述循环引用的问题。
在Java语言中,可作为 GC Roots 的对象包括下面几种:
a. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
b. 方法区中类静态属性引用的对象。
c. 方法区中常量引用的对象。
d. 本地方法栈中 JNI(Native方法)引用的对象
作为 GC Roots 的节点主要在全局性的引用与执行上下文中。要明确的是,tracing gc必须以当前存活的对象集为 Roots,因此必须选取确定存活的引用类型对象。
GC 管理的区域是 Java 堆,虚拟机栈、方法区和本地方法栈不被 GC 所管理,因此选用这些区域内引用的对象作为 GC Roots,是不会被 GC 所回收的。
其中虚拟机栈和本地方法栈都是线程私有的内存区域,只要线程没有终止,就能确保它们中引用的对象的存活。而方法区中类静态属性引用的对象是显然存活的。常量引用的对象在当前可能存活,因此,也可能是 GC roots 的一部分。
3.强引用、软引用、弱引用、虚引用
软引用需要通过SoftReference类来实现。当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空 间不足时,系统可能会回收它。
弱引用通过WeakReference类实现。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。
强引用就是指在程序代码之中普遍存在的,类似"Object obj=new Object()"这类的引用,垃圾收集器永远不会回收存活的强引用对象。
- 软引用:还有用但并非必需的对象。在系统 将要发生内存溢出异常之前 ,将会把这些对象列进回收范围之中进行第二次回收。
软引用关联的对象不会被
GC立即
回收。JVM
在分配空间时,若果Heap
空间不足,就会进行相应的GC
,但是这次GC
并不会收集软引用关联的对象,但是在JVM发现就算进行了一次回收后还是不足(Allocation Failure
),JVM
会尝试第二次GC
,回收软引用关联的对象。 - 弱引用也是用来描述非必需对象的,被弱引用关联的对象 只能生存到下一次垃圾收集发生之前 。当垃圾收集器工作时,无论内存是否足够,都会回收掉只被弱引用关联的对象。
- 虚引用是最弱的一种引用关系。 无法通过虚引用来取得一个对象实例 。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
4.垃圾收集算法
1.标记-清除算法
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分为“标记”和“清除”两个阶段:标记:遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
更正:标记整理算法和标记清除算法 标记阶段 标记的是可回收对象,清除阶段是清除带标记的对象。
它的主要不足有两个:
- 效率问题,标记和清除两个过程的效率都不高;
空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
现在的商业虚拟机都采用这种算法来回收新生代,IBM 研究指出新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。
当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden:Survivor = 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(其中一块Survivor不可用),只有 10% 的内存会被“浪费”。
当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
3.标记-整理算法
复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块并采用不用的垃圾收集算法。
一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
5.Minor GC 和 Full GC 有什么不一样吗?
-
新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
-
老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。Major GC 的速度一般会比 Minor GC 慢 10 倍以上。
6.大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组( byte[] 数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(特别是短命大对象,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
7.长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1 。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就将会被晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold
设置。
8.新生代为什么需要两个survivor?
设置两个Survivor区最大的好处就是解决了内存碎片化,详细见下文。
参考:https://blog.csdn.net/antony9118/article/details/51425581
zjp总结:
1..为什么需要survivor?
答,因为新生代98%的对象都是朝生夕死,所以选用复制算法进行垃圾回收(把存活的对象复制到另一端,存活对象少,效率高),复制算法就要求对新生代进行分区。
2.为什么一个survivor不行?
答:当只有一个survivor时,第一次进行垃圾回收,存活的对象被复制在survivor中,当新生代满了,咱进行第二次垃圾回收时 也会回收survivor中的对象,造成survivor中出现内存碎片。如图
3.为什么两个survivor就可以?
答:如果新生代中有两个survivor,当进行第二次垃圾回收时,会把eden区和其中一块存在存活对象的survivor中的存活对象都复制在第二个survivor中,然后对eden和另外一块survivor进行清空。这样不管进行多少次GC,都会保证有一块survivor为空,这样放进对象不会出现内存碎片的问题。如图: