面试官:JVM 中的堆、堆中的内存分配以及堆中的垃圾回收了解吗?我:要不我直接回去等通知?
堆
我们从前面的文章中知道了,我们创建的对象实例会存放在堆中,也就是对象实例会在堆里分配内存,所以堆也是垃圾收集的主要区域。但仅仅知道这些是不够的,今天我们就来具体看一看堆以及堆中的垃圾回收。
这里在网上找到了一个比较好的堆相关内容的图,我们就根据这个图来一步步了解堆:
我们可以看到,堆主要分成了两部分:
- 新生代(Young Generation)
- 老年代(Old Generation)
而 新生代(Young Generation)又可以分成
- Eden 区
- Survivor 0 区
- Survivor 1 区
其中,Eden 区、Survivor 0 区 也被称为 From 区和 To 区。(至于为什么要这样分?不要急,后面会说,这里先有个分区概念就行。)
我们来先看整体再看细节。
整体上看新生代与老年代
整体上可以看到,堆主要分成了两部分:
- 新生代(Young Generation)
- 老年代(Old Generation)
对象实例会在新生代里诞生,当新生代区域内存不够时,会进行 垃圾回收 ,被称为 Minor GC,会将新生代区域里一些已经不再使用的对象实例给回收掉,释放出内存。因为新生代的对象实例大部分存活时间都很短,因此 Minor GC 会频繁执行,且执行的速度一般也会比较快;而那些还需要继续使用的对象实例,也就是经过 Minor GC 之后还在的对象实例,年龄会加 1 。
而当新生代区域里的某个对象实例的年龄到达一定程度时,比如 15(默认是 15 ),这个对象实例会被移动到老年代区域。
或者可以形象的理解为,一个新兵在战场(新生代)上,扛过了 15 次 战争(Minor GC)之后还存活着,那么就可以功成名就的去后方指挥部(老年代)当将军了。
不过,老年代区域的内存也是有限的,当老年代区域的内存不够时,也会进行 垃圾回收,被称为 Major GC 或者 Full GC;并且,进行 Full GC 时,经常会伴随着至少一次的 Minor GC(但并非绝对会出现)。因为老年代对象其存活时间长,因此 Full GC 很少执行,且执行速度会比 Minor GC 慢很多。
你可能会有疑问,当老年代发生 Full GC 之后,老年代区域的内存依旧不够,会怎么样?会报 java.lang.OutOfMemoryError: Java heap space 异常,简称 OOM ,也就是内存溢出。至于解决的办法,后面的内容里会说,这里先知道一下就行。
做个堆整体上内容的小总结: 堆整体上分成了新生代和老年代, 对象实例在新生代里诞生;新生代内存不够时,会进行垃圾回收;当一个对象实例经过一定次数的垃圾回收之后还没有被回收,就会进入老年代;当老年代内存不够时,也会进行垃圾回收,而如果老年进行垃圾回收之后,内存依旧不够,就会出现 OOM 异常。
- 整体上我们已经了解了,但细节上还有一些问题:
- 为什么新生代要划分成 Eden 区、From 区、To 区?
- 垃圾回收是怎么进行的?
- 怎么知道一个对象实例是否该回收?
- … …
带着这些疑问,我们再从细节上看一下堆的新生代
细节上看新生代
为什么新生代要划分成 Eden 区、From 区、To 区?垃圾回收的过程?
新生代为什么这样划分,其实我们来了解一下新生代 Minor GC 的过程就知道了(其实也是顺便了解一下复制算法)。
Minor GC 过程:复制→清空→互换
复制
首先,当 Eden 区内存满的时候会触发第一次 Minor GC,然后把 GC 之后还活着的对象实例拷贝到 From 区;
当 Eden 区再次触发 GC 的时候,会扫描 Eden 区和 From 区,对这两个区域进行 GC;经过这次 GC 回收后还存活的对象,则会直接复制到To区域,并且把这些对象的年龄 +1(如果有对象的年龄已经达到了老年的标准,则会复制到老年代区)。清空
第二次及之后的 Minor GC 是把 GC 之后 Eden 区和 From 区还存活的对象复制到 To 区;复制之后,会把 Eden 区和 From 区都清空掉。互换
然后会将此时的清空后的 From 区会与 To 区交换位置,也就是 From 区变成 To 区,To 变成 From 区;这样做是为了保证每一次 To 区都是空的,当下一次 GC 时,就又可以把 From 区的对象实例复制到 To 区了。
所以,你应该为什么要将新生代划分成 Eden 区、From 区、To 区了吧?
Eden 区里是为新产生的对象实例准备的,而 From 区、To 区是为了每次的复制与交换准备的。
(这里提一句,每次 GC 之后会 From 区与 To 区都会交换,那么这两个区的内存大小应该满足什么样的关系?聪明的你应该想到了,为了满足每次的交换动作, From 区与 To 区的内存应该是要一样大的! )
怎么知道一个对象实例是否可以回收 / 怎么判断对象实例是否死亡?
1. 引用计数算法
我们可以给对象实列添加一个引用计数器,每当有一个地方引用这个对象实列时,计数器加 1 ,当这个地方不再引用它时,也就是引用时效时,计数器减 1 ;引用计数器为 0 的对象实列就是可以被回收的对象,也就是死亡的对象。
但引用计数器算法可能会出现一个问题:循环引用的情况下,使用引用计数器算法进行垃圾回收会出问题。
循环引用指的是 A 对象引用了 B 对象, B 对象引用了 A 对象;如此一来,引用计数器永远都不会为 0 ,就会导致无法对它们进行回收。
也正因为循环引用导致的这个问题,Java 虚拟机没有使用引用计数器算法来进行垃圾回收。
2. 可达性分析算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
在 Java 技术体系里面,固定可以作为 GC Roots 的的对象包含如下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用调用的方法堆栈中使用的参数、局部变量、临时变量等。
- 在方法区中类静态变量属性引用的对象,比如 Java 类的引用类型静态变量。
- 在方法区中常量引用的对象,比如字符串常量池里的引用。
- 在本地方法栈中JNI(也就是 Native 方法)引用的对象。
- Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如空指针异常,OOM)等,还有系统加载器。
- 所有被同步锁(synchronized 关键字)持有的对象。
- 反映 Java 虚拟机内部清空的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
除了上面这些固定的 GC Roots ,还可以根据用户所选用的垃圾收集器以及当前回收的内存区域不同,加入其他“临时性”的对象。
垃圾怎么回收的?(垃圾回收算法有哪些?)
复制算法
这个算法,我们在讲新生代的垃圾回收时,说的就是这个算法。
简单来说就是:将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,将剩下还存活的对象复制到另一块去,然后再把使用的空间进行一次清理。
好处是:效率高,不会产生大量内存碎片(内存碎片:对象在内存中不连续,这一个那一个,是内存碎片化了)。
坏处是:只使用了一半的内存,浪费了内存。标记-清除算法
这个算法其实就和它的名字一样 标记-清除:首先将需要回收的对象标记一下,当内存不够触发垃圾回收时,就会将所有标记了的对象清除掉。
这个算法很简单,但是问题也很明显,因为标记对象的位置不确定,所以会产生大量内存碎片,并且标记清除过程的效率不高。标记-整理算法
标记-整理算法,也同样会对需要回收的对象进行标记,但后续不是直接清除标记过的对象,而是让所有存活对象(也就是未标记对象)全部向内存的一端移动,然后再清理掉端边界以外的内存。
相比与标记-清除算法,标记-整理算法不会产生内存碎片;但是同样,效率不高,因为需要移动大量对象,所以处理效率自然不高。分代收集算法
分代收集算法其实不是什么新算法,而是将上面说的三种扬长避短,根据不同的情况使用不同的算法。
分代收集算法会根据对象存活周期将内存划分为不同的几个部分,一般就是我们前面说的分成 新生代和老年代。
- 新生代:复制算法
- 老年代:标记-清除算法 或者 标记-整理算法
关于 JVM 堆相关的内容就写到这里了,想了解更深更细致的内容,推荐大家可以去看看 周志明大神写的 《深入理解Java虚拟机》。
注:如果猿兄这篇博客有任何错误和建议,欢迎大家留言,不胜感激!
JVM 系列文章相关推荐:
持续更新,点个关注,不再迷路
这里是 猿兄,为你分享程序员的世界。
非常感谢各位优秀的程序员们能看到这里,如果觉得文章还不错的话,求点赞???? 求关注???? 求分享????,对我来说真的 非常有用!!!