当前位置: 首页 > news >正文

JVM G1笔记

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

java 引用类型

java 引用类型:类类型(class type) 指向动态创建的类实例

                        数组类型(array type) 指向动态创建的数组实例

                        接口类型 interface type 实现了某个接口的类实例或数组实例

                        null 引用的默认值,(当一个引用不指向任何对象时,它的值为null)

JVM 内存模型

 java内存区域

5a97123ab07ee79f608caa8d4706d9401b6.jpg

Java7中已经将运行时常量池存放在Java 堆(Heap)中。并且将String pool (介绍)移到堆中。而在Java8中,方法区改为元空间,并且存放在本地内存。

Program Counter Register pc寄存器

    jvm 支持多线程,为了保证多线程切换后恢复到正确的执行位置,每个线程都有一个独立的pc寄存器,当虚拟机线程执行的当前方法为java方法,则计数器纪录虚拟机正在执行的字节码指令地址。若方法为Native Method 则存undefined。

Java Virtual Machine Stack Java虚拟机栈

    JVMStack 与线程同时创建,生命周期相同,用于存储栈帧(Stack Frame),因为除了栈帧的出栈和入栈外,JVM不再受其他因素影响,所以栈帧可以在堆(非java heap)中分配。(java 语言程序里分配在java stack中的数据,从实现虚拟机的程序角度上看则可能分配在heap(系统heap) 中)。

Java Heap java堆

    是提供所有线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域,在JVM启动时创建,它存储了被自动内存管理系统(garbage collector)所管理的对象。

    随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有的对象都会分配在堆上也变得没有那么绝对。

Permanent Generation

    方法区(method area)是供各个线程共享的运行时内存区域。方法区与传统语言中的编译代码存储区(storage area for compiled code)或操作系统进程的正文段(text message)的作用类似,它存储每一个类的结构信息,运行时常量池,字段和方法数据构造函数和普通方法的字节码内容等等。

    方法区作为堆的一个逻辑部分,

    JDK1.7 将永久代的string pool 移出,1.8 改为Metaspace 存放类的元数据

    这个区域的gc目标主要是为了常量池的回收和类型的卸载,OSGI。

Runtime Constant Pool 运行时常量池

    运行时常量池是class文件中每一个类或者接口的常量池的运行时表示形式。Runtime constant pool 在Method area 中分配,在加载类或接口到JVM中后,创建对应的runtime constant pool ,除了保存class文件中描述的符号引用外,还会把翻译出的直接引用也存储在运行时常量池。

cf6d919c6746aa4f620592b5ee9afac4034.jpg

intern()

82f85e93e219a85fcc811968a308d35db5b.jpg

04d327b21804a5afdf49c2efda23ba6268c.jpg

 栈帧中 本地变量表与 variable slot ,与gc root

    操作数栈, 深度在编译时写入code max_stacks中,

    动态连接 符号引用转化为直接引用有两种, 一部分在类加载阶段或第一次使用时转化为直接引用,称为静态解析。另一部分在每一次运行期间转化为直接引用,称为动态连接

    方法返回地址 返回字节码指令或者 异常

a39f00cbb3e810ca9cc72e4d15704aa4b02.jpg

G1堆中分为若干个region,region根据堆的尺寸改变,在1m-32m之间为2的幂,每个分区关联一个Rset(remembered set,用于纪录分区外指向分区内的引用,优化堆的扫描)。同样分 老年代,年轻代(eden,survivor),多了一个Humongous Region(巨型对象),巨型对象就是大小达到甚至超过50%region空间的对象,巨型分区分为 巨型开始(Humongous start) 和 巨型连续(Humongous continues)。巨型分区是老年代的组成部分。

TLAB 为了优化线程分配对象的速度,在eden区开辟了一小块空间作为TLAB (volatile),对于g1来说TLAB就是Eden的一个Region

PLAB Promotion Local Allocation Buffers 用于年轻代回收,

Rset Remembered Set 跟踪指向某个Region内的对象引用

Cset Collection Set 保存GC中将执行垃圾回收的Region ,GC时在Cset中所有存活数据都会被复制/移动

JVM 字节码


JVM GC策略及垃圾收集器

