资料:
https://zhuanlan.zhihu.com/p/45558897
https://tech.meituan.com/2017/12/29/jvm-optimize.html
虚拟机内存区域划分
image.png堆 (heap)
虚拟机管理内存中最大的一块。在虚拟机启动的时候创建。目的就是存对象实例
方法区 (Method Area)
JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池。
JDK1.6及之前,运行时常量池是方法区的一个部分,同时方法区里面存储了类的元数据信息、静态变量、即时编译器编译后的代码等。
JDK1.7及以后,JVM已经将运行时常量池从方法区中移了出来,在JVM堆开辟了一块区域存放常量池。字符串常量池被移到了堆中了。此时常量池存储的就是引用了。JDK1.8以后方法区在元空间,元空间在本地内存。
程序计数器(Program counter Register)
内存中比较小的一块内存区域,作用是当前线程所执行的字节码的行号指示器。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
jvm栈(jvm stacks)
虚拟机描绘java方法执行的内存模型。每个方法执行的时候 都会创建一个帧栈(Stack Frame)用于存储局部变量表,操作栈,动态链接方法出口信息。
栈帧
首先 一个线程对应一个栈帧, jvm调用一个java方法的时候,他对应的类的类型信息中得到这个方法的局部变量区和操作数栈的大小。并依据这个进行分配栈帧内存。压入jvm栈中。 在活动线程中,只有在栈顶的栈帧才是有效的,被称为当前栈帧。与这个栈帧关联的方法被称为当前方法 栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。 在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了。因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
本地方法栈 (Naitve Methord Stacks)
是与虚拟机栈发挥作用非常相似, 虚拟机栈为虚拟机执行java方法, 而本地方法栈为虚拟机使用到的Native 方法服务。
GC相关
GC工作区域
GC的主要工作是在Heap (堆)和 metaSpace(元空间中的方法区)。如果在Direct Memory(直接内存 ) 如果使用的是 DirectByteBuffer,那么在分配内存不够时则是 GC 通过 Cleaner#clean 间接管理。
为什么回收主要在堆和方法区
Java虚拟机栈、本地方法栈、程序计数器这三者是线程私有的,随线程而生随线程而灭。栈中的栈帧随着方法的进入和退出有条不紊的出栈入栈,每个栈帧需要分配多少内存,在类结构确定下来时就是已知的(尽管运行期间会有JIT编译器进行一些优化,但在基于概念模型的讨论中,大体可以认为是编译器可知的)。因此上述这些区域的内存分配和回收都具备确定性,故不需要过多考虑垃圾回收的问题。而Java堆和方法区则不一样:一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支所需的内存也不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的,所以垃圾回收器所关注的重点是位于Java堆和方法区上的内存。
算法决生死(java对象生死判断算法)
引用计数法(Reference Counting)
对每个对象的引用进行计数,每当有一个地方引用它时计数器 +1、引用失效则 -1,引用的计数放到对象头中,大于 0 的对象被认为是存活对象。虽然循环引用的问题可通过 Recycler 算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。引用计数法是可以处理循环引用问题的。
可达性分析,又称引用链法(Tracing GC)
从 GC Root 开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。目前 Java 中主流的虚拟机均采用此算法 此算法的基本思路就是通过一系列的“GC Roots”的对象作为起始点,从起始点开始向下搜索到对象的路径。搜索所经过的路径称为引用链(Reference Chain),当一个对象到任何GC Roots都没有引用链时,则表明对象“不可达”,即该对象是不可用的。
GC Root 可以使用的对象
-
栈帧中的局部变量表中的reference引用所引用的对象
-
方法区中类static静态引用的对象
-
方法区中final常量引用的对象
-
本地方法栈中JNI(Native方法)引用的对象
对象自我救赎
对象经过可达性算法分析后,判断为不可达,那么对象就必死无疑了么?不一定,对象在面临垃圾回收器的处理时,还有最后一次求生的机会。
要kill掉一个对象,至少要经过垃圾回收器的2次标记过程,不可达的对象被第一次标记后会进行一次筛选,筛选的条件是「此对象是否有必要执行finalize()方法」,当对象没有覆盖finalize方法或者已经执行过finalize方法时,会被判断为:没必要执行。如果被判断为有必要执行,则该对象会被放置在一个F-Queue队列,并在稍后虚拟机建立的Finalizer线程中执行finalize()来kill掉对象。在回收前垃圾回收器会对F-Queue队列中的对象进行第二次标记,如果在标记前,对象成功与引用链上的任意对象建立了关联,则会在第二次标记时被移出F-Queue,从而实现自救。
GC垃圾收集算法
Mark-Sweep(标记-清除)
回收过程主要分为两个阶段,第一阶段为追踪(Tracing)阶段,即从 GC Root 开始遍历对象图,并标记(Mark)所遇到的每个对象,第二阶段为清除(Sweep)阶段,即回收器检查堆中每一个对象,并将所有未被标记的对象进行回收,整个过程不会发生对象移动。整个算法在不同的实现中会使用三色抽象(Tricolour Abstraction)、位图标记(BitMap)等技术来提高算法的效率,存活对象较多时较高效。
缺点:
-
效率较低,因为标记和清除这两个过程效率都比较低
-
空间问题,标记清除后会产生大量不联系的内存空间(碎片),导致如果有大内存的对象,那么就无法找到足够大的连续内存空间以供分配。
Mark-Compact (标记-整理/压缩)
算法的主要目的就是解决在非移动式回收器中都会存在的碎片化问题,也分为两个阶段,第一阶段与 Mark-Sweep 类似,第二阶段则会对存活对象按照整理顺序(Compaction Order)进行整理。主要实现有双指针(Two-Finger)回收算法、滑动回收(Lisp2)算法和引线整理(Threaded Compaction)算法等。在青年代采用复制算法是非常合适的,因为青年代的特点是对象数量多,生存时间短,所以空间利用率比较重要,而复制算法对于老年代Old Generation则不太适合,因为老年代的对象数量虽少,但比较稳定存活率高这样会有较多的复制开销,针对这种情况,出现了标记-压缩算法。标记-压缩算法和标记-清除算法类似,先通过标记找出等待回收的对象,然后在清除之前将存活的对象都整理整齐放到一边,然后再清除掉边界以外的内存。
image-20210830152422172.png
复制算法
将完整内存区域分为大小相等的2块,每次只使用其中的一块,当这块内存满了(用完),则将此块内存上的对象都「复制」到另一块空内存上去,然后将用完的那块内存进行垃圾回收。
-
优点 吞吐量高,不需要遍历全堆,只需要处理活动对象。不会有碎片化的问题,因为每次复制都将存活对象从from复制到to的一端
-
缺点 堆利用率较低,因为在复制算法下,只有一半的内存用来存储对象
分代收集
Java堆是垃圾收集器管理的主要内存,由于主流的虚拟机实现中,垃圾收集器大多采用分代式垃圾回收算法(Generational Garbage Collection),所以会将垃圾收集器所管理的堆内存划分为不同的代。
在Java7以前Hotspot虚拟机中将Java堆内存分为3个部分:
-
青年代 Young Generation
-
老年代 Old Generation
-
永久代(1.8删除) Permanent Generation
在Java8以后,由于方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中,所以永久代就不存在了。
[图片上传失败...(image-b01c3b-1630309095177)]
青年代(新生代)
其中青年代中单独划分成了三块——Eden+Survivor+Survivor。大部分对象在Eden区中生成。在这些不同区域上任何一个内存“满”了以后,都会触发一次垃圾收集过程。Java中绝大部分的新创建的对象都被分配到了青年代中的Eden区。当内存不够时,虚拟机将会发动一次MinorGC。
晋升老年代
当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。 为了更好地适应不同程序的内存状况,虚拟机并不是永远要求对象年龄必须达到「阈值」才能提升至老年代。在有的垃圾收集器实现中,如果Survivor空间中相同年龄的对象占用空>Survivor总空间的一半,则此年龄的所有对象就可以提前进入老年代,而不是必须达到阈值。
老年代
大对象直接进入老年代 所谓的大对象是指需要大量连续内存空间的Java对象,长字符串及大容量的数组。安放这些大对象,虚拟机会直接将其放在老年代,因为大对象一般涉及到的引用多,不容易「死」掉。而且大对象占内存,所以直接在老年代为其开辟一块连续的内存就比较合适。如果内存不够分配,虚拟机会触发垃圾收集过程。 长期存活的对象进入老年代 既然虚拟机采用分代收集的策略来管理内存,那么内存回收时就应该相应的判别哪些对象该放在青年代,哪些放在老年代。为此,JVM给每个对象定义了一个年龄计数器。如果对象在Eden出生,并且经过一次MinorGC后仍然存在,则「年龄」增加1岁。当年龄增加到一定数目(如:默认为15岁),就会被提升至老年代。 MinorGC 在MinorGC之前,JVM会首先检查老年代最大可用的连续内存空间是否 > 青年代所有对象总空间,并以其作为MajorGC执行的「担保」。如果大于则MinorGC可以正常执行。否则JVM会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,则继续执行MinorGC,否则则执行MajorGC用来回收足够的内存空间。 在年轻代内存区域上的垃圾收集过程,因为大多数在年轻代上的对象“朝生夕灭”,所以MinorGC非常频繁,一般收集的速度也很快。
MajorGC/Full GC 指发生在老年代的GC,此区域一般对象存活率高,GC一次的速度同城比MinorGC慢10倍以上。
总结 可以晋升老年代的类型
1、分配担保机制
Eden区满时,进行Minor GC,当Eden和一个Survivor区中依然存活的对象无法放入到Survivor中,则通过分配担保机制提前转移到老年代中。
2、对象过大
若对象体积太大,新生代无法容纳这个对象,就会绕过新生代, 直接在老年代分配, 此参数只对Serial及ParNew两款收集器有效。
参数-XX:PretenureSizeThreshold用来设置这个门限值。
3、长期存活的对象
对象头的Mark Word中包含对象的年龄。当年龄增加到一定的临界值时,就会晋升到老年代中。
该临界值由参数:-XX:MaxTenuringThreshold来设置,默认为15,即对象在经历15次minor gc后会晋升到老年代。
4、动态对象年龄判定
如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
CMS和G1
CMS(Concurent Mark Sweep)
从名字可以看出这款收集器是一款比较优秀的基于标记-清除算法的并发收集器。之前也提到过,此收集器的目标在于尽量小的Stop The World间隔时间,用于用户交互比较多的场景。
收集过程
-
初始标记
-
并发标记
-
重新标记
-
并发清除
其中初始标记和重新标记两个步骤仍需要Stop The World间隔。初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots追踪的过程,而重新标记则是为了修正并发标记期间由于用户程序继续执行可能产生变动的那部分对象的标记记录,此阶段会比初始标记长一些,但远小于并发标记的时间。
整个阶段并发标记和并发清除是耗时最长的两个阶段。但是由于CMS收集器是并发执行的,故可以和用户线程一起工作,所以从整体上CMS收集器的工作过程是和用户线程并发执行的。
优点:
GC收集间隔时间短,多线程并发。
缺点:
-
并发时对CPU资源占用多,不适合CPU核心数较少的情况。
-
且由于采用标记清除算法,所以会产生内存碎片。
-
无法处理浮动垃圾。
G1 (Garbage-First)
Java11官网描述中已经说明:G1取代了Concurrent Mark-Sweep(CMS)收集器。它也是默认的收集器。表明在Java11中G1是默认的垃圾收集器,而CMS收集器从JDK 9开始就不推荐使用了
G1特点:
-
并行与并发:
G1能充分利用多CPU下的优势来缩短Stop The World的时间,同时在其他部分收集器需要停止Java线程来执行GC动作时,G1收集器仍然可以通过并发来让Java线程同步执行。
-
分代收集:
与其他收集器一样,分代的概念在G1中任然被保留。可以不需要配合其他的垃圾收集器,就独立管理整个Java堆内存的所有分代区域,且采用不同的方式来获得更好的垃圾收集效果。
-
空间整合:
G1从整体来看,使用的是标记-压缩算法实现的,从局部两个Region来看,采用的是复制算法实现的,对内存空间的利用非常高效,不会像CMS一样产生内存碎片。
-
可以预测的停顿:
除了追求低停顿以外,G1的停顿时间可以被指定在一个时间范围内。
如果不计算维护Remenbered Set的操作,G1收集器的工作阶段大致区分如下:
-
初始标记
-
并发标记
-
最终标记
-
筛选回收
网友评论