目录
目录
目录
JVM内存分配
说到JVM垃圾回收,我还是得提一下JVM内存分配,虽然相信大家对这部分已经很懂了,但是我不懂呀,我讲讲你们看看有什么错的地方还请帮我指正(嘿嘿😁)!
JVM的内存结构大概分为:
- 堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。
- 方法区(Method Area):线程共享。存储类信息、常量(1.7以后常量池放在了堆中)、静态变量、即时编译器编译后的代码。
- 虚拟机栈栈(VM Stack):线程私有。生命周期与线程相同,每个方法被执行的同时会创建栈桢(下文会看到),主要保存执行方法时的局部变量表、操作数栈、动态连接和方法返回地址等信息,方法执行时入栈,方法执行完出栈,出栈就相当于清空了数据,入栈出栈的时机很明确,所以这块区域不需要进行 GC。储局部变量表、操作栈、动态链接、方法出口,对象指针。
- 本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。这块区域也不需要进行 GC
- 程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。
关于方法区的实现,其实JVM在1.8以前和1.8及以后的实现方式是不一样的。上图!
在 Java 8 之前有个永久代的概念,实际上指的是 HotSpot 虚拟机上的永久代,它用永久代实现了 JVM 规范定义的方法区功能,主要存储类的信息,常量,静态变量,即时编译器编译后代码等,不过由于永久代有 -XX:MaxPermSize 的上限,所以如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易造成 OOM,有人说可以把永久代设置得足够大,但很难确定一个合适的大小,受类数量,常量数量的多少影响很大。所以在 Java 8 中就把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了,也就不会进行 GC,也因此提升了性能(发生 GC 会发生 Stop The Word,造成性能受到一定影响,后文会提到),也就不存在由于永久代限制大小而导致的 OOM 异常了,也方便在元空间中统一管理。综上所述,在 Java 8 以后这一区域也不需要进行 GC。
垃圾识别法
1. 引用计数法
引用计数算法在每个对象都维护着一个内存字段来统计它被多少”部分”使用—引用计数器,每当有一个新的引用指向该对象时,引用计数器就+1 ,每当指向该引用对象失效时该计数器就-1 ,当引用数量为0的时候,则说明对象没有被任何引用指向,可以认定是”垃圾”对象.
缺点:无法解决循环引用的问题!
public class TestRC {
TestRC instance;
public TestRC(String name) {
}
public static void main(String[] args) {
// 第一步--a,b的引用计数1
A a = new TestRC("a");
B b = new TestRC("b");
// 第二步 -- a,b的引用计数1+1=2
a.instance = b;
b.instance = a;
// 第三步 -- a,b的引用计数2-1=1
a = null;
b = null;
}
}
第三步,虽然 a,b 都被置为 null 了,但是由于之前它们指向的对象互相指向了对方(引用计数都为 1),所以无法回收。
2. 可达性算法
该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。
如图,对象a,b不在roots路径上,所以是不可达的,但是此时还没被判定为回收对象,还需要进行一次判定,当发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法,在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!
注意: finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收!这一点切记!
在Java语言中,可作为GC Roots的对象包含以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
- 对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。
- 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
- 在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。
- 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)
- 常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。
- 本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)
- 在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。
3. 三色标记算法
三色标记算法是一种常见的垃圾收集的标记算法,属于根可达算法的一个分支,垃圾收集器CMS,G1在标记垃圾过程中就使用该算法。
白色:
1:在标记开始时,堆内存中的对象都是白色的。
2:在标记结束时,仍然是白色的对象,将会被视为已死的对象而被清除。
灰色:该对象已经被标记过了,但该对象的引用对象还没标记完。
黑色:该对象已经被标记过了,并且他的全部引用对象也都标记完了。
三色标级的过程分为3个阶段。初始标记 -> 并发标级 -> 重新标记
初始标记:遍历所有的根对象,将根对象和直接引用的对象标记为灰色。在这个阶段中,垃圾回收器只会扫描被直接或者间接引用的对象,而不会扫描整个堆。(需要STW)
并发标级::在这个过程中,垃圾回收器会从灰色对象开始遍历整个对象图,将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑色。并发标记过程中,应用程序线程可能会修改对象图,因此垃圾回收器需要使用写屏障(Write Barrier)技术来保证并发标记的正确性。(不需要STW)
重新标记:重新标记的主要作用是标记在并发标记阶段中被修改的对象以及未被遍历到的对象。这个过程中,垃圾回收器会从灰色对象重新开始遍历对象图,将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑色。(需要STW)
在重新标记阶段结束之后,垃圾回收器会执行清除操作,将未被标记为可达对象(白色)的对象进行回收,从而释放内存空间。这个过程中,垃圾回收器会将所有未被标记的对象标记为白色(White)。
- 写屏障:是垃圾回收器一种用来保证并发标记正确性的技术。当应用程序线程修改了一个对
象的引用时,写屏障会记录该对象的新标记状态。如果该对象未被标记过,那么它
会被标记为灰色,以便在垃圾回收器的下一次遍历中进行标记。如果该对象已经被
标记为可达对象,那么写屏障不会对该对象进行任何操作。
- 缺点:由于采用了写屏障技术,会带来额外的性能开销,而且由于在某些高度并发的条件
下,对象的引用变化频繁且难以捕捉,所以某些垃圾回收器会采取保守策略,从而导
致多标或者漏标的情况。
1: 多标:发生在并发标级的过程中,一般是应用线程把本来由引用的对象引用
删除了,导致该对象变成了一个浮动的垃圾对象(没有了引用,但是被标记
上了颜色),这种情况不用担心,等到下一洗GC的时候会重新标级删除。
2: 漏标:这种情况比较严重,是应为本应该存活的对象被错误的标级为删除对
象。解决这种方式,不同垃圾回收器的处理方式不太一样。例如GMS和G1
垃圾回收器。
对象的分代晋升
Java的堆由年轻代(Young)和老年代(Old)组成。
年轻代由Eden区和Survivor区组成,Eden区占整体年轻代的80%,survivor区占整体年轻代的20%,其中Form区和To区各占10%。可以通过-XX:SurvivorRatio参数调整。
大部分对象一开始都会出现在Eden区,当Eden区满时,会触发GC,非存活的对象会被标记为死亡,存活的对象会被转移代survivor区。如果survivor区的内存区域也用完,多次GC后,存活的对象会被转移到Old区
对象晋升代老年代,一般看对象的大小和年龄:
1、躲过15次GC。每次垃圾回收后,存活的对象的年龄就会加1,累计加到15次(jdk8默认的),也就是某个对象躲过了15次垃圾回收,那么JVM就认为这个是经常被使用的对象,就没必要再待在年轻代中了。具体的次数可以通过 -XX:MaxTenuringThreshold 来设置在躲过多少次垃圾收集后进去老年代。
2、动态年龄规则:如果在Survivor空间中小于等于某个年龄的所有对象大小的总和大于Survivor空间的一半时,那么就把大于等于这个年龄的对象都晋升到老年代。
解释:其实就是把对象按照年龄排序,然后从小到达加起来,求对象的内存大小总和,当这个和超过surivior空间的一半时,就把此时年龄大于当前对象年龄的对象全部晋升到老年代。
3、大对象直接进入老年代,-XX:PretenureSizeThreshold 来设置大对象的临界值,大于该值的就被认为是大对象,就会直接进入老年代。
参考:
1. 看完这篇垃圾回收,和面试官扯皮没问题了(现在点外卖是天价!)
3. 深入理解Java虚拟机(第3版)