GC 策略

    引用计数法

        简单,但是没有办法解决对象间的互相循环引用的问题。

    可达性分析算法

        以GC Root 为起点,向下搜索,走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连接,就说对象不可达。

        GC ROOT 包括

        1)栈帧中本地变量表引用的对象

        2)方法区中类静态属性(static)引用的对象

        3)方法区中常量(CONSTANT_VALUE)引用的对象

        4)本地方法栈中JNI引用的对象

    标记清除法

        mutator 除了垃圾收集器之外的部分,mutator的职责是NEW(分配内存)、READ(从内存读取内容)、WRITE(写入内存)

        collector 垃圾收集

        在标记阶段,从根对象开始进行扫描,对不可以访问到的对象都打上一个标记,一般在对象的head中,将其纪录为不可达对象。在清除阶段,将标记的对象进行回收。

        深入理解JVM中--》标记要回收的对象,清除标记的对象,

        深入JVM & G1和部分博文中--》对可以访问到的对象打标记,清除未打标记的对象,比如博文

        // TODO 查文献

        查证得:标记存活对象,清楚未标记的对象。 三色标记法

        缺点是会产生大量的空间碎片,因为回收后的空间是不连续的。在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续的内存空间的工作效率要低于连续的空间。

    复制算法

        将内存分成两块,每次只使用一块,gc时将存活对象复制到未使用的内存块中,清除正在使用的内存块的所有对象,并且交换两个内存的角色。

        缺点是内存利用率会降低,使用场景Eden Survivor。

    标记整理法 也叫标记压缩

        针对老年代特性提出,标记与标记清除法一样,整理为让所有存活的对象向一端移动,然后清除边界以外的内存。当成功执行压缩后,已用和未用的内存都各自一边,彼此之间维系着一个纪录下一次分配起始点的标记指针,当为新对象分配内存时,则可以使用指针碰撞(Bump the Pointer)技术修改指针的偏移量将新对象分配在第一个空闲内存位置上。

    分代收集算法

        比如老年代 年轻代分代收集,根据垃圾回收对象的特性,分成不同代,并将不同代的内存区间的特点,使用合适的算法回收。

    增量算法

        基本思想是如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集已小片区域的内存空间,接着切换到应用程序线程。直到垃圾收集完成。使用这种方式能减少系统的停顿时间,但是因为线程切换和上下文转换的消耗,会使垃圾回收的成本上升,造成系统吞吐量下降。

        增量收集的基础仍是标记清除和复制算法。

