哪些内存
关于内存什么时机分配
PC什么时候被分配:一个线程被创建的时候 那么对应着当线程被终止的时候,就会被回收
栈中的内存什么时候被分配:一个方法被调用的时候,这个线程的方法产生一个栈帧,那么在调用结束的时候让这个栈帧出栈
堆中的内存:一个对象被实例化的时候,那么在对象不再使用的时候,就会回收
方法区和运行时常量池:一个类加载的时候分配内存,那么对应在一个类被卸载的时候回收内存(满足下面的条件只是允许被回收,不是一定要被回收)
这些分配和回收中,其实只有对象不被使用这个点是比较难明确的

所以我们的 GC主要探讨的就是堆内存回收的问题。PC和栈,方法区的回收是比较确定,所以就将GC的问题聚焦在了堆的内存回收和方法区(方法区一般比较少,因为太过苛刻)的回收

如何判断垃圾对象
回收的对象一定是垃圾对象,但是不保证所有的垃圾对象回收——可以接受

我们假定内存回收是整体的回收,GC期间,整个时间是静止的(也就是这几张图的结构是不会变化的,应用程序也不再进行执行任何指令)
[Java的引用]: Java的五大引用 - 刘颂成 - 博客园 (cnblogs.com)(https://www.cnblogs.com/lscCurry/p/16887018.html)
标记:引用根节点开始遍历,标记所有被引用的对象。一般是在对象的 Header 中记录为可达对象。标记的是引用的对象,不是垃圾!!
清除:堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header中 没有标记为可达对象,则将其回收
这里所谓的清除并不是真的置空,而是把需要清除的对象地址(起始地址和结尾地址)保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的位置。
如果内存规整
如果内存不规整
虚拟机需要维护一个列表
空闲列表分配
存在的问题

标记复制算法在对象存活率比较高的时候就要进行较多的复制操作,效率也会降低,更关键,如果不想浪费那一半的空间,还需要额外的空间进行分配担保,我们的老年代存活几率比较大,所以一般不采用这种标记复制算法


因为对这种内存碎片我们没有很好的解决方法,所以对内存进行进一步划分,对不同区域有不同的处理这种思想来缓解

相关的JVM参数
| 堆初始大小 | -Xms |
|---|---|
| 堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
| 新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
| 幸存区比例 | -XX:SurvivorRatio=ratio |
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
| 晋升详情 | -XX:+PrintTenuringDistribution |
| GC详情 | -XX:+PrintGCDetails -verbose:gc |
| FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
演示的代码
package cn.itcast.jvm.t2;
import java.util.ArrayList;
/**
* 演示内存的分配策略
*/
public class Demo2_1 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
public static void main(String[] args) throws InterruptedException {
ArrayList list = new ArrayList<>();
list.add(new byte[_8MB]);//为了测试大对象
list.add(new byte[_8MB]);
/*list.add(new byte[_7MB]);
list.add(new byte[_6MB]);*/
//多线程演示
/* new Thread(() -> {
ArrayList list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);*/
}
}
新生代垃圾回收

大对象直接入老年区

关于工作线程溢出对进程的影响

这一部分知识是后面经典垃圾回收器的前置知识,可以先看后面,在回头看着
我们JVM中采用的是可达性分析算法来判定是不是垃圾,但是GC RootS尽管目标明确,但是查找过程并不是一件容易的事情,现在Java应用越做越大,光是方法区的大小都可能有数百上千兆,里面的类,常量更是恒河沙数,逐个检查肯定是不现实的
迄今为止,所有的收集器在根节点枚举这一步(初始标记)这一步骤都是需要暂停用户线程的(Stop The World),因为根节点的枚举我们需要必须保证在一个能够保障一致性的快照中才能得以进行——这里的快照说的在整个枚举根节点的过程中 对象引用链就像是冻结在某个时间点一样
为了提高枚举的速度,我们采用了OopMap这种数据结构来达到目的——对栈进行扫描,找到哪些地方存储了对象的引用。
然而,栈存储的数据不止是对象的引用,因此对整个栈进行全量扫描,显然是很耗费时间,影响性能的。因此,在 HotSpot 中采取了空间换时间的方法,使用 OopMap 来存储栈上的对象引用的信息。在 GC Roots 枚举时,只需要遍历每个栈桢的 OopMap,通过 OopMap 存储的信息,快捷地找到 GC Roots。

