GC算法
一、对象存活判断
在正式进入GC算法之前先要了解JVM是怎么在GC过程判断对象存活的。
常见的判断方法有以下两种
1. 引用计数法
简而言之就是给 Java 对象添加一个引用计数器,每当有一个地方引用它时,计数器 +1;引用失效则 -1。当GC发生时把引用为0的全部清理掉
比如:
A a = new A();
B b = new B();
a.b = b;
对象a引用了对象b,a的计数器为0,b的计数器为1,当GC发生时a被回收,b的计数器-1变成0,下一次GC发生时b就会被回收。
这个方法虽然简单粗暴,但是有一个致命性的问题,那就是无法判断循环引用
A a = new A();
B b = new B();
a.b = b;
b.a = a;
比如遇到这种情况,即使a和b没有被其他对象引用,也无法被回收。
2. 可达性分析
将一系列的 GC Roots 对象作为起点,从这些起点开始向下搜索,所有在引用链中的对象即为存活对象。
可作为 GC Root 的对象有:
- Java虚拟机栈(栈帧的本地变量表)中引用的对象
- 本地方法栈 中 JNI引用对象
- 方法区 中常量、类静态属性引用的对象
二、GC算法
1. 复制算法
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
复制算法的问题有两个:
- 空间浪费,内存被分为2部分
- 效率不稳定,当存活对象过多的时候,效率会非常低
2. 标记-清除算法
顾名思义,这种方法分为两个步骤,标记和清除
- 标记步骤:GC开始的时候,JVM先从GC Roots开始遍历整个堆,将存活对象进行标记
- 清除步骤:将未被标记的所有对象全部清理掉
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4lOKN0p-1691029166510)(https://joshua-1301529810.cos.ap-chengdu.myqcloud.com/img/%E6%A0%87%E8%AE%B0-%E6%B8%85%E9%99%A4%E7%AE%97%E6%B3%95.png)]
缺点:造成大量的内存碎片
3. 标记—整理算法
标记-整理是对标记-清除的改进,同样分为两个步骤,只是将清除换成了整理
- 整理步骤:将被标记的对象全部移动到内存一端,然后将存活对象的后面内存全部释放
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyT1Kw7a-1691029166511)(https://joshua-1301529810.cos.ap-chengdu.myqcloud.com/img/%E6%A0%87%E8%AE%B0-%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95.png)]
4.分代收集
分代收集与其说是一种算法,不如说是解决方案。
分代收集将堆内存分为新生代和老年代,针对新生代和老年代的特性采用不同的GC算法进行回收。
新生代:创建和消亡频繁,存活率低,因此采用复制算法
老年代:存活率高,采用标记-清除或者标记-整理
当新生代的对象经过多次GC之后依然存活就会被复制到老年代
三、JVM的做法
JVM采用分代收集的方式进行GC,将堆内存分为了新生代、老年代和持久代,并且将新生代分为Eden、From Survivor、To Survivor三个区间
1. 内存区划分
- 新生代: Eden + From Survivor(S0) + To Survivor(S1)
- 老年代
- 持久代:方法、static变量、JAVA类(1.8之后变为元空间,作用于本地内存,被所有JVM实例共享)
划分Survivor区的好处是防止老年代区被过快填满而发生Full GC,这个过程将会非常慢,如果没有Survivor做缓冲区,每次新生代发生GC都会转化为老年代,老年代很快就满了。
而将Survivor区分为两个区间是为了减少内存碎片,当发生GC的时候,会将Eden和From Survivor的所有存活对象复制到To Survivor,清空Eden和From Survivor,然后将From Survivor和To Survivor角色替换,这也是两个Survivor区大小一样的原因。
2. 内存区的比例
- 新生代:老年代 = 1:2, 可通过参数-XX:NewRatio配置
- Eden : S0 : S1 = 8 : 1 : 1, 可通过参数-XX:SurvivorRatio配置
- 新生代转化为老年代的复制次数为15,可以通过参数-XX:+MaxTenuringThreshold配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kD1qxk1A-1691029166511)(https://joshua-1301529810.cos.ap-chengdu.myqcloud.com/img/JVM%E5%A0%86%E5%86%85%E5%AD%98%E5%88%86%E4%BB%A3.jpg)]
JVM内存结构
根据《Java虚拟机规范》,JVM的内存数据结构分为以下几个部分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YdD15XEL-1691029166512)(https://joshua-1301529810.cos.ap-chengdu.myqcloud.com/img/JVM%E5%86%85%E5%AD%98%E7%BB%93%E6%9E%84.jpg)]
不过我们都知道JVM的实际制定不一定要完全按照规范,在JDK7以前HotSpot虚拟机都是以“永久代”的形式实现方法区,JDK7的时候将运行时常量池(包含类信息、字符串常量池等)移动到了堆内存中,而在JDK8使用了“元空间”完全代替了老年代,但是元空间是放在本地内存中,由多个JVM实例锁共享的,所以这和《Java虚拟机规范》中方法区在JVM实例内部已经不符合了。
1. 方法区
方法区存储了类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
在Hotspot虚拟机中对于该区域的实现发生了多次的变化。
- JDK6以前:使用永久代实现方法区
- JDK7:依然使用永久代保存类型信息,但是常量池和静态变量移动到了堆内存
- JDK8: 使用元空间实现方法区,且元空间存在于本地内存,多个JVM共享,这意味着常量池可以被多个JVM共享,减少了内存的消耗
所以JAVA8中的实际内存结构应该是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-psyNWBuZ-1691029166512)(https://joshua-1301529810.cos.ap-chengdu.myqcloud.com/img/Java8%E5%86%85%E5%AD%98%E7%BB%93%E6%9E%84.jpg)]
2. 堆
这个很熟悉了,堆是整个JVM管理的内存中最大的一块,它的唯一目的就是存储对象,Java对象几乎全部都存放在对上,《Java虚拟机规范》中提到所有的对象和数组都应当在对上分配(其实数组也是一个特殊的对象,只不过是由虚拟机操作字节码生成的,格式为[Lxxx.xxx.xxx),之所以说是几乎是因为现在Java确实存在这种手段。
在GC算法的笔记中中也提到过,HotSpot采用分代收集做GC收集,所以JVM堆从逻辑上分为了新生代和老年代(默认比例1:2),其中新生代由由Eden和2个Survivor区组成(默认比例8:1:1)。之所以说是“逻辑上”是因为从G1收集器开始已经出现了不使用分代进行GC的做法,在这种情况下JVM仅保留了逻辑概念上的分代,以便于使用分代回收的收集器依然能够工作。以G1收集器来看,它就是将整个堆分为若干个小块,每次进行GC的时候会根据哪块内存中存放的垃圾最多,将存活的对象复制到新的块中,然后释放掉这一块内存,所以在G1收集器下新生代和老年代都是一部分内存块的集合罢了。
3. 虚拟机栈
每个线程都会拥有其对应的栈,其生命周期是和线程一致的。
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,JAVA虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口德国信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
4. 本地方法栈
本地方法栈与虚拟机栈功能很相近,区别就是本地方法栈是为Native方法服务,《Java虚拟机规范》中没有对本地方法栈做任何规定,甚至于连我们的HotSpot就直接将本地方法栈和虚拟机栈合二为一了。
5. 程序计数器
这个放在最后主要是因为这部分对于开发者没有直接关系,它只和JVM执行JAVA字节码有关系。我们可以将程序计数器比作字节码的行号指示器,它记录了当前线程执行的是那一条字节码指令,从而可以让线程在切换后恢复到正确的执行位置。
如果线程正在执行的是Java方法,这个计数器记录的就是字节码指令的地址;如果执行的是native方法,这个计数器值就该为Undefined,程序计数器也是《Java虚拟机规范》中唯一一个没有规定OOM的区域。
JVM参数
1.常用JVM参数
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -Xmn:新生代大小
- -XX:NewRatio:设置新生代和老年代的比值。如:为3,表示年轻代与老年代比值为1:3
- -XX:SurvivorRatio:新生代中Eden区与两个Survivor区的比值,数值为Eden区占比,默认为8。注意Survivor区有两个。如:为3,表示Eden:Survivor1: Survivor2 =3:1:1,一个Eden区占整个新生代的1/5
- -XX:MaxTenuringThreshold:设置转入老年代的存活次数。如果是0,则跳过Survivor直接进入老年代
- -XX:PermSize、-XX:MaxPermSize:分别设置永久代最小大小与最大大小(Java8以前)
- -XX:MetaspaceSize、-XX:MaxMetaspaceSize:分别设置元空间最小大小与最大大小(Java8以后)
2.收集器设置
- -XX:+UseSerialGC:使用Serial+SerialOld
- -XX:+UseParallelGC:使用ParallelScavenge+SerialOld
- -XX:+UseParalledlOldGC:使用ParallelScavenge+ParallelOld (JDK8默认组合)
- -XX:+UseConcMarkSweepGC:使用ParNew+(CMS & SerialOld)
- -XX:+UseG1GC:使用G1 (JDK9之后默认)