垃圾回收器

    Serial/Serial Old 收集器

        Serial 收集器作用于年轻代中,采用复制算法、串行回收和stop-the-world的机制进行回收;

        Serial Old 作用于老年代中, 在老年代标记存活对象,检查堆内空间,留下幸存对象(清除),从头开始,顺序填满堆空间,将存活对象连续的存放在一起(整理),部分收集器的早期full gc 是使用的Serial Old 比如G1,CMS 后期转为parallel收集器。

    parNew 收集器

        Serial 的多线程版本,采用并行收集方式回收年轻代。一般和CMS 收集器配合。

    Parallel /Parallel Old 收集器

        垃圾收集器中吞吐量和低延迟这两个目标本身是互相矛盾的,因为如果选择以吞吐量优先,那么必然需要降低内存回收的频率,不管是Parallel 还是CMS 还是G1 都要遵循这个规律。

        Paralle/Parallel Old收集器是以吞吐量为核心的垃圾收集器,Paralle采用复制算法,Paralle Old 采用了标记压缩法,同样基于并行回收和stop-the-world机制。在吞吐量优先的场景中,Paralle/Parallel Old基本上优于所有垃圾收集器。

    CMS (Concurrent-Mark-Sweep) 收集器

        在Parallel收集器的基础上,对低延迟的考虑设计了CMS收集器,CMS是老年代收集器,采用标记-清除法。

        CMS gc有4个主要阶段,初始标记(Initial-Mark)阶段、并发标记(Concurrent-Mark)阶段,再次标记(Remark)阶段和并发清除(Concurrent-Sweep)阶段。

        初始标记阶段会出现stop-the-world,主要是标记根节点及可达对象,并发标记阶段将之前不可达对象标记为垃圾对象,为了保证工作线程对标记后的垃圾对象做出修改建立引用关系,会有一次再次标记阶段也会触发stop-the-world,之后会进入到并发清除阶段进行内存回收。

        当老年代达到一定大小时,或者因为使用标记清除法,大对象无法分配时会触发一次Full gc即(serial old)进行清理和压缩整理,但是时间非常长。不可控。

        CMS与JDK9 废弃(JEP 291)。吞吐量 parallel>CMS>G1 ; GC时延则是G1>CMS>Parallel;

    G1

        G1 GC 切分堆内存为多个Region 从而避免了很多GC操作在整个堆或者整个年代进行。G1的年轻代收集阶段上一个并行的独占收集器。和其他HotSpot垃圾收集器一样,当一个年轻代收集进行时,整个年轻代都会被回收,G1老年代与其他的收集器不同,G1的老年代回收依次只需要扫描一小部分的Region,并且与年轻代一起回收。G1一旦整个堆空间占有率达到指定的阈值,G1会立即启动一个独占并行的初始标记阶段进行垃圾回收。初始阶段一般和年轻代GC一起运行,一旦初始标记结束,并行多线程的标记阶段就开始启动取标记所有老年代还存活的对象,当这个标记阶段运行完毕之后,为了再次确认是否由逃过扫描的对象,启动一个独占式的再次标记阶段,尝试标记所有遗漏的对象。

        CSet里面可以包含多少Region取决于多少空间可以被释放,G1的Region类型,Unused Region (未分配,不属于任何代)、Eden Region、Survivor Region、Humongous Region 属于老年代。如果标记阶段结束后一个老年代的Region不存在对象那么它会被放回可用Region队列

    Epsilon

        一个处理内存分配但不实现任何实际内存回收机制的GC。一旦可用的Java堆耗尽,JVM将关闭。。。。。。。

        主张性能优先,为提高性能不去GC,让应用程序自己去管理内存,

  • JVM将内存分成两个部分来管理:堆和栈。这就是为什么当缺少内存时会有两个不同的错误(OutOfMemoryError和StackOverflowError)。栈内存只能被当前线程和当前执行的方法访问,因此,当线程离开当前执行的方法,这块内存会被自动释放,而无需额外垃圾回收器。然而,堆内存可以被整个应用程序在任何时刻访问,这意味着独立的垃圾回收器需要区分内存块什么时候才不被使用,可以被回收。
  • 基本数据类型内存总是在栈上分配,因此这对垃圾回收器没有压力。如果代码中基本上都使用基本类型,那么垃圾回收器处理的对象就少了。
  • 不产生垃圾不等于不创建对象,如果对象创建满足以下几个条件,仍然可以在创建对象之后不需要垃圾回收器:
    • 应用程序或者库在初始化的时候生成有限个数的对象,然后不断复用这些对象。但是这需要依赖开发者非常熟悉应用程序的内存占用。
    • 有的时候编译器可以发现一些特定对象不会在方法外使用,这被称为逃逸分析。当确认对象生命周期不会超过方法,其内存可以分配到栈而非堆(栈上分配)。因此,这些对象占用的内存会在当前方法结束的时候自动消除。

    用于性能测试、压力测试、VM接口测试等非常短暂的工作。

    ZGC

        是一个可扩展的低延迟垃圾收集器。

  • GC暂停时间不应超过10毫秒
  • 处理堆的大小从相对较小(几百兆字节)到非常大(多太字节)不等
  • 与使用G1相比,应用程序吞吐量减少不超过15%
  • 为未来的GC功能和优化利用有色对象指针和加载屏障奠定基础
  • 最初支持的平台:Linux / x64

        ZGC是一个并发的,单代的,基于区域的,NUMA感知的压缩收集器。Stop-the-world阶段仅限于根扫描,因此GC暂停时间不会随堆或活动集的大小而增加。

        ZGC的核心原则就是组合使用加载屏障(load barrier)与有色对象指针(colored object pointers,colored oops)。正是采用了这两项技术,ZGC才能做到一些操作的并发执行,比如在Java应用运行的时候,进行对象位置的重新分配。从Java线程的角度来讲,加载Java对象中的引用域会受到加载屏障影响。除了对象地址以外,有色对象指针还会包含加载屏障所需的其他信息,用来决定Java线程在使用指针之前是否要采取一些额外的措施。例如,如果对象有可能会被重新分配地址的话,加载屏障会探测到这种情况并采取相应的措施。

        介绍

        

G1 内存模型G1 GC策略

    G1的垃圾收集

        G1的垃圾收集主要分4种,年轻代收集周期,多级并发标记周期、混合收集周期和Full GC;

        Full GC 通常在转移失败(担保失败,目标空间用尽,甚至溢出,通常是因为没有足够多余的空间晋升对象)

        年轻代收集:存活对象从年轻代晋升到老年代的过程叫做“tenuring”对象,老化的阈值称为“任期阈值”(tenuring threshold),对象晋升到Survivor Region或老年代Region 的过程是在GC线程的晋升本地分配缓冲区(promotion local allocation buffer,PLAB)中进行的。

        巨型对象采用的是一种独立且复杂的分配路径,它无法利用年轻代里的TLAB和PLAB,确定分配对象开始的位置时成本会非常大,在JDK8u40中,巨型分区一旦没有外部引用指向自己,那么将会在年轻代收集中被回收。

        混合收集:

        使用增量算法。随着老年代里面的对象越来越多,为了避免堆空间继续上涨,JVM会启动一个混合收集,为了识别出老年代垃圾最多的Region,同时计算分区的活跃度,需要在对象分配、晋升及标记周期触发的比例上有一个微妙的平衡,使JVM不会耗尽空间,JVM会设置一个占用阈值就是InitiatingHeapOccupancyPercent 默认是45是java堆总数的45%,简称IHOP,当老年代占用比例达到了IHOP则会触发并发标记,在标记结束的时候,G1会计算每个老年代分区的存活对象个数,并且会评定每个Region的GC价值。

        一次混合收集周期可能会有多次混合收集收集,混合收集周期只能在达到IHOP阈值并且完成并发标记周期之后才能启动。

        -XX:G1MixedGCCountTarget和-XX:G1HeapWastePercent 这两个重要的参数用于决定一个混合收集周期中包含混合收集的总数。

        -XX:G1MixedGCCountTarget 缺省8,意义是给标记周期结束之后所能启动混合收集的数目设置一个物理限制。

        每次混合收集的老年代CSet最小数量 = 混合收集周期将回收的候选老年代分区/G1MixedGCCountTarget        

        -XX:G1HeapWastePercent  缺省为堆总大小的5%,当G1垃圾收集器达到堆废物阈值百分比,G1就不会在启动新的混合收集,同时混合收集周期也结束。设置堆废物百分比本质上是你愿意浪费一定数量的堆空间,通过它们可以有效提升混合收集周期的效率。

        因此每个混合收集周期中所包含的混合收集数可以通过两个方面控制,1、每次混合收集老年代CSet的最小数量,2、堆废物百分比。

        CSet

        是一系列被回收Region的集合,对于年轻代收集,CSet只容纳年轻代Region,对于混合收集,CSet会收集年轻代和一部分老年代Region。

        -XX:G1MixedGCLiveThresholdPercent 缺省为一个Region 分区的85%大小,任何一个低于活跃度阈值的老年代分区都会被包含在混合收集的CSet中。

        -XX:G1OldCSetRegionThresholdPercent 缺省为总大小的10%,这个参数设置了每个混合收集所能收集的老年代分区数量上限。

        RSet

        为了更好的收集Region并且避免全部扫描,每个Region都会由一个RSet维护并跟踪外部对本Region所拥有的引用,当G1进行年轻代GC或者混合收集时,它会扫描包含在CSet 中的分区中的RSet。在Region的对象被移动时RSet也会更新引用。简单来说,RSet里面存的是别的Region中对本Region的引用,也就是存活对象。

        RSet的数据结构是Hash表,里面的数据是Card Table(堆中每512byte 映射在 card table 1 byte),每个Region划分为若干个块(Chunks),G1中最小的单元是512字节的堆块(Card)

        CardTable : 为了支持高频率的新生代回收,虚拟机使用一种叫做卡表(card table)的数据结构,卡表作为一个byte位的集合,,每一个比特位可以用来标识老年代某一区域中的所有的对象是否持有新生代对象的引用,这样新生代GC可以不用花大量的时间扫描老年代对象,来确定每一个对象的引用,而可以先扫描卡表,只有卡表标识为1,才需要扫描该区域的老年代对象,为0则一定不包含新生代的引用。

        G1 GC只在两个场景中依赖RSet:

  • 老年代到年轻代的引用:G1 GC维护了从老年代区间到年轻代区间的指针,这个指针保存在年轻代的RSet里面。
  • 老年代到老年代的引用:从老年代到老年代的指针保存在老年代的RSet里面。

        Remembered Set是一种抽象概念,而card table可以是remembered set的一种实现方式。 

        Remembered Set是在实现部分垃圾收集(partial GC)时用于记录从非收集部分指向收集部分的指针的集合的抽象数据结构。

        gc收集的过程只需要保证被收集掉的一定是垃圾,这样就不会引发mutator的正确性问题。GC并不需要保证所有的垃圾都被立即收集掉了,即没有被收集的可以是垃圾,没关系的,可能下一次就收集掉了。

        使用card table的remembered set,只要card的粒度大于一个word,那它都是不准确的。这种不准确性跟保守式GC的不准确性类似,虽然不影响GC的正确性(活的对象在GC后仍然会式活的),但却会带来一定程度的内存开销(少量死的对象在某次GC后可能还没被回收)。这种死了的对象叫做floating garbage。使用card marking的取舍就是要尽量让mutator快,而collector付出更多代价,消耗内存也增多。 

        以上部分摘自 http://hllvm.group.iteye.com/group/topic/44381#post-272188       

        对于某些应用有可能某个特定的Region(包括RSet)非常受欢迎,于是在这一个Region会发生很多的更新,G1应对这种受欢迎的请求方法是改变RSet的密度,RSet的密度分为三个级别,稀少(sparse):纪录引用对象的卡片索引,细粒度(Fine):纪录引用对象的分区索引,粗粒度(Coarse):纪录引用情况,每个分区对应一个比特位。或者

        * 字粒度:每个记录精确到一个机器字(word)。该字包含有跨代指针。 
        * 对象粒度:每个记录精确到一个对象。该对象里有字段含有跨代指针。 
        * card粒度:每个记录精确到一大块内存区域。该区域内有对象含有跨代指针。   

        一个Per-Region-Table (PRT)是RSet存储颗粒度级别一个抽象。sparse PRT是一个包含Card目录的hash table。G1 GC内部维护这些card。card包含来自region的引用,这个region的引用是card到“owning region”的关联的地址。fine-grain PRT是一个开放的hash table,每一个entry代表一个指向owning region的引用的region。region里面的card目录,是一个bitmap。当达到fine-grain PRT的最大容量,coarse grain bitmap里面的相应的coarse-grained bit被设置,相应地entry从 fine grain PRT删除。coarse bitmap有一个每个region对应的bit。coarse grain map设置bit意味着关联的region包含到“owning region”的引用。        

        https://www.infoq.com/articles/tuning-tips-G1-GC

    STAB(Snaoshot-At-The-Beginning)

        STAB算法聚焦于标记清洗GC的并行标记阶段,解决了HotSpot JVM的CMS GC算法中存在重标记暂停延迟时间较长的缺陷。

        三色标记法  

        垃圾回收的并发标记阶段,gc线程和应用线程是并发执行的,所以一个对象被标记之后,应用线程可能篡改对象的引用关系,从而造成对象的漏标、误标,其实误标没什么关系,顶多造成浮动垃圾,在下次gc还是可以回收的,但是漏标的后果是致命的,把本应该存活的对象给回收了,从而影响的程序的正确性。

        为了解决在并发标记过程中,存活对象漏标的情况,GC HandBook把对象分成三种颜色:

  1. 黑色:根对象,或者该对象与它的子对象都被扫描
  2. 灰色:对象本身被扫描,但还没扫描完该对象中的子对象
  3. 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象

当GC开始扫描对象时,按照如下图步骤进行对象的扫描:

根对象被置为黑色,子对象被置为灰色。

6

继续由灰色遍历,将已扫描了子对象的对象置为黑色。

7

遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。

这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题

我们看下面一种情况,当垃圾收集器扫描到下面情况时:

9

这时候应用程序执行了以下操作:

A.c=C
B.c=null

这样,对象的状态图变成如下情形:

 

这时候垃圾收集器再标记扫描的时候就会下图成这样:

 

很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:

  1. 在插入的时候记录对象
  2. 在删除的时候记录对象

刚好这对应CMS和G1的2种不同实现方式:

在CMS采用的是增量更新(Incremental update)(post-write barrier),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在G1中,使用的是STAB(snapshot-at-the-beginning)(pre-write barrier)的方式,删除的时候记录所有的对象,它有3个步骤:

1,在开始标记的时候生成一个快照图标记存活对象

2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)

3,可能存在游离的垃圾,将在下次被收集

这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。

引用 http://blog.jobbole.com/109170/
https://www.jianshu.com/p/9e70097807ba

    SATB算法创建了一个对象图,它上堆的一个逻辑快照。STAB标记确保并发标记阶段开始时所有垃圾对象都能通过快照被鉴别出来。并发标记阶段分配的对象被认为是存活对象,但它们没有被跟踪,只能保证并发阶段开始时存活的对象都会被标记和跟踪,同时,在标记周期内所有通过并发mutator线程新分配的对象都会被标记为存活对象。所以也不会被回收。

    STAB的核心结构是两个bitmap previous和next

