自动内存管理
特指java堆、方法区两个区域的内存的分配与回收。
判断对象是否"存活"
引用计数算法
-
在对象中添加一个引用计数器,每当一个地方引用它时,计数器加一;当引用失效时,计数器减一;在任何时候,计数器为0的对象不可能再被使用。
-
主流的java虚拟机中没有使用引用计数算法来管理内存,因为有很多例外的情况需要考虑,比如对象之间循环引用问题
可达性分析算法
基本思路:通过一系列为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有引用链,或者用图论的话来说是从GC Roots 到这个对象不可达,则证明此对象时不可被使用的。
可作为GC Roots的对象(7种)
-
虚拟机栈(栈帧中的本地变量表)中引用的对象,比如线程被调用的方法堆栈中使用的参数、局部变量、临时变量等
-
本地方法栈中JNI(即通常所说的Native方法)引用的变量;
-
方法区中类静态属性引用的对象;
-
方法区中常量引用的对象,比如字符串常量池中的引用;
-
所有被同步锁(synchronized)持有的关键字;
-
java虚拟机内部的引用,如:
-
基本数据类型对应的Class对象;
-
一些常驻的异常对象,如NullPointException、OutOfMemoryError 等;
-
系统类加载器
-
-
反应java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
关于”引用“
-
JDK1.2之前,传统的引用定义:reference类型的数据中存放的是数值代表的是另一块内存的起始地址。
一个对象只有”被引用“或者”未被引用“两种状态 -
JDK1.2之后,对引用的概念进行了扩充:强引用、软引用、弱引用、虚引用,强度依次减弱
-
强引用(
StronglyReference
):最传统的引用定义,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象; -
软引用(
SoftReference
):描述一些还有用,但非必须的对象,被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列入回收范围之内进行二次回收,如果这次回收还是没有足够的内存,才会抛出内存溢出异常; -
弱引用(
WeakReference
):描述那些非必须的对象,但是比软引用强度更弱,被弱引用关联得对象,只能生存到下一次垃圾收集器发生为止。当垃圾收集器工作开始,无论当前内存是否足够,都会回收掉只被如弱引用关联得对象; -
虚引用(
PhantomReference
):也称为”幽灵引用“,”幻影引用“,最弱的一种引用。不管是否存在虚引用,完全不会对其生存时间造成影响,也无法通过虚引用取得一个对象实例,唯一目的是为了在这个对象被收集器回收时收到一个系统通知。
-
判断对象是否需要回收
在可达性分析中标记为不可达的对象不是非”死“不可的,真正回收对象至少需要经过两次标记:
-
如果对象在可达性分析中,没有与GC Roots相连,会进行第一次标记,随后进行一次筛选;
-
筛选的条件是此对象是否有必要执行
finalize()
方法,如果对象没有覆盖finalize()
方法,或者finalize()
方法已经被虚拟机调用过了,这两种情况都是”没有必要执行“; -
如果对象有必要执行
finalize()
方法,那么这个对象会被放置在一个F-Queue的队列中,稍后会由Finalizer线程(由虚拟机自动创建,低调度优先级)去执行它们的finalize()
方法,但并不保证一定等待它运行结束(这是为了避免陷入死循环,其他对象永久地等待,甚至回收子系统崩溃)。
-
-
finalize()
方法是对象避免回收的最后一次机会,稍后收集器会对F-Queue中地对象进行第二次小规模的标记。-
如果对象要在
finalize()
方法中成功逃脱,只要重新与引用链上的任何一个对象建立关联即可,那么在第二次标记时,会被移出”即将回收集合“; -
如果对象这时还没有逃脱,基本上就真的要被回收了。
-
注意:
finalize()
方法只会被系统调用一次;
一个对象的
finalize()
方法被执行,依然有可能存活。
finalize()
方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不建议使用
方法区的回收
-
方法区的回收主要涉及常量池的回收和类型的卸载;
-
方法区的回收成果比较低,存在未实现或未完全实现方法区类型卸载的收集器,如JDK11时期的ZGC收集器就不支持类型卸载;
对废弃常量的回收
与java堆中对象的回收很相似:
以常量池中字面量为例,如果当前系统中没有任何一个字符串对象引用常量池中的某个字符串,且虚拟机也没有在其他地方引用这个字面量,那么若此时发生垃圾回收,而且垃圾收集器判断确实有必要的话,这个字面将会被系统请离开出常量池。
常量池中的其他类(接口)、方法、字段的符号引用也类似。
对不再被使用的类型的卸载
判断一个类型不再被使用需要同时满足三个条件:
-
该类的所有实例都已经被回收,也就是在java堆中不存在该类及其子类的实例;
-
该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法; -
该类的类加载器已经被回收,这个条件除非是精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则很难达到。
注意:
java虚拟机允许对满足以上三个条件的无用类进行回收,这里仅仅是“被允许”,并不是说必然会被回收;
关于类型是否会被回收,HotSpot虚拟机提供了
-Xnoclassgc参数
进行控制,以及查看类加载和卸载信息的参数:
-verbose:class
、
-XX:+TraceClassLoading
、
-XX:+TraceClassUnLoading
垃圾收集算法
分代收集理论
分代收集名为理论,实际上是经验法则建立在两个分代假说之上:
弱分代假说绝大多数对象都是朝生夕灭的;
强分代假说:熬过越多次垃圾收集过程的对象越难以消亡。
分代收集的思想:
-
将java堆划分成不同的区域,依照将回收对象的年龄(也就是对象熬过垃圾收集过程的次数)分配到不同的区域中存储;
-
如果一个区域的大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中在一起,每次回收只关注少量存活的对象,就可以以较低的代价回收到大量的空间;
-
如果剩下的都是难以消亡的对象,那么把它们集中在一起,虚拟机可以以较低的频率回收这片区域;
-
可以同时兼顾时间开销和内存空间的利用。
回收类型:
-
Minor GC(新生代收集)
:指目标是只是新生代的垃圾收集; -
Major GC(老年代收集)
:指目标是只是老年代的收集。目前只有CMS收集器有单独收集老年代的行为; -
Mixed GC(混合收集)
:指目标是整个新生代和部分老年代的垃圾收集。目前只有G1收集器有这种行为; -
Full GC(整堆收集)
:收集整个java堆和方法区的垃圾收集。
垃圾收集算法:
-
标记-复制算法;
-
标记-清除算法;
-
标记-整理算法。
java堆一般划分成:
-
新生代;
-
老年代;
跨代引用问题
新生代的对象可能被老年代引用,不得不在固定的GC Roots之外额外遍历整个老年代,以确保可达性分析的正确,但这会增加性能负担,为解决这个问题,提出了第三条假说:
跨代引用假说:跨代引用相对于同代引用,仅占极少数。
利用跨代引用假说,不需要为了少量的跨代引用去扫描整个老年代,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集,Remembered Set”),这个结构把老年代或分为若干小块,标识出老年代那一块内存会存在跨代引用。当发生 Minor GC时只需要把包含引用的小块内存加入GC Roots进行扫描即可
标记-清除算法
过程:
-
“标记”过程:对象是否属于垃圾的过程;
-
“清除”过程:标记完成之后,统一清除
缺点:
-
执行效率不稳定,如果存在大量对象需要回收,那么效率会随着对象数量的增长而降低;
-
内存空间碎片化问题,清除之后会产生大量不连续的内存碎片,可能会导致后续需要为较大对象分配空间时无法找到足够的连续空间,而再一次触发垃圾收集。
需要停顿用户线程来标记、清除可回收对象。
标记-复制算法
可以解决*标记-清除算法面对大量可回收对象时执行效率低的问题。
思路:
-
将可用内存划分成两块,每次只使用其中的一块;
-
当这块的内存用完,就把还存活的对象复制到另一块上。
-
然后再把已使用过的内存一次清理掉。
优点:
-
每次都是针对整个分区进行清理,分配内存不必考虑内存碎片的复杂情况,只需要移动栈顶指针,按顺序分配即可。
缺点:
-
如果内存中大多数对象都是存活的,那么会产生大量的内存空间复制开销;
-
缩小了可用内存,空间浪费。
java虚拟机大多采用标记-复制算法回收新生代。
HotSpot虚拟机的Serial、ParNew等新生代收集器采用的Appel式回收:
可用空间:Eden(8)+Survivor(1) 复制空间:Survivor(1)
分配担保机制:当另一块的Survivor空间不足以存放上次新生代收集到的存活对象,这些对象会通过分配担保机制直接进入老年代,这对虚拟机来说是安全的。
标记-整理算法
针对老年代的垃圾收集算法,对象存活率较高时适用
思路:
-
标记过程与“标记-清除”相似,但后续的过程不是直接对可回收对象进行清除,而是让所有存活对象都向内存空间一端移动,然后直接清除掉边界以外的对象。
-
属于移动回收。
关于移动操作:
-
移动整理操作必须暂停用户线程才能进行。(最新的ZGC 和Shenandoah收集器使用读屏障(Read Barrier)技术实现了整理过程和用户线程的并发执行)
计算机硬盘存储大文件不需要物理连续的磁盘空间,而是通过磁盘分区表实现在碎片化的硬盘上存储和访问的
-
移动则内存回收时更复杂,不移动则内存分配时更复杂。从停顿时间来看,不移动停顿时间更短甚至可用不需要停顿,但从整个程序的吞吐量看,移动会更划算。(在这里,吞吐量的实质是赋值器(Mutator,可以理解为使用垃圾收集的用户线程))和收集器的效率总和。
-
HotSpot虚拟机中关注吞吐量的
Parallel Scavenge
收集器是基于标记-整理算法的。 -
而关注延迟的
CMS
收集器是基于标记-清除算法的。但是在空间碎片化程度影响到对象分配时,再采用标记-整理算法