◉ 栈里和寄存器内的引用
在即时编译中,在特定的位置记录下栈里和寄存器里哪些位置是引用
◉ 对象内的引用
类加载动作完成时,HotSpot 就会计算出对象内什么偏移量上是什么类型的数据
注:把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为有效地址或偏移量,因此,实际地址=所在段的起始地址+偏移量
这样我们的收集器就可以直接知道这些信息,并不需要真正一个不漏地从方法区等GC RootS开始查找
卡表只是我们记忆集的一种具体实现方式,类似Map和HashMap的关系
在我们的分代收集理论的时候,提到了有关对象跨代引用带来的问题(并不是只有新生代和老年代存在这种问题,比如我们的G1部分区域收集会存在相似的问题),为了解决这种问题,引入了记忆集这种数据结构——在新生代中存储

以上这种情况,我们不可能因为新生代的收集,去遍历老年代吧,这样非常耗时,所以采用空间换时间的思想
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,比如我们把这个指针保存在我们的新生代,指针指向的区域是我们的老年代,就说明被指向老年代的哪些区域引用我们这个新生代的对象

三种精度选择
卡表的最简单的形式只是一个字节数组,而HotSpot也是这样做的
CARD_TABLE[this address >> 9]=1;//类似于取余操作

这样我们的字节数组的每一个元素都对应着器标识的内存区域中的一块特定大小的内存块,这个内存块也被称为卡页(HotSpot使用的是2^9次幂,即512字节)
写屏障使用了记忆集来缩减 GC Roots 扫描范围的问题,但是没有解决卡表元素如何维护的问题,例如如何变脏,什么时候变脏
有其他分代区域对象引用了本区域对象时,其对应的卡表元素就应该变脏,
变脏的时机原则上应该发生在引用类型字段赋值的那一刻。
如果是解释执行的字符串,虚拟机有充分的介入空间。但经过即时编译后的代码
已经是纯粹的机器指令流了,因此必须找到一个在机器码层面的手段把维护卡表的动作
放到每一个赋值操作之中。
在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。注意,这不是内存屏障。
写屏障可以看做是虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生
一个环形(Around)通知,提供程序执行额外的动作,赋值前的部分的写屏障叫做写前屏障(Pre-Write Barrier),
赋值后的则叫写后屏障(Post-Write Barrier)。
void oop_field_store(oop* field, oop new_value) {
// 引用字段赋值操作
*field = new_value;
// 写后屏障,在这里更新卡表
post_write_barrier(field, new_value);
}
应用写屏障后,虚拟机会为所有赋值操作生成相应指令,无论更新的是不是老年代对新生代对象的引用,
每次只要对引用进行更新,都会触发更新卡表的动作。这个额外的开销与Minor GC时扫描整个老年代的代价要低得多。

这张图中最后 B C H 是不可达的,是垃圾
三色代表的意义

三色标记遍历过程
假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:
注:如果标记结束后对象仍为白色,意味着已经“找不到”该对象在哪了,不可能会再被重新引用。
可能出现的问题——如果我们的用户线程都被冻结,只有垃圾回收的线程在工作,那么不会出现任何问题,但是如果是用户线程和收集器线程并发执行,那么就可能出现一些一致性带来的问题

此刻之后,对象B/C/D是“应该”被回收的。然而因为B已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮GC不会回收这部分内存。
这部分本应该回收 但是 没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。

B到D 断开,A引用 D
此时切回GC线程继续跑,因为B已经没有对D的引用了,所以不会将D放到灰色集合;尽管因为D重新引用了G,但因为A已经是黑色了,不会再重新做遍历处理。
最终导致的结果是:D会一直停留在白色集合中,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。
漏标必须要同时满足以下两个条件:
这两个条件必须全部满足,才会出现对象消失的问题。那么我们只需要对上面条件进行破坏,破坏其中的任意一个,都可以防止对象消失问题的产生。这样就产生了两种解决方案:
增量更新
增量更新破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用时,就将这个新加入的引用记录下来,待并发标记完成后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次——可以简化理解为黑色对象一旦新插入指向白色对象的引用,它就变为灰色对象