G1CMBitMap * _prev_mark_bitmap; /* 全局的bitmap,存储preTAMS偏移位置,也即当前标记的对象的地址(初始值是对应上次已经标记完成的地址) */
G1CMBitMap * _next_mark_bitmap; /* 全局的bitmap,存储nextTAMS偏移位置。标记过程不断移动,标记完成后会和_prev_mark_bitmap互换。 */        

    previous 位图会保存最近一次完成的标记信息。并发标记周期会创建并更新next位图。随着时间的推移,previous会过时,最终在标记结束时next会覆盖previous。

    bitmap分别存储着每个Region中,并发标记过程里的两个top-at-mark-start (TAMS)分别时previous TAMS 和 next TAMS。TAMS用于确定在标记周期内分配的对象。

  1. 假设第n轮并发标记开始,将该Region当前的Top指针赋值给nextTAMS,在并发标记标记期间,分配的对象都在[ nextTAMS, Top ]之间,SATB能够确保这部分的对象都会被标记,默认都是存活的。
  2. 当并发标记结束时,将nextTAMS所在的地址赋值给previousTAMS,SATB给[ Bottom, previousTAMS ]之间的对象创建一个快照Bitmap,所有垃圾对象能通过快照被识别出来。
  3. 第n+1轮并发标记开始,过程和第n轮一样。

1.SATBç®æ³.png

 

引用 https://blog.coderap.com/book/note/36

 

并发标记周期

    初始标记

        初始标记因为要把堆中的根节点都要标记出来所以mutator线程(java 应用线程)会被停掉(stop-the-world),因为年轻代收集为stop-the-world方式的同时会跟踪根节点,不管从便利性还是时效性的角度来看,在年轻代收集的同时进行标记都是非常合适的。也被称为“借道”(piggybacking)。在初始标记暂停过程,每个分区的NTAMS值都会被设置到分区的顶部。

    [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0012359 secs]

    根分区标记

        在分区设置完TAMS之后,应用程序就会被重新启动,然后G1 GC线程就与mutator线程并行,为了确保标记算法正确性,所有在初始标记和年轻代手机中被拷贝到survivor分区的对象,都需要被扫描并被看作是标记根,因此G1 GC扫描survivor分区,任何被Survivor分区引用的对象都将被标记,survivor也被称作根分区。

5.551: [GC concurrent-root-region-scan-start]
5.552: [GC concurrent-root-region-scan-end, 0.0001604 secs]

    并发标记

5.552: [GC concurrent-mark-start]

5.580: [GC concurrent-mark-end, 0.0282578 secs]

        并发标记阶段是并发且多线程,默认会使用-XX:ParallelGCThreads线程总数的1/4来进行并发标记,或者使用-XX:ConcGCThreads来设置。

        G1 垃圾收集器会用一个写前栅栏来执行STAB并发标记所要求的动作。如果一个应用改变了它的对象图,那么在标记开始时,可达对象和快照的一部分对象就可能被覆盖(对象漏标),直到一个标记线程发现它们并跟踪它们。因此SATB标记算法要求变更应用线程将指针变更前的值纪录在一个SATB日志队列或者缓冲区,并发标记就可以找到被覆盖的对象。

        ex:x.f=y

if(marking_is_active){
            pre_val = x.f;
            if(pre_val != null ){
                satb_enqueue(pre_val);
            }
        }

        marking_is_active当标记开始时被设为true。stab_enqueue()首先尝试将以前的值排列到一个线程局部变量缓冲区中TLAB,也就是SATB缓冲区。一个SATB缓冲区初始尺寸256条纪录,然后每个mutator都有一个SATB缓冲区。如果SATB缓冲区中没有多余的空间存放pre_val,线程的SATB缓冲区就会退休,并放入专门存放SATB缓冲区的全局列表,然后再给线程分配新的SATB缓冲区,并记录pre_val。并发标记会定期检查和处理那些被填满的缓冲区来标记那些被纪录的对象。

        在标记阶段,全局列表的SATB缓冲区会被逐个处理,并通过在bitmap中设置对应的标记位来标记每个被纪录的对象。

    重新标记

        会有stop-the-world在这个阶段G1会处理剩下的SATB日志缓冲区和所有更新,同时G1垃圾收集器还会找出所有未被访问的存活对象。因为mutator负责更新SATB缓冲区,而且本身拥有这些缓冲区。所以需要一个stop-the-world来处理数据,

        重度使用引用对象(弱,软,虚)会导致重新标记时间过长。        

[GC remark 5.581: [Finalize Marking, 0.0000953 secs] 5.581: [GC ref-proc, 0.0000619 secs] 5.582: [Unloading, 0.0017910 secs], 0.0020940 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]

    清除

       在清除阶段,bitmap交换自己的角色,PTAMS和NTAMS也会交换。清除阶段的三个主要贡献是:识别所有空闲分区;整理堆分区,为混合垃圾回收识别出高效率的老年代分区;RSet梳理。当前的启发式算法会根据活跃度和RSet的尺寸对分区定义不同等级。

5.584: [GC concurrent-cleanup-start]
5.584: [GC concurrent-cleanup-end, 0.0000046 secs]

  

    转移失败引发fullGC

[GC pause (G1 Evacuation Pause) (mixed)... (to-space exhausted), 0.1145790 secs]

    G1 GC  优化

    年轻代调优  

        -XX : MaxGCPauseMillis 暂停时间目标,默认200ms

        -XX : G1NewSizePercent 年轻代初始大小,显示为堆总尺寸百分比,默认5%

        -XX : G1MaxNewSizePercent 年轻代增长上线,显示为堆总尺寸百分比,默认60%

        想提高暂停时间可以增加CSet里年轻代Region

    并行标记周期调优

        -XX : InitialingHeapOccupancyPercent = n堆大小总数45% 并发标记周期触发条件。如果某个并发标记任务过重,导致整个周期需要很长时间完成,那么混合收集暂停将会被延迟,这可能最终导致转移失败。

        措施:

  •         必须将标识阈值设置为适合应用程序静态短暂存活数据需要的值。过高面临转移失败,过低则提前触发并发标记很有可能在混合回收收集不到空间。
  • 增加并发线程数。-XX : ConcGCThreads默认为 -XX : ParallelGCThreads的1/4,可以增加并发线程数或增加GC线程,可以有效提高并发线程
  •   

       

    MixGC调优

        -XX : G1HeapWastePercent 应用程序中可以容忍的垃圾总量大小,占总体堆的百分比,默认为5%。

        每个混合收集暂停的CSet中包含了老年代分区数量的最小阈值,它通过 -XX : G1MixedGCCountTarget ,并且默认为8,也就是每个混合收集暂停中的老年分区最小数目:

        每次混合收集暂停的最小老年代CSet尺寸 = 混合收集周期确认的老年代分区总数/G1MixedGCCountTarget

        -XX : G1MixedGCLiveThresholdPercent默认85%,它是CSet中所能容纳一个分区存活数据的最大百分比,这个选项直接控制每个分区的碎片化程度,增加意味着需要更长的时间来转移老年代分区,这意味着混合收集暂停也会持续的更长。

    引用调优    

[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]

Ref Proc是处理引用对象时所花的时间,Ref Enq是引用对象排列时所花的时间。Ref Proc一般会占用较高的时间,因为它是单线程的,一般Ref Proc超过GC暂停时间总数的10%就需要优化GC的引用处理。对于G1的重新标记,一般是处理引用占据的时间很多,因为老年代收集周期中发现的大部分引用对象,就是在并发周期的重新标记阶段处理。

-XX : ParallelRefProcEnabled 激活多线程方式的引用处理。默认是单线程,是为了减少内存占用并让CPU周期对其他应用程序可用。启用多线程会消耗更多的CPU

-XX : PrintReferenceGC 会纪录每次垃圾收集中每个引用对象类型的统计数据。

-XX : SoftRefLRUPolicyMSPerMB 默认为1000 ms 刺激软引用的回收

JVM 类加载机制

加载 连接 初始化 

连接又分 验证,准备,解析

触发加载的条件:

1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要进行初始化。代码场景:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final 修饰、已在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候。

2)对类进行反射调用的时候,

3)初始化一个类的时候,如果发现其父类没有进行过初始化,优先进行父类的初始化,

4)优先初始化main方法所在的主类

5)动态语言

这五种场景中的行为称为对一个类进行主动引用,除此之外的引用都不会触发初始化,并成为被动引用。

被动引用的例子:通过子类引用父类的静态字段不会导致子类初始化,

通过数组定义来引用类,不会导致该类初始化,使用常量不会触发定义常量的类的初始化。

加载

1)通过一个类的全限定名(com.xx.xx)来获取定义此类的二进制字节流

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3)在内存中生成一个代表这个类的java.lang.Class对象(hotspot存放在方法区/元空间内),作为方法区这个类的各种数据的访问入口

数组类本身不通过类加载器创建,它是由jvm直接创建的。但是数组类的元素类型最终要通过类加载器创建,一个数组类的创建要遵循如下规则:

1)如果数组的组件类型(数组去掉一个维度的类型)是引用类型,那就递归加载,数组C将在加载该组件类型的类加载器的类名空间上被标识(用于确定类)

2)如果组件类型不是引用类型,那么数组C标记为与引导类加载器关联

3)数组类的可见性与它的组件类型的可见性一致

连接

验证

   1、文件格式验证

    1)是否以魔数0xCAFEBABE开头

    2)主次版本是否在jvm处理范围内

    3)常量池是否有不支持的类型

。。。。。 

    基于二进制字节流验证,通过该验证后,字节流进入方法区中进行存储并对方法区存储结构进行验证

   2、元数据验证

    1)是否有父类

    2)是否继承了不允许继承的类(final)

    3)是否实现了所有的方法

    主要是进行语义验证

   3、字节码验证

    主要是对栈映射帧(StackMapTable)进行验证,验证代码块的语义

   4、符号引用验证

    1)符号引用中通过字符串描述的全县定名是否能找到对应的类

    2)在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

    3)符号引用的类、字段、方法的访问性是否可以被当前类访问

。。。

-Xverify:none 关闭类验证措施 (节省不了多少时间)

准备

    准备阶段是为类变量(被static修饰)分配空间并赋值的阶段。(实例变量将会在对象实例化时随着对象一起分配在java堆);

    假设类变量

    public static int value = 123;

    那么在准备阶段过后value的值为初始值0,因为这时候未执行任何java方法,把value赋值为123的putstatic指令存放于类构造器<clinit>中,如果字段属性表存在ConstantValue,那么在准备阶段变量value就会被初始化为Constantvalue属性所指定的值。

    public static final int value = 123;

    准备阶段value值为123;

解析

    解析阶段是虚拟机将常量池内符号引用转化为直接引用的过程,

    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符进行,分别对应CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info。。。等等

 

初始化

    类初始化时调用<clinit>实例化时调用<init>

    <clinit>是由编译器自动收集类中的所有类变量的赋值动作和静态代码块合并产生的,顺序由代码顺序决定,静态语句块中只能访问到定义在静态语句块之前的变量,定义在之后的变量,在前面的静态语句块可以赋值,不能访问。

    子类在执行<clinit>之前,父类的<clinit>一定会提前执行完毕。(子类加载之前一定会触发父类的加载)

    在多线程情况下<clinit>是线程安全的    

 

 

转载于:https://my.oschina.net/haloooooo/blog/2231574

相关文章:

  • Linux下切换用户出现su: Authentication failure的解决办法
  • [MicroPython]TPYBoard v102 CAN总线通信
  • Java多线程——AQS框架源码阅读
  • 超大数据下大批量随机键值的查询优化方案
  • node中的npm的使用
  • 未来五年中国本土机器人制造业将显著提升
  • RabbitMq之Work模式
  • (原)记一次CentOS7 磁盘空间大小异常的解决过程
  • python里使用反斜杠转义遇到问题记录
  • AliOS Things 电源管理框架使用说明
  • python三级菜单实例(傻瓜版和进阶版)
  • linux之HTTP服务
  • 【原】戏说Java
  • WPF 获取鼠标屏幕位置、窗口位置、控件位置
  • 使用ABAP正则表达式解析HTML标签
  • Android框架之Volley
  • angular组件开发
  • JS学习笔记——闭包
  • Webpack4 学习笔记 - 01:webpack的安装和简单配置
  • Xmanager 远程桌面 CentOS 7
  • 从setTimeout-setInterval看JS线程
  • 大型网站性能监测、分析与优化常见问题QA
  • 代理模式
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 欢迎参加第二届中国游戏开发者大会
  • 简单易用的leetcode开发测试工具(npm)
  • 看完九篇字体系列的文章,你还觉得我是在说字体?
  • 如何用vue打造一个移动端音乐播放器
  • 事件委托的小应用
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • 想写好前端,先练好内功
  • TPG领衔财团投资轻奢珠宝品牌APM Monaco
  • # 睡眠3秒_床上这样睡觉的人,睡眠质量多半不好
  • #define MODIFY_REG(REG, CLEARMASK, SETMASK)
  • #stm32整理(一)flash读写
  • (C++17) std算法之执行策略 execution
  • (补)B+树一些思想
  • (附源码)php新闻发布平台 毕业设计 141646
  • (七)Java对象在Hibernate持久化层的状态
  • (十三)Maven插件解析运行机制
  • (提供数据集下载)基于大语言模型LangChain与ChatGLM3-6B本地知识库调优:数据集优化、参数调整、Prompt提示词优化实战
  • (一)Linux+Windows下安装ffmpeg
  • (一)pytest自动化测试框架之生成测试报告(mac系统)
  • (转)四层和七层负载均衡的区别
  • (转)淘淘商城系列——使用Spring来管理Redis单机版和集群版
  • .net framework profiles /.net framework 配置
  • .net mvc部分视图
  • .net Stream篇(六)
  • .NET/C# 项目如何优雅地设置条件编译符号?
  • .Net7 环境安装配置
  • .Net中wcf服务生成及调用
  • @Bean注解详解
  • @EnableWebMvc介绍和使用详细demo
  • @GetMapping和@RequestMapping的区别
  • @ResponseBody