【架构师训练营 1 期】第九周作业
作业一:请简述 JVM 垃圾回收原理。
(通过课程学习理解 + 网上资料查阅与整理)
引申知识
Java 中的四种引用:强引用(平时用的最多)、软引用、弱引用、虚引用。
强引用
我们一般声明对象时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,就说明他不是垃圾,则不会被垃圾回收。(即只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时 JVM 也会直接抛出 OutOfMemoryError,不会去回收这些对象)
软引用
软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生 OutOfMemory 时,肯定是没有软引用存在的。
弱引用
弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。换句话说,弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get()方法,而且它的 get()方法仅仅是返回一个 null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
所以,这四种引用具有不同的垃圾回收时机,引用的强度从强、软、弱、虚依次递减,越往后的引用所引用的对象越容易被垃圾回收。
JVM 内存结构
由虚拟机栈、堆、本地方法栈、方法区、程序计数器组成
方法区:
方法区中主要存储了 Java 代码中的类加载的相关信息,比如类的名称、修饰符、构造器、静态方法、定义为 final 的常量、勒种的方法信息。这是一个面向全局线程共享的区域。
虚拟机栈:
这一区域主要存放了大量的线程相关信息,它是随着虚拟机线程的诞生而诞生的,所以它是独属于线程私有的一块区域,其中存放了 Java 八大基本类型的变量,还有一些局部变量,也就是方法内部的变量。
本地方法栈:
这里存放了 Java 代码中调用的本地方法的信息,主要用于 native 方法。
堆:
堆是 JVM 里很重要的一个区域,也是跟方法区一样属于全部线程都可以共享访问的一个区域,在堆中存储了大量的 Java 对象信息,但凡是 new 出来的对象都存放在这里,并且可以通过 jvm 提供的-Xmx 和-Xms 来调节堆的大小。其中-Xmx 是堆的最大值是多少,-Xms 是堆的最小内存,如果两者一致,则这个 Jvm 的堆大小就不能弹性伸缩(堆内部也划分除了多个区域,分别是新生代、老年代以及持久代。这其中新生代又分为 Eden 和 Survivor01、Survivor02 等)。
新生代中存储了所有新创建的对象,一个新对象创建 OK 首先是存放在 Eden 区域的,当 Eden 区域快要被填满时,就会自动触发 Jvm 的 GC 机制,GC 会回收一些空闲的对象,再将幸存下来的其他对象转移到 Survivor01 中去,同样,如果 01 也满了再次对 01 这一区域进行 GC 回收,将幸存者放入 Survivor02 区域中去。一旦在 Survivor02 中历经磨难多次存活超过 15 次,就会将其转移至老年代中去。至于持久代中,则主要存储一些常量池、方法区等数据。
相关参数如下:
垃圾识别机制
一、引用计数法
最容易想到的一种方法就是引用计数法。引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为 0 的时候,JVM 就认为对象不再被使用,是垃圾了。
引用计数器实现简单,效率高,但是不能解决循环引用的问题(A 对象引用 B 对象,B 对象又引用 A 对象,但是 A 和 B 对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了额外的开销,所以在 JDK1.1 之后,这个算法已经弃用。
二、可达性算法
一种听起来比较新颖的方法,也被称为根搜索算法。根搜索算法是通过一些“GCRoots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(ReferenceChain),当一个对象没有被 GCRoots 的引用链连接的时候,说明这个对象是垃圾,应该被回收。
垃圾回收算法
一、标记清除算法
标记清除算法是现代垃圾回收算法的思想基础。标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。标记阶段就是上面讲过的垃圾识别机制,会将有用的对象进行标记。因此,未被标记的对象就是未被引用的垃圾对象,然后,在清除阶段,清除所有未被标记的对象。
(缺点:标记清除算法使用后,内存中会出现很多的碎片。如果这样的碎片很多,则会导致内存的利用率急剧下降)
二、标记整理算法
标记整理算法类似与标记清除算法,这种算法可以解决碎片问题。原理在于,它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向内存的一端移动覆盖,然后直接清理掉边界以外的内存。
(缺点:虽然该方法可以完美的解决碎片问题,但是移动对象必然导致效率的降低,尤其是对象存活率较高的时候这个效率问题会更加明显)
三、复制算法
复制算法可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收。
(缺点:虽部分解决了效率问题,但是这种方法会浪费一半的内存空间)
四、分代回收算法
当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法,在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法。而老年代因为对象存活率高,没有额外空间对它进行分配担保,就使用“标记清理”或者“标记整理”算法来进行回收。
评论