原始快照
原始快照破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,并发扫描结束后,再将这些记录过的引用关系中的会色对象为根,重新扫描一次。——在这可以简化理解为,无论引用关系删除是否,都会按照刚开始扫描的那一刻的对象图快照来进行搜索

会出现这种浮动垃圾问题,但是是不致命的

现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等。
对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:



工程实现中,读写屏障还有其他功能,比如写屏障可以用于记录跨代/区引用的变化,读屏障可以用于支持移动对象的并发执行等。功能之外,还有性能的考虑,所以对于选择哪种,每款垃圾回收器都有自己的想法。
增量更新:黑色对象新增一条指向白色对象的引用,那么要进行深入扫描白色对象及它的引用对象。
原始快照:灰色对象删除了一条指向白色对象的引用,可能会产生了浮动垃圾,好处是不需要像 CMS 那样 remark,再走一遍 root trace 这种相当耗时的流程。
我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。
相关的概念
Serial 收集器是最基本的、发展历史最悠久的收集器
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!

-XX:+UseSerialGC =serial + serialOld //开启SerialGC
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
Serial Old 是 Serial 收集器的老年代版本
ParNew 收集器其实就是 Serial 收集器的多线程并行版本,跟Serial一样也是串行的,也是用于新生代的回收
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R0iIMTUj-1668746726422)(https://gitee.com/song-cheng-liu/boketuchuangtuchuang/raw/master/img/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC84LzQvMTY1MDJjN2I4OGNkYTk1Mg)]

XX:+UseParallelGC ~ -XX:+UseParallelOldGC //表示使用对应的垃圾回收器
-XX:+UseAdaptiveSizePolicy // 是一个开关参数 表示采用自适应的策略 主要调整新生代的大小(eden和幸存区的比例大小)还有晋升老年代的年龄大小
-XX:GCTimeRatio=ratio //表示垃圾时间所占的比例 1/ration+1,默认的ration为99 比例就为0.01 100分钟只允许一分钟进行垃圾回收
-XX:MaxGCPauseMillis=ms //表示一次垃圾回收的时间不超过多少ms
//这两个目标其实是冲突,想让一次GC的时间短,得让堆变小,一次回收的垃圾会少一点 但是堆变小了,我们的垃圾回收的频率就会变高,吞吐量可能就下来了
- XX:ParallelGCThreads=n //表示垃圾回收时并行的线程数量
与吞吐量关系密切,故也称为吞吐量优先收集器
特点:属于新生代收集器也是采用标记复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)
GC自适应调节策略:
Parallel Scavenge 收集器使用两个参数控制吞吐量:
XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)
XX:GCTimeRatio=ratio 直接设置吞吐量的大小
是 Parallel Scavenge 收集器的老年代版本
特点:多线程,采用标记-整理算法(老年代没有幸存区)

XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
//使用CMS垃圾回收器搭配着ParNewGC回收新生代 可能发生并发失败的问题,CMS导致退化为SerialOld回收器
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads //设置并行的线程数 设置并发的线程数
-XX:CMSInitiatingOccupancyFraction=percent //因为我们的并发清理的时候,会产生新的垃圾,所以不能到堆满才开始清理,这个percent表示到堆的比例就开始回收
-XX:+CMSScavengeBeforeRemark //我们重新标记,因为运行所以可能会有新生代的对象,如果我们的重新标记 有新生代对象指向了老年代(但是这个新生代可能是垃圾),会导致我们的可达性分析的时间较长而且无用,所以再重新标记前对新生代进行垃圾回收
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
CMS 收集器的运行过程分为下列4步:
初始标记:标记 GC Roots 能直接关联到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记:就是从GC Roots直接关联的对象开始遍历整个对象图的过程,找出存活对象且用户线程可并发执行,所以不需要Stop The World。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生这些垃圾是在这次CMS的并发回收进行回收,只能下一次进行回收,所以这些垃圾就叫浮动垃圾。并发清除的时候用户线程还行需要运行,是需要内存空间的,所以我们不能等堆都快慢了才进行回收,需要设置一个比例,到达这个比例就开始回收
虽然CMS的初始标记和重新标记也存在Stop The World的问题,但是着两个阶段时间很短,所以总体上对应每次GC停止时间还是比较短的
为什么这样不是追求优秀的吞吐量
CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。
定义: Garbage First 它也是一款跨时代的垃圾回收器
2004 论文发布
2009 JDK 6u14 体验 不足于商用
2012 JDK 7u4 官方支持
JDK 8U40提供了并发的类卸载的支持
2017 JDK 9 默认
适用场景:
JDK8 并不是默认开启的,所需要参数开启
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
实现的思想

Young Collection:对新生代垃圾收集
Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。
Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。
与之前的经典垃圾回收器不同,最大的改变是在于之前的垃圾回收器收集的目标要么是整个新生代,要么是老年代,要么就是Full GC,整个堆的回收,而G1跳出这个禁锢,它可以面对堆内存任何部分来组成回收集来进行回收,衡量的标准不再是它属于哪个分代,而是哪个内存中的垃圾多,回收效益最大,就比如Mixed Collection
Young Collection

分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间!
E:eden,S:幸存区,O:老年代
采用的是复制算法
新生代E的存活对象复制到S幸存区
S幸存区的对象复制到另一个幸存区,或者是GC年龄到了进入O老年代
新生代收集会产生 STW !
Young Collection + CM
在 Young GC 时会进行 GC Root 的初始化标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),类似我们CMS的并发标记,由下面的 JVM 参数决定 -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

Mixed Collection

会对 E S O 进行全面的回收
-XX:MaxGCPauseMills=xxms 用于指定最长的停顿时间!
问:为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
另一个角度四个步骤
Full GC
SerialGC
ParallelGC
CMS
G1
新生代回收的跨代引用(老年代引用新生代)问题(可能一些根对象在老年代)


卡表 与 Remembered Set
在引用变更时通过 post-write barried + dirty card queue
concurrent refinement threads 更新 Remembered Set
JDK 8u20 字符串去重
将所有新分配的字符串(底层是 char[] )放入一个队列
当新生代回收时,G1 并发检查是否有重复的字符串
如果字符串的值一样,就让他们引用同一个字符串对象
注意,其与 String.intern() 的区别
String.intern() 关注的是字符串对象
字符串去重关注的是 char[]
在 JVM 内部,使用了不同的字符串标
优点与缺点
节省了大量内存
新生代回收时间略微增加,导致略微多占用 CPU
-XX:+UseStringDeduplication
在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类

JDK 9 并发标记起始时间的调整**
并发标记必须在堆空间占满前完成,否则退化为 FulGC
JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
JDK 9 可以动态调整
-XX:InitiatingHeapOccupancyPercent 用来设置初始值
进行数据采样并动态调整
总会添加一个安全的空挡空间

查看虚拟机参数命令
java -XX:+PrintFlagsFinal -version | findstr "GC"//在控制台运行
1
可以根据参数去查询具体的信息
低延迟/高吞吐量? 选择合适的GC
首先排除减少因为自身编写的代码而引发的内存问题
查看 Full GC 前后的内存占用,考虑以下几个问题
数据是不是太多?
resultSet = statement.executeQuery(“select * from 大表 limit n”)
数据表示是否太臃肿
是否存在内存泄漏
新生代的特点
新生代内存越大越好么?
不是
新生代内存设置为内容纳[并发量(请求-响应)]的数据为宜*
幸存区需要能够保存 当前活跃对象+需要晋升的对象
晋升阈值配置得当,让长时间存活的对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution
以 CMS 为例:
CMS 的老年代内存越大越好
先尝试不做调优,如果没有 Full GC 那么说明已经很好,否者先尝试调优新生代。
观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent