什么是垃圾?:Java内存中已经不再被使用的内存空间
怎么判断对象是否存活?
-
引用计数法。在对象中添加一个引用计数器,有一个地方引用它时,计数器值就加一; 当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。但是无法解决**相互循环引用**的问题。
-
可达性分析。从GCRoot出发,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain), 如果某个对象到GC Roots间没有任何引用链相连,即GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
哪些对象可以作为GCRoot?
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象,譬如字符串常量池里的引用。
- 虚拟机栈中引用的对象,如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
- native方法引用的对象
- 类加载器、基本数据类型对应的Class对象、常驻的异常对象等虚拟机内部的引用
- 所有被同步锁(synchronized关键字)持有的对象。
- 根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。
(补充)HotSpot使用了oopMap的数据结构来达到准确式GC的目的。在类加载完成后。虚拟机会记录在哪个偏移位置上有哪些引用,扫描时直接从oopMap扫描,不用全部从根节点去扫描。
强引用、软引用、弱引用、虚引用
强引用:类似于必不可少的⽣活⽤品,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。
软引用:类似于可有可⽆的⽣活⽤品,如果内存空间充足则不会回收,内存空间不足了就对软引用进行回收
弱引用:类似于可有可⽆的⽣活⽤品,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象。
虚引用:为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
对象被判定不可达后是否一定回收?
要真正宣告一个对象死亡,至少要经历两次标记过程:
1.第一次标记为对象不可达。
2. 进行1次筛选,假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用 过,则视为“没有必要执行”
如果对象没有执行finalize()方法,那么如果在执行方法过程中对象重新与GCRoot建立连接,就不会被回收。
每个对象的finalize()方法只会被调用一次!!
方法区的回收
方法区的垃圾收集主要**回收两部分内容**:废弃的常量和不再使用的类型。
怎么判断一个类是否废弃?:
- 类的所有实例都被回收。
- 加载该类的类加载器已经被回收,这个条件很难达成
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方 法。
垃圾收集算法
1.标记-清除算法:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。(CMS收集器)
缺点:1. 执行效率不稳定,如果Java堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作
,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
2.标记清除后会产生大量的空间碎片。导致当后续分配较大内存时,可能无法找到足够的连续内存来分配,导致提前出发另一次垃圾回收。
2.复制算法:将可用内存划分成两个大小相等的两块,每次只用一块,当这一块内存用完就将还存活的对象复制到另一块,再进行内存清理。(主要用在新生代的垃圾回收)
缺点:1.每次只使用一半的内存,造成部分内存浪费。
2.如果大量对象存活,就会产生大量的内存间复制开销
优点:1.效率高,分配内存不需要考虑空间碎片化
3.标记-整理算法:首先标记出需要回收的对象,标记完成后,让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。(主要用在老年代的垃圾回收)
缺点:1.在移动对象后,需要更新引用这些对象的指针指向的地址。
4.分代收集:将Java堆划分为新生代和老年代,这样可以*提高GC效率*,新生代每次Minor GC会有大量对象死去,可以选择复制算法。
老年代对象存活时间比较长,且没有额外空间进行分配担保,选择“标记清除”或者“标记整理”算法。
Minor GC:指对新生代的垃圾收集。收集频率高,速度快。
Major GC:指对老年代的垃圾收集。触发频率比较低,目前只有CMS收集器会有单 独收集老年代的行为。
Mixed GC:目前只有CMS收集器会有单 独收集老年代的行为。目前只有G1收 集器会有这种行为。
Full GC::收集整个Java堆和方法区的垃圾收集。可能还会对方法区顺便收集。
HotSpot算法实现细节
-
根节点枚举:所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的(STW)。但是在Java虚拟机中并不需要一个不漏地检查完所有 执行上下文和全局的引用位置。HotSpot使用了oopMap的数据结构来记录哪些地方存放对象引用。在类加载完成后,虚拟机会记录在哪个偏移位置上有哪些引用,扫描时直接从oopMap扫描,不用全部从根节点去扫描。
-
安全点:JVM不能为每条指令生成oopMap,所以设置了特定的位置,强制要求必须执行到达安全点后才能够暂停。安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集 过程的安全点。
如何在垃圾收集发生时让所有线程都跑到最近的安全点?: 1.抢先式中断。系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行 让它一会再重新中断,直到跑到安全点上。 2.主动式中断。是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅设置一 个标志位,各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最 近的安全点上主动中断挂起。 HotSpot使用**内存保护陷阱**的方式, 把轮询操作精简至只有一条汇编指令的程度。
-
安全区域:程序不执行时,即没有分配处理器时间,处于Sleep状态或者Blocked状态,线程无法响应虚拟机中断请求。安全区域指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任 意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
-
记忆集:记忆集用来解决跨代引用问题,避免把整个老年代加进GC Roots扫描范围。记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
(补充)跨代引用:新生代对象引用了老年代的对象,由于老年代的对象难以消亡,导致新生代的对象在收集时仍然存活。
-
卡表:卡表是记忆集的一种实现方式,卡表最简单的形式可以只是一个字节数组。卡表的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作“卡页”。
卡表变脏:一个卡页的内存中通常包含不止一个对象,只要有一个或多个对象的字段存在跨代引用,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(卡表变脏)。
筛选时筛选出卡表变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。 -
写屏障:用来维护卡表状态。在引用对象赋值时会产生一个环形通知,在赋值前的部分的写屏障叫作写前屏障,在赋值 后的则叫作写后屏障。
卡表在高并发下的伪共享问题:当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影 响(写回、无效化或者同步)而导致性能降低。 如何解决?:是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏
-
并发的可达性分析:如果用户线程和收集器是并发工作,并发时出现的对象消失问题。
黑色对象:对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。 白色对象:对象尚未被垃圾收集器访问过。 灰色对象:对象已经被垃圾收集器访问过,但引用还没有被全部扫描过。
对于第一种情况:黑色对象插入了到白色对象的引用,采用增量更新的方法:
当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束后,再将这些记录过的引用关系中的黑色对象为根,
重新扫描一次。即黑色对象变为灰色对象。CMS是基于增量更新 来做并发标记的。
对于第二种情况:删除了全部从灰色对象到该白色对象的直接或间接引用,采用原始快照:
无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索,在并发结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。G1、Shenandoah用原始快照来实现。
垃圾收集器
-
Serial收集器。适合在Client模式下运行
单线程工作的收集器,“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时, 必须暂停其他所有工作线程,直到它收集结束。 新生代采用-复制算法 ,老年代采用-标记整理算法
-
Serial Old收集器
Serial Old是Serial收集器的老年代版本,1.作为CMS 收集器发生失败时的后备预案。 2.在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用
-
ParNew收集器–合并入CMS收集器,CMS的新生代收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为与Serial收集器一样。
-
Parallel Scavenge收集器–关注吞吐量
Parallel Scavenge收集器关注的是吞吐量,就是CPU中⽤于运⾏⽤户代码的时间与CPU总消耗时间的⽐值。 -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间 -XX:GCTimeRatio:直接设置吞吐量大小 新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
-
Parallel Old收集器
-
CMS收集器–以获取最短回收停顿时间为目标
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。收集过程包括以下四个步骤: 1. 初始标记(STW)。标记GC Roots能**直接**关联到的对象。 2. 并发标记。从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程中用户线程也能同时运行,会导致引用关系发生变化。 3. 重新标记(STW)。修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 4. 并发清除。清理删除掉标记阶段判断的已经死亡的 对象,不需要移动存活对象。
CMS的优缺点:
优点:并发收集、低停顿。 缺点:1.对CPU资源敏感。在并发阶段,虽然不会导致用户线程停顿,但会占用了一部分线程而导致应用程序变慢,降低总吞吐量。 CMS默认启动的回收线程数是(处理器核心数量 +3)/4,如果CPU数<4,会导致占用处理器资源过多。 2.无法处理“浮动垃圾”。在并发标记和并发清理阶段,用户线程还在继续运行,伴随有新的垃圾对象不断产生,这一部分垃圾 对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。 3.基于“标记-清除”算法实现,收集结束时会有大量空间碎片产生。
-
G1收集器
G1 (Garbage-First)是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征.几个重要特征: 1.虽然遵循分代收集理论,但基于Region的堆内存布局,把连续的Java堆划分为多个大小相等的独立区域(Region), 每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。 Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。 2.空间整合。局部使用 复制算法 ,整体使用 标记-整理算法。 3.能建立可预测的停顿时间模型。能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标 几个存在的问题: 1. Region里面存在的跨Region引用对象如何解决? 使用记忆集避免全堆作为GC Roots扫描,每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region 指向自己的指针, 并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一 种哈希表,Key是别的Region的起始地址, Value是一个集合,里面存储的元素是卡表的索引号。 2. 怎么保证收集线程和用户互不干扰? 用户线程改变对象引用关系时,而G1 收集器则是通过原始快照(SATB)算法来保障的。 垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上。,G1为每一个Region设 计了两个名为TAMS(Top at Mark Start)的指针, 把Region中的一部分空间划分出来用于并发回收过 程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。 G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。 运行步骤: 1.初始标记。标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。 2.并发标记。从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。 当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象 3.最终标记。处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。 4.筛选回收。根据用户所期望的停顿时间来制定回收计划,选择回收回收价值最大的Region。 G1和CMS比较: 1. 内存占用。G1和CMS都使用卡表来处理跨代指针 ,G1的卡表实现更为复杂,而且堆中每个Region都必须有一份卡表,这导致G1的记忆集 (和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间。 CMS的卡表就相当简单, 只有唯一一份,而且只需要处理老年代到新生代的引用。因为新生代的对象具有朝生夕灭的不稳定性,引用变化频繁。 2. 执行负载。都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障,还需要使用写前屏障来跟踪并发时的指针变化情况。 原始快照和增量更新的区别? 原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点, 但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。
内存分配和回收
-
什么对象直接进入老年代?
1.大对象直接进入老年代。长字符串、数量庞大的数组。 -XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配。 目的:避免在Eden区及两个Survivor区 之间来回复制,产生大量的内存复制操作。 2.存活时间长的对象直接进入老年代。当对象年龄达到当-XX:MaxTenuringThreshold设定的值后就进入老年代 3.如果survivor区中低于或等于某个年龄的对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
-
分配担保机制
1.发生Minor GC之前,虚拟机必须先检查老年代最大可用的**连续空间**是否大于新生代**所有对象总空间**,如果大于就执行Minor GC 2.如果不成立,先检查是否允许担保失败。如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。- XX:HandlePromotionFailure 3.如果大于,将尝试进行一次Minor GC,(赌概率的方法),如果Minor GC失败则Full GC 4.如果小于,或者设置不允许冒险,那这时就要改为进行一次Full GC。