本文正在参与「金石计划 . 瓜分6万现金大奖」

前语

本系列文章首要是汇总了一下大佬们的技能文章,归于Android根底部分,作为一名合格的安卓开发工程师,咱们肯定要熟练掌握java和android,本期就来说说这些~

[如有侵权,请奉告我,我会删去]

DD一下: Android进阶开发各类文档,也可重视公众号<Android苦做舟>获取。

1.Android高档开发工程师必备根底技能
2.Android功用优化核心常识笔记
3.Android+音视频进阶开发面试题冲刺合集
4.Android 音视频开发入门到实战学习手册
5.Android Framework精编内核解析
6.Flutter实战进阶技能手册
7.近百个Android录播视频+音视频视频dome
.......

Android虚拟机类和目标的结构

1.目标内存结构

在 JVM 中,Java目标保存在堆中时,由以下三部分组成:

  • 目标头(object header):包括了关于堆目标的布局、类型、GC状况、同步状况和标识哈希码的根本信息。Java目标和vm内部目标都有一个一同的目标头格局。
  • 实例数据(Instance Data):首要是寄存类的数据信息,父类的信息,目标字段特点信息。
  • 对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。

目标头分为Mark Word(符号字)和Class Pointer(类指针),假如是数组目标还得再加一项Array Length(数组长度)。

Mark Word

用于存储目标自身的运转时数据,如哈希码(HashCode)、GC分代年纪、锁状况标志、线程持有的锁、倾向线程ID、倾向时刻戳等等。

Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。咱们翻开openjdk的源码包,对应路径/openjdk/hotspot/src/share/vm/oops,Mark Word对应到C++的代码markOop.hpp,能够从注释中看到它们的组成,本文全部代码是依据Jdk1.8。

在64位JVM中是这么存的:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

尽管它们在不同位数的JVM中长度不一样,但是根本组成内容是一致的。

  • 锁标志位(lock):差异锁状况,11时表明目标待GC收回状况, 只要最终2位锁标识(11)有用。
  • biased_lock:是否倾向锁,因为无锁和倾向锁的锁标识都是 01,没办法差异,这儿引入一位的倾向锁标识位。
  • 分代年纪(age):表明目标被GC的次数,当该次数抵达阈值的时分,目标就会搬运到老时代。
  • 目标的hashcode(hash):运转期间调用System.identityHashCode()来计算,推迟计算,并把效果赋值到这儿。当目标加锁后,计算的效果31位不行表明,在倾向锁,轻量锁,重量锁,hashcode会被搬运到Monitor中。
  • 倾向锁的线程ID(JavaThread):倾向形式的时分,当某个线程持有目标的时分,目标这儿就会被置为该线程的ID。 在后边的操作中,就无需再进行测验获取锁的动作。
  • epoch:倾向锁在CAS锁操作进程中,倾向性标识,表明目标更倾向哪个锁。
  • ptr_to_lock_record:轻量级锁状况下,指向栈中锁记载的指针。当锁获取是无竞争的时,JVM运用原子操作而不是OS互斥。这种技能称为轻量级承认。在轻量级承认的状况下,JVM经过CAS操作在目标的标题字中设置指向锁记载的指针。
  • ptr_to_heavyweight_monitor:重量级锁状况下,指向目标监视器Monitor的指针。假如两个不同的线程一同在同一个目标上竞争,则必须将轻量级承认升级到Monitor以办理等待的线程。在重量级承认的状况下,JVM在目标的ptr_to_heavyweight_monitor设置指向Monitor的指针。

Klass Pointer

即类型指针,是0目标指向它的类元数据的指针,虚拟机经过这个指针来承认这个目标是哪个类的实例。

实例数据

假如目标有特点字段,则这儿会有数据信息。假如目标无特点字段,则这儿就不会有数据。依据字段类型的不同占不同的字节,例如boolean类型占1个字节,int类型占4个字节等等;

对齐数据

目标能够有对齐数据也能够没有。默许状况下,Java虚拟机堆中目标的开端地址需求对齐至8的倍数。假如一个目标用不到8N个字节则需求对其填充,以此来补齐目标头和实例数据占用内存之后剩下的空间巨细。假如目标头和实例数据现已占满了JVM所分配的内存空间,那么就不必再进行对齐填充了。

全部的目标分配的字节总SIZE需求是8的倍数,假如前面的目标头和实例数据占用的总SIZE不满足要求,则经过对齐数据来填满。

为什么要对齐数据?字段内存对齐的其间一个原因,是让字段只呈现在同一CPU的缓存行中。假如字段不是对齐的,那么就有或许呈现跨缓存行的字段。也就是说,该字段的读取或许需求替换两个缓存行,而该

字段的存储也会一同污染两个缓存行。这两种状况对程序的履行功率而言都是不利的。其实对其填充的终究目的是为了计算机高效寻址。

至此,咱们现已了解了目标在堆内存中的全体结构布局,如下图所示

重学Android基础系列篇(五):Android虚拟机类和对象的结构

2. JVM内存结构、Java目标模型和Java内存模型差异

JVM内存结构、Java目标模型和Java内存模型,这就是三个截然不同的概念,而这三个概念很简略混淆。这儿具体差异一下

2.1 JVM内存结构(5个部分)

咱们都知道,Java代码是要运转在虚拟机上的,而虚拟机在履行Java程序的进程中会把所办理的内存区分为若干个不同的数据区域,这些区域都有各自的用途。其间有些区域跟着虚拟机进程的发动而存在,而有些区域则依靠用户线程的发动和结束而树立和销毁。

在《Java虚拟机规范(Java SE 8)》中描绘了JVM运转时内存区域结构如下:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

JVM内存结构,由Java虚拟机规范界说。描绘的是Java程序履行进程中,由JVM办理的不同数据区域。各个区域有其特定的功用。

为了进步运算功率,就对空间进行了不同区域的区分,因为每一片区域都有特定的处理数据办法和内存办理办法。

JVM 的内存区分:

栈内存:寄存的是办法中的局部变量,办法的运转必定要在栈傍边,办法中的局部变量才会在栈中创立。

成员变量在堆内存,静态变量在办法区。

办法区:存储.class相关信息。

重学Android基础系列篇(五):Android虚拟机类和对象的结构

与开发相关的时办法栈、办法区、堆内存

new出来的都放在堆内存!堆内存里边的东西都有一个地址值。办法进入办法栈中履行。

JVM堆内存分为年青代和老时代。

2.2 Java目标模型()

Java是一种面向目标的言语,而Java目标在JVM中的存储也是有必定的结构的。而这个关于Java目标自身的存储模型称之为Java目标模型。

HotSpot虚拟机中(Sun JDK和OpenJDK中所带的虚拟机,也是现在运用范围最广的Java虚拟机),规划了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是一般目标指针,而Klass用来描绘目标实例的具体类型。

每一个Java类,在被JVM加载的时分,JVM会给这个类创立一个 instanceKlass 目标,保存在办法区,用来在JVM层表明该Java类。当咱们在Java代码中,运用new创立一个目标的时分,JVM会创立一个instanceOopDesc 目标,这个目标中包括了目标头以及实例数据目标的引证在办法栈中

重学Android基础系列篇(五):Android虚拟机类和对象的结构

这就是一个简略的 Java目标的OOP-Klass模型,即Java目标模型。

2.3 java内存模型

Java内存模型就是一种符合内存模型规范的,屏蔽了各种硬件和操作体系的拜访差异的,确保了Java程序在各种平台下对内存的拜访都能确保效果一致的机制及规范。

Java内存模型是依据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是实在存在的。他仅仅一个笼统的概念。

JSR-133: Java Memory Model and Thread Specification中描绘了,JMM是和多线程相关的,他描绘了一组规矩或规范,这个规范界说了一个线程对同享变量的写入时对另一个线程是可见的。

简略总结下,Java的多线程之间是经过同享内存进行通讯的,而因为选用同享内存进行通讯,在通讯进程中会存在一系列如可见性、原子性、次序性等问题,而JMM就是围绕着多线程通讯以及与其相关的一系列特性而树立的模型。JMM界说了一些语法集,这些语法集映射到Java言语中就是volatile、synchronized等要害字。

重学Android基础系列篇(五):Android虚拟机类和对象的结构

JMM 线程操作内存的根本的规矩:

第一条关于线程与主内存:线程对同享变量的全部操作都必须在自己的作业内存(本地内存)中进行,不能直接从主内存中读写。

第二条关于线程间本地内存:不同线程之间无法直接拜访其他线程本地内存中的变量,线程间变量值的传递需求经过主内存来完结。

主内存

首要存储的是Java实例目标,全部线程创立的实例目标都寄存在主内存中,不管该实例目标是成员变量仍是办法中的本地变量(也称局部变量),当然也包括了同享的类信息、常量、静态变量。(主内存中的数据)因为是同享数据区域,多条线程对同一个变量进行拜访或许会发现线程安全问题。

本地内存

首要存储当时办法的全部本地变量信息(本地内存中存储着主内存中的变量副本拷贝),每个线程只能拜访自己的本地内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程履行的是同一段代码,它们也会各自在自己的作业内存中创立归于当时线程的本地变量,当然也包括了字节码行号指示器、相关Native办法的信息。留意因为作业内存是每个线程的私有数据,线程间无法彼此拜访作业内存,因而存储在作业内存的数据不存在线程安全问题。

3.Object堆内办理战略

3.1 目标分配进程彻底解析

在开端之前,首要介绍一下HSDB东西运用

3.1.1 HSDB东西应用

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

进入对应的JDK-Lib目录,然后输入java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB 就会呈现HSDB窗体应用程序

然后运转对应的Demo代码

public class HSDBTest {
    public HSDBTest() {
    }
    public static void main(String[] args) {
        Teacher kerwin = new Teacher();
        kerwin.setName("kerwin");
        for(int i = 0; i < 15; ++i) {
            System.gc();
        }
        Teacher jett = new Teacher();
        jett.setName("jett");
        StackTest test = new StackTest();
        test.test1(1);
        System.out.println("挂起....");
        try {
            Thread.sleep(10000000L);
        } catch (InterruptedException var5) {
            var5.printStackTrace();
        }
    }
}

敞开新的dos指令

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

当运转成功后,在对应HSDB应用上输入对应的进程号就能看到对应进程的加载状况!

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

假如说对应的HSDB一向呈现加载状况,那么就得查看翻开HSDB对应的dos指令页面上是否报错。

假如说报 UnsatisfiedLinkError反常

那么阐明:JDK目录中缺失sawindbg.dll文件

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

此时,就需求把自己其间\jre\bin目录下sawindbg.dll 粘贴到另一个\jre\bin 目录下,然后封闭HSDB,再次翻开既ok

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

在这儿挑选对应的main线程,Stack Memory 就能看到对应Stack具体信息!

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

翻开对应的Tools -heap parametes 就能看到对应的年青代,老时代对应的开端点!

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

从这两张图可知:年青代里边包括Eden区,From区和To区,对应的内存地址块都在年青代范围内!

OK!到这儿,相信你对 年青代和老时代里边具体区分有了必定的认知!!!

那么!年青代和老时代它们之间是怎样运作的呢?为什么年青代要分为Eden、From、To三个模块呢?

因而迎来了本篇要点:目标的分配进程,前面都是引子!

3.1.2 堆的核心结构解析

那么堆是什么呢?

堆概述:
  1. 一个JVM进程存在一个堆内存,堆是JVM内存办理的核心区域
  2. java 堆区在JVM发动是被创立,其空间巨细也被承认,是JVM办理的 最大一块内存(堆内存巨细能够调整)
  3. 本质上堆是一组在物理上不接连的内存空间,但是逻辑上是接连的 空间(参阅上面HSDB剖析的内存结构)
  4. 全部线程同享堆,但是堆内关于线程处理仍是做了一个线程私有的 部分(TLAB)

那么堆的目标分配、办理又是怎样的呢?

堆的目标办理
  • 在《JAVA虚拟机规范》中对Java堆的描绘是:全部的目标示例以及数 组都应当在运转时分配在堆上
  • 但是从实践运用视点来看,不是肯定,存在某些特殊状况下的目标产 生是不在堆上分配
  • 这儿请留意,规范上是肯定、实践上是相对
  • 办法结束后,堆中的目标不会立刻移除,需求经过GC履行废物收回后 才会收回
堆的内存细分

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

  • 堆区结构最外层分为:年青代和老时代,份额为 1:2
  • 年青代里边又分为:Eden区和Survivo区,份额为8:2
  • Survivo区,又分为From区和To区,份额为1:1

至于为什么要这样分配,这就和分代彼此关联了!

那么!为什么要分代(年青代和老时代)呢?

分代思想
  1. 不同目标的生命周期不一致,但是在具体运用进程中70%- 90的目标是暂时目标
  2. 分代仅有的理由是优化GC功用。假如没有分代,那么全部目标在一块空间,GC想要收回扫描他就必须扫描全部的目标,分代之后,长期持有的目标能够挑出,短期持有的目标能够固定在一个方位进行收回,省掉很 大一部分空间利用

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

  • 那些暂时目标就会放在年青代里边,当对应暂时目标,生命周期履行结束时,将会触发暂时目标的GC收回;
  • 而老时代寄存的是:生命周期长的目标,将不再由暂时目标GC收回,而是由老时代对应的GC负责收回
  • 假如这儿没有分代,那么每次收回时,将会全员检测,相当消耗资源
堆的默许巨细

默许空间巨细:

  • 初始巨细:物理内存巨细 / 64
  • 最大内存巨细:物理内存巨细 / 4

那么怎么查看本机空间巨细呢?

public class EdenSurvivorTest {
    public static void main(String[] args) {
        EdenSurvivorTest test = new EdenSurvivorTest();
        test.method1();
//        test.method2();
    }
    /**
     * 堆内存巨细示例
     * 默许空间巨细:
     *  初始巨细:物理电脑内存巨细 / 64
     *  最大内存巨细:物理电脑内存巨细 / 4
     */
    public void method1(){
        long initialMemory = Runtime.getRuntime().totalMemory();
        long maxMemory = Runtime.getRuntime().maxMemory();
        System.out.println("初始内存:"+(initialMemory / 1024 / 1024));
        System.out.println("最大内存:"+(maxMemory / 1024 / 1024));
        try {
            Thread.sleep(100000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运转效果

初始内存:245
最大内存:3621

当然也能够运用jstat指令查看

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

这儿简略的提一下这儿面的类型表明什么意思,更多jstat指令查看

  • 结束C 代表总量
  • 结束U代表已运用量
  • S0 S1代表 survivor区的From 与 To
  • E代表的是 Eden区
  • OC代表 晚年总量 OU代表晚年运用量
3.1.3 目标分配进程

到这儿才开端解说本篇的要点

留意:Java 阈值是15,Android阈值是6,这儿就拿Android举例

正常分配进程

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

全部变量的产生都在Eden区,当Eden区满了时,将会触发minorGC

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

当minorGC 触发后,不需求的变量将会被收回掉,正在运用中的变量将会移动至From区,而且对应的阈值+1

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

当下一次Eden区满了后,对应minorGC,将会带同From区、Eden区一同,符号目标

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

收回成功后,对应的From区以及Eden区,正在运用的的都会进入To区,对应阈值+1

同理,当下一次Eden满了后,对应To区和Eden区都会被对应minorGC符号,正在运用中的目标又全部移动至From区,一向来回替换!对应的阈值也会自增

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

当对应的From区或许To区存在未收回的目标的阈值满足进入老时代条件时,对应的目标将会移动至老时代!

当然在老时代里边,假如内存满了,也会触发Full GC,未被收回的目标阈值+1

为了加深形象,这儿用一段小故事来描绘整段进程!

  1. 我是一个一般的java目标,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,咱们 在Eden区中玩了挺长时刻。
  2. 有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区, 我就开端了我流浪的人生,有时分在Survivor的“From”区,有时分在Survivor的“To”区,居无定所
  3. 直到我18岁(阈值到达老时代)的时分,爸爸说我成人了,该去社会上闯闯了。于是我就去了年迈代那边,年迈代 里,人许多,而且年纪都挺大的,我在这儿也认识了许多人。在年迈代里,我生活了20年(每次 GC加一岁),然后被收回。

这就是一整段很规范的内存分配进程,那么假如存在特殊状况将会是怎样的呢?

比如说,产生的目标Eden直接装不下的那种

非正常分配进程

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

进入老时代的办法有四种办法:

  • 正常的阈值到达老时代要求
  • 在From/To区放不下时也会提升老时代(就是阈值没到达老时代,但是Eden产生的正在运用的目标过多)
  • 目标申请时,Eden区直接放不下,将会直接进入老时代判别
    • 假如Old区放的下,那就直接提升老时代
    • 假如Old区放不下,那就触发Major GC,假如放得下就提升,不然就OOM

验证目标分配进程

短生命周期分配进程

说了这么多,来验证一把哇

public class EdenSurvivorTest {
    public static void main(String[] args) {
        EdenSurvivorTest test = new EdenSurvivorTest();
        test.method2();
    }
    public void method2(){
        ArrayList list = new ArrayList();
        for (;;) {
            TestGC t = new TestGC();
//            list.add(t);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

这儿咱们大概剖析下代码,在for死循环里,目标TestGC 生命周期仅限于当时循环里,归于短生命周期目标,那么咱们来看看具体是目标是怎么分配的!

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

翻开JDK-BIN 目录,然后双击对应的exe

留意:

  • JDK11以上好像没有对应exe
  • 首次翻开该exe时,需求安装对应插件,然后封闭,再次翻开即可!

全部准备就绪后,运转上面代码,然后翻开该exe,就能看到

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

图里边该说的都说了,不过留意的是,这儿OLD区并没有任何数据!

因为在上面代码解析的时分就现已说了,产生的目标生命周期仅限于For循环里,并非长生命周期目标

那么能否举一个有长生命周期目标的例子呢?

长生命周期分配进程

public class EdenSurvivorTest {
    public static void main(String[] args) {
        EdenSurvivorTest test = new EdenSurvivorTest();
        test.method2();
    }
    public void method2(){
        ArrayList list = new ArrayList();
        for (;;) {
            TestGC t = new TestGC();
            list.add(t);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运转该代码,然后再次查看刚刚的Exe

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

因为对应变量的生命周期不再仅限于for内部,因而当阈值满足老时代要求时,将直接进入老时代

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

因为老时代里边的目标一向持有,并没有未运用的目标,当老时代满了时,就会触发OOM反常!!

在上面提到过好几个GC,那么不同的GC有什么差异呢?

MinorGc/MajorGC/FullGC的差异

JVM在进行GC时,并非每次都对上面三个内存区域一同收回,大部分的只会针关于Eden区进行 在JVM规范中,他里边的GC依照收回区域区分为两种:

  • 一种是部分收集(Partial GC ):
    • 重生代收集(Minor GC / YongGC):(只收集重生代数据)
    • 老时代收集(Major GC / Old GC):(只收集老时代数据,现在只要CMS会独自收集老时代)
    • 混合收集(Mixed GC)(收集重生代与老时代部分数据,现在只要G1运用)
  • 一种是整堆收集(Full GC):
    • 收集整个堆与办法区的全部废物
GC触发战略

年青代触发机制

  • 当年青代空间缺乏时,就会触发MinorGc,这儿年青代满值得是Eden区中满了
  • 因为Java大部分目标都是具有朝生熄灭的特性,所以MinorGC十分频频,一般收回速度也快
  • MinorGc会动身STW行为,暂停其他用户的线程

老时代GC触发机制:

  • 呈现MajorGC经常会伴随至少一次MinorGC(非肯定,老时代空间缺乏时会测验触发 MinorGC假如空间仍是缺乏则会动身MajorGC)
  • MajorGC比MinorGC速度慢10倍,假如MajorGC后内存仍是缺乏则会呈现OOM

FullGC触发

  • 调用System.gc()时
  • 老时代空间缺乏时
  • 办法区空间缺乏时
  • 经过MinorGC进入老时代的均匀巨细大于老时代的可用内存
  • 在Eden运用Survivor进行复制时,目标巨细大于Survivor的可用内存,则该目标转入老时代,且 老时代的可用内存小于该对消

Full GC 是开发或许调优中尽量要避开的

GC日志查看

重学Android基础系列篇(五):Android虚拟机类和对象的结构

如图所示

在这儿增加:-Xms9m -Xmx9m -XX:+PrintGCDetails 提交后,再次运转代码:

Connected to the target VM, address: '127.0.0.1:53687', transport: 'socket'
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->740K(9728K), 0.0032500 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2291K->504K(2560K)] 2544K->2280K(9728K), 0.0040878 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2343K->504K(2560K)] 4120K->4104K(9728K), 0.0010760 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2341K->504K(2560K)] 5942K->5912K(9728K), 0.0013867 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 5408K->5741K(7168K)] 5912K->5741K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0044415 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1859K->600K(2560K)] [ParOldGen: 5741K->6941K(7168K)] 7601K->7541K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0042249 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1836K->1800K(2560K)] [ParOldGen: 6941K->6941K(7168K)] 8778K->8742K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0018656 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 1800K->1800K(2560K)] [ParOldGen: 6941K->6925K(7168K)] 8742K->8725K(9728K), [Metaspace: 3336K->3336K(1056768K)], 0.0043790 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 2560K, used 1907K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 93% used [0x00000000ffd00000,0x00000000ffedcfd8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 6925K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 96% used [0x00000000ff600000,0x00000000ffcc3688,0x00000000ffd00000)
 Metaspace       used 3369K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 364K, capacity 392K, committed 512K, reserved 1048576K

就能查看对应的GC日志了。

3.2 目标创立进程解析

3.2.1 目标创立
  • 在开发运用时,创立 Java 目标仅仅仅仅是经过要害字new
A a = new A();
  • 但是 Java目标在虚拟机中创立则是相对复杂。今日,我将详解Java目标在虚拟机中的创立进程

限于一般目标,不包括数组和Class目标等

创立进程

当遇到要害字new指令时,Java目标创立进程便开端,整个进程如下:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

Java目标创立进程

下面我将对每个进程进行解说。

进程进程

进程1:类加载查看

  1. 查看 该new指令的参数 是否能在 常量池中 定位到一个类的符号引证
  2. 查看 该类符号引证 代表的类是否已被加载、解析和初始化过

进程2:为目标分配内存

  • 虚拟机将为目标分配内存,即把一块承认巨细的内存从 Java 堆中区分出来

目标所需内存的巨细在类加载完结后便可彻底承认

  • 关于分配内存,此处首要解说内存分配办法
  • 内存分配 依据 Java堆内存是否肯定规整 分为两种办法:指针磕碰 & 闲暇列表
  1. Java堆内存 规整:已运用的内存在一边,未运用内存在另一边
  2. Java堆内存 不规整:已运用的内存和未运用内存彼此交织

重学Android基础系列篇(五):Android虚拟机类和对象的结构

办法1:指针磕碰

  • 假设Java堆内存肯定规整,内存分配将选用指针磕碰
  • 分配办法:已运用内存在一边,未运用内存在另一边,中间放一个作为分界点的指示器

重学Android基础系列篇(五):Android虚拟机类和对象的结构

  • 那么,分配目标内存 = 把指针向 未运用内存 移动一段 与目标巨细持平的距离

重学Android基础系列篇(五):Android虚拟机类和对象的结构

办法2:闲暇列表

  • 假设Java堆内存不规整,内存分配将选用 闲暇列表
  • 分配办法:虚拟机维护着一个 记载可用内存块 的列表,在分配时从列表中找到一块足够大的空间区分给目标实例,并更新列表上的记载

额外常识

  • 分配办法的挑选 取决于 Java堆内存是否规整;
  • 而java堆是否规整 由所选用的废物收集器是否带有紧缩收拾功用决定。因而:
    1. 运用带 Compact 进程的废物收集器时,选用指针磕碰;

Serial、ParNew废物收集器

  1. 运用依据 Mark_sweep算法的废物收集器时,选用闲暇列表。

CMS废物收集器

特别留意

  • 目标创立在虚拟机中是十分频频的操作,即便仅仅修正一个指针所指向的方位,在并发状况下也会引起线程不安全

如,正在给目标A分配内存,指针还没有来得及修正,目标B又一同运用了原来的指针来分配内存

所以,给目标分配内存会存在线程不安全的问题。

解决 线程不安全 有两种计划:

  1. 同步处理分配内存空间的行为

虚拟机选用 CAS + 失利重试的办法 确保更新操作的原子性

  1. 把内存分配行为 依照线程 区分在不同的内存空间进行
  1. 即每个线程在 Java堆中预先分配一小块内存(本地线程分配缓冲(Thread Local Allocation BufferTLAB)),哪个线程要分配内存,就在哪个线程的TLAB上分配,只要TLAB用完并分配新的TLAB时才需求同步锁。
  2. 虚拟机是否运用TLAB,能够经过-XX:+/-UseTLAB参数来设定。

进程3: 将内存空间初始化为零值

内存分配完结后,虚拟机需求将分配到的内存空间初始化为零(不包括目标头)

  1. 确保了目标的实例字段在运用时可不赋初始值就直接运用(对应值 = 0)
  2. 如运用本地线程分配缓冲(TLAB),这一作业进程也能够提早至TLAB分配时进行。

进程4: 对目标进行必要的设置

如,设置 这个目标是哪个类的实例、怎么才干找到类的元数据信息、目标的哈希码、目标的GC分代年纪等信息。

这些信息寄存在目标的目标头中


  • 至此,从 Java 虚拟机的视点来看,一个新的 Java目标创立结束
  • 但从 Java 程序开发来说,目标创立才刚开端,需求进行一些初始化操作。

总结

下面用一张图总结 Java目标创立的进程

重学Android基础系列篇(五):Android虚拟机类和对象的结构


3.3 目标的内存布局

  • 问题:在 Java 目标创立后,到底是怎么被存储在Java内存里的呢?
  • 答:在Java虚拟机(HotSpot)中,目标在 Java内存中的 存储布局 可分为三块:
    1. 目标头 存储区域
    2. 实例数据 存储区域
    3. 对齐填充 存储区域

重学Android基础系列篇(五):Android虚拟机类和对象的结构

下面我会具体阐明每一块区域。

3.2.1 目标头 区域

此处存储的信息包括两部分:

  • 目标自身的运转时数据(Mark Word
  1. 如哈希码(HashCode)、GC分代年纪、锁状况标志、线程持有的锁、倾向线程ID、倾向时刻戳等
  2. 该部分数据被规划成1个 非固定的数据结构 以便在极小的空间存储尽量多的信息(会依据目标状况复用存储空间)
  • 目标类型指针
  1. 即目标指向它的类元数据的指针
  2. 虚拟机经过这个指针来承认这个目标是哪个类的实例

特别留意

假如目标 是 数组,那么在目标头中还必须有一块用于记载数组长度的数据

因为虚拟机能够经过一般Java目标的元数据信息承认目标的巨细,但是从数组的元数据中却无法承认数组的巨细。


3.2.2 实例数据 区域
  • 存储的信息:目标真实有用的信息

即代码中界说的字段内容

  • 注:这部分数据的存储次序会遭到虚拟机分配参数(FieldAllocationStyle)和字段在Java源码中界说次序的影响。
// HotSpot虚拟机默许的分配战略如下:
longs/doubles、ints、shorts/chars、bytes/booleans、oop(Ordinary Object Pointers)
// 从分配战略中能够看出,相同宽度的字段总是被分配到一同
// 在满足这个条件的条件下,父类中界说的变量会呈现在子类之前
CompactFields = true// 假如 CompactFields 参数值为true,那么子类之中较窄的变量也或许会插入到父类变量的空隙之中。
3.2.3 对齐填充 区域
  • 存储的信息:占位符

占位效果

  • 因为目标的巨细必须是8字节的整数倍
  • 而因HotSpot VM的要求目标开端地址必须是8字节的整数倍,且目标头部分正好是8字节的倍数。
  • 因而,当目标实例数据部分没有对齐时(即目标的巨细不是8字节的整数倍),就需求经过对齐填充来补全。
3.2.4 总结

重学Android基础系列篇(五):Android虚拟机类和对象的结构


3.4 目标的拜访定位

  • 问:树立目标后,该怎么拜访目标呢?

实践上需拜访的是 目标类型数据 & 目标实例数据

  • 答:Java程序 经过 栈上的引证类型数据(reference) 来拜访Java堆上的目标

因为引证类型数据(reference)在 Java虚拟机中只规则了一个指向目标的引证,但没界说该引证应该经过何种办法去定位、拜访堆中的目标的具体方位

所以目标拜访办法取决于虚拟机完成。现在主流的目标拜访办法有两种:

  • 句柄 拜访
  • 直接指针 拜访

具体请看如下介绍:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

4.逃逸剖析

JIT 即时编译还有一个最前沿的优化技能:逃逸剖析(Escape Analysis) 。废话少说,咱们直接步入正题吧。

4.1 逃逸剖析

首要咱们需求知道,逃逸剖析并不是直接的优化手法,而是经过动态剖析目标的效果域,为其它优化手法供给依据的剖析技能。具体而言就是:

逃逸剖析是“一种承认指针动态范围的静态剖析,它能够剖析在程序的哪些地方能够拜访到指针”。Java虚拟机的即时编译器会对新建的目标进行逃逸剖析,判别目标是否逃逸出线程或许办法。即时编译器判别目标是否逃逸的依据有两种:

  1. 目标是否被存入堆中(静态字段或许堆中目标的实例字段),一旦目标被存入堆中,其他线程便能获得该目标的引证,即时编译器就无法追踪全部运用该目标的代码方位。

    简略来说就是,如类变量或实例变量,或许被其它线程拜访到,这就叫做线程逃逸,存在线程安全问题。

  2. 目标是否被传入不知道代码中,即时编译器会将未被内联的代码当成不知道代码,因为它无法承认该办法调用会不会将调用者或所传入的参数存储至堆中,这种状况,能够直接以为办法调用的调用者以及参数是逃逸的。(不知道代码指的是没有被内联的办法调用)

    比如说,当一个目标在办法中界说之后,它或许被外部办法所引证,作为参数传递到其它办法中,这叫做办法逃逸,

​ 办法逃逸咱们能够用个事例来演示一下:

//StringBuffer目标产生了办法逃逸
public static StringBuffer createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
  }
  public static String createString(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
  }

关于逃逸剖析技能,本人想过用代码展现目标是否产生了逃逸,比如说上述代码,依据理论常识能够以为 createStringBuffer 办法中产生了逃逸,但是具体是个什么状况,咱们都不清楚。尽管 JVM 有个参数 PrintEscapeAnalysis 能够显示剖析效果,但是该参数仅限于 debug 版别的 JDK 才干够进行调试,屡次测验后,未能编译出 debug 版别的 JDK,暂且没什么思路,所以查看逃逸剖析效果这件事先往后放一放,后续学习 JVM 调优再进一步来学习。

4.2 依据逃逸剖析的优化

即时编译器能够依据逃逸剖析的效果进行比如同步消除、栈上分配以及标量替换的优化。

同步消除(锁消除)

线程同步自身比较消耗资源,JIT 编译器能够借助逃逸剖析来判别,假如承认一个目标不会逃逸出线程,无法被其它线程拜访到,那该目标的读写就不会存在竞争,则能够消除对该目标的同步锁,经过-XX:+EliminateLocks(默许敞开)能够敞开同步消除。 这个撤销同步的进程就叫同步消除,也叫锁消除。

咱们仍是经过事例来阐明这一状况,来看看何种状况需求线程同步。

首要构建一个 Worker 目标

@Getter
public class Worker {
  private String name;
  private double money;
  public Worker() {
  }
  public Worker(String name) {
    this.name = name;
  }
  public void makeMoney() {
    money++;
  }
}

测试代码如下:

public class SynchronizedTest {
  public static void work(Worker worker) {
    worker.makeMoney();
  }
  public static void main(String[] args) throws InterruptedException {
    long start = System.currentTimeMillis();
    Worker worker = new Worker("hresh");
    new Thread(() -> {
      for (int i = 0; i < 20000; i++) {
        work(worker);
      }
    }, "A").start();
    new Thread(() -> {
      for (int i = 0; i < 20000; i++) {
        work(worker);
      }
    }, "B").start();
    long end = System.currentTimeMillis();
    System.out.println(end - start);
    Thread.sleep(100);
    System.out.println(worker.getName() + "一共赚了" + worker.getMoney());
  }
}

履行效果如下:

52
hresh一共赚了28224.0

能够看出,上述两个线程一同修正同一个 Worker 目标的 money 数据,关于 money 字段的读写产生了竞争,导致最终效果不正确。像上述这种状况,即时编译器经过逃逸剖析后确定目标产生了逃逸,那么肯定不能进行同步消除优化。

换个目标不产生逃逸的状况试一下。

//JVM参数:-Xms60M -Xmx60M  -XX:+PrintGCDetails -XX:+PrintGCDateStamps
public class SynchronizedTest {
  public static void lockTest() {
    Worker worker = new Worker();
    synchronized (worker) {
      worker.makeMoney();
    }
  }
  public static void main(String[] args) throws InterruptedException {
    long start = System.currentTimeMillis();
    new Thread(() -> {
      for (int i = 0; i < 500000; i++) {
        lockTest();
      }
    }, "A").start();
    new Thread(() -> {
      for (int i = 0; i < 500000; i++) {
        lockTest();
      }
    }, "B").start();
    long end = System.currentTimeMillis();
    System.out.println(end - start);
  }
}

输出效果如下:

56
Heap
 PSYoungGen      total 17920K, used 9554K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 15360K, 62% used [0x00000007bec00000,0x00000007bf5548a8,0x00000007bfb00000)
  from space 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000)
  to   space 2560K, 0% used [0x00000007bfb00000,0x00000007bfb00000,0x00000007bfd80000)
 ParOldGen       total 40960K, used 0K [0x00000007bc400000, 0x00000007bec00000, 0x00000007bec00000)
  object space 40960K, 0% used [0x00000007bc400000,0x00000007bc400000,0x00000007bec00000)
 Metaspace       used 4157K, capacity 4720K, committed 4992K, reserved 1056768K
  class space    used 467K, capacity 534K, committed 640K, reserved 1048576K

在 lockTest 办法中针对新建的 Worker 目标加锁,并没有实践意义,经过逃逸剖析后确定目标未逃逸,则会进行同步消除优化。JDK8 默许敞开逃逸剖析,咱们测验封闭它,再看看输出效果。

-Xms60M -Xmx60M  -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+PrintGCDateStamps

输出效果变为:

73
2022-03-01T14:51:08.825-0800: [GC (Allocation Failure) [PSYoungGen: 15360K->1439K(17920K)] 15360K->1447K(58880K), 0.0018940 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 17920K, used 16340K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 15360K, 97% used [0x00000007bec00000,0x00000007bfa8d210,0x00000007bfb00000)
  from space 2560K, 56% used [0x00000007bfb00000,0x00000007bfc67f00,0x00000007bfd80000)
  to   space 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000)
 ParOldGen       total 40960K, used 8K [0x00000007bc400000, 0x00000007bec00000, 0x00000007bec00000)
  object space 40960K, 0% used [0x00000007bc400000,0x00000007bc402000,0x00000007bec00000)
 Metaspace       used 4153K, capacity 4688K, committed 4864K, reserved 1056768K
  class space    used 466K, capacity 502K, committed 512K, reserved 1048576K

经过比照发现,封闭逃逸剖析后,履行时刻变长,且内存占用变大,一同产生了废物收回。

不过,依据逃逸剖析的锁消除实践上并不多见。一般来说,开发人员不会直接对办法中新构造的目标进行加锁,如上述事例所示,lockTest 办法中的加锁操作没什么意义。

事实上,逃逸剖析的效果更多被用于将新建目标操作转换成栈上分配或许标量替换。

标量替换

在解说 Java 目标的内存布局时提到过,Java 虚拟机中目标都是在堆上分配的,而堆上的内容对任何线程大都是可见的(除开 TLAB)。与此一同,Java 虚拟机需求对所分配的堆内存进行办理,而且在目标不再被引证时收回其所占有的内存。

假如逃逸剖析能够证明某些新建的目标不逃逸,那么 Java 虚拟机彻底能够将其分配至栈上,而且在 new 语句地点的办法退出时,经过弹出当时办法的栈桢来主动收回所分配的内存空间。这样一来,咱们便无须借助废物收回器来处理不再被引证的目标。

但是现在 Hotspot 并没有完成真实意义上的栈上分配,而是运用了标量替换这么一项技能。

所谓的标量,就是仅能存储一个值的变量,比如 Java 代码中的局部变量。与之相反,聚合量则或许一同存储多个值,其间一个典型的例子就是 Java 目标。

若一个数据现已无法再分化成更小的数据来表明了,Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分化了,那么这些数据就能够被称为标量。相对的,假如一个数据能够继续分化, 那它就被称为聚合量(Aggregate),Java 中的目标就是典型的聚合量。

标量替换这项优化技能,能够当作将本来对目标的字段的拜访,替换为一个个局部变量的拜访。

如下述事例所示:

public class ScalarTest {
  public static double getMoney() {
    Worker worker = new Worker();
    worker.setMoney(100.0);
    return worker.getMoney() + 20;
  }
  public static void main(String[] args) {
    getMoney();
  }
}

经过逃逸剖析,Worker 目标未逃逸出 getMoney()的调用,因而能够对聚合量 worker 进行分化,得到局部变量 money,进行标量替换后的伪代码:

public class ScalarTest {
  public static double getMoney() {
    double money = 100.0;
    return money + 20;
  }
  public static void main(String[] args) {
    getMoney();
  }
}

目标拆分后,目标的成员变量改为办法的局部变量,这些字段既能够存储在栈上,也能够直接存储在寄存器中。标量替换因为不必创立目标,减轻了废物收回的压力。

别的,能够手动经过-XX:+EliminateAllocations能够敞开标量替换(默许是敞开的), -XX:+PrintEliminateAllocations(同样需求debug版别的JDK)查看标量替换状况。

栈上分配

故名思议就是在栈上分配目标,其完成在 Hotspot 并没有完成真实意义上的栈上分配,实践上是标量替换。

在一般状况下,目标和数组元素的内存分配是在堆内存上进行的。但是跟着 JIT 编译器的日渐成熟,许多优化使这种分配战略并不肯定。JIT编译器就能够在编译期间依据逃逸剖析的效果,来决定是否需求创立目标,是否能够将堆内存分配转换为栈内存分配。

4.3 部分逃逸剖析

C2 的逃逸剖析与控制流无关,相对来说比较简略。Graal 则引入了一个与控制流有关的逃逸剖析,名为部分逃逸剖析(partial escape analysis)。它解决了所新建的实例仅在部分程序路径中逃逸的状况。

如下代码所示:

public static void bar(boolean cond) {
  Object foo = new Object();
  if (cond) {
    foo.hashCode();
  }
}
// 能够手艺优化为:
public static void bar(boolean cond) {
  if (cond) {
    Object foo = new Object();
    foo.hashCode();
  }
}

假设 if 语句的条件建立的或许性只要 1%,那么在 99% 的状况下,程序没有必要新建目标。其手艺优化的版别正是部分逃逸剖析想要主动到达的效果。

部分逃逸剖析将依据控制流信息,判别出新建目标仅在部分分支中逃逸,而且将目标的新建操作推延至目标逃逸的分支中。这将使得本来因目标逃逸而无法避免的新建目标操作,不再呈现在只履行 if-else 分支的程序路径之中。

咱们经过一个完好的测试事例来直接验证这一优化。

public class PartialEscapeTest {
  long placeHolder0;
  long placeHolder1;
  long placeHolder2;
  long placeHolder3;
  long placeHolder4;
  long placeHolder5;
  long placeHolder6;
  long placeHolder7;
  long placeHolder8;
  long placeHolder9;
  long placeHoldera;
  long placeHolderb;
  long placeHolderc;
  long placeHolderd;
  long placeHoldere;
  long placeHolderf;
  public static void foo(boolean flag) {
    PartialEscapeTest o = new PartialEscapeTest();
    if (flag) {
      o.hashCode();
    }
  }
  public static void main(String[] args) {
    for (int i = 0; i < 1000000; i++) {
      foo(false);
    }
  }
}

本次测试选用的是 JDK11,敞开 Graal 编译器需求装备如下参数:

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

别离输出运用 C2 编译器或 Graal 编译器的 GC 日志,对应指令为:

java -Xlog:gc* PartialEscapeTest
java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -Xlog:gc* PartialEscapeTest

经过比照 GC 日志能够发现内存占用状况不一致,Graal 编译器下内存占用更小一点。

C2

[0.012s][info][gc,heap] Heap region size: 1M
[0.017s][info][gc     ] Using G1
[0.017s][info][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3
[0.345s][info][gc,heap,exit ] Heap
[0.345s][info][gc,heap,exit ]  garbage-first heap   total 262144K, used 21504K [0x0000000700000000, 0x0000000800000000)[0.345s][info][gc,heap,exit ]   region size 1024K, 18 young (18432K), 0 survivors (0K)
[0.345s][info][gc,heap,exit ]  Metaspace       used 6391K, capacity 6449K, committed 6784K, reserved 1056768K
[0.345s][info][gc,heap,exit ]   class space    used 552K, capacity 571K, committed 640K, reserved 1048576K

Graal

[0.019s][info][gc,heap] Heap region size: 1M
[0.025s][info][gc     ] Using G1
[0.025s][info][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3
[0.611s][info][gc,start     ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
[0.612s][info][gc,task      ] GC(0) Using 6 workers of 10 for evacuation
[0.615s][info][gc,phases    ] GC(0)   Pre Evacuate Collection Set: 0.0ms
[0.615s][info][gc,phases    ] GC(0)   Evacuate Collection Set: 3.1ms
[0.615s][info][gc,phases    ] GC(0)   Post Evacuate Collection Set: 0.2ms
[0.615s][info][gc,phases    ] GC(0)   Other: 0.6ms
[0.615s][info][gc,heap      ] GC(0) Eden regions: 24->0(150)
[0.615s][info][gc,heap      ] GC(0) Survivor regions: 0->3(3)
[0.615s][info][gc,heap      ] GC(0) Old regions: 0->4
[0.615s][info][gc,heap      ] GC(0) Humongous regions: 5->5
[0.615s][info][gc,metaspace ] GC(0) Metaspace: 8327K->8327K(1056768K)
[0.615s][info][gc           ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 29M->11M(256M) 3.941ms
[0.615s][info][gc,cpu       ] GC(0) User=0.01s Sys=0.01s Real=0.00s
Cannot use JVMCI compiler: No JVMCI compiler found
[0.616s][info][gc,heap,exit ] Heap
[0.616s][info][gc,heap,exit ]  garbage-first heap   total 262144K, used 17234K [0x0000000700000000, 0x0000000800000000)
[0.616s][info][gc,heap,exit ]   region size 1024K, 9 young (9216K), 3 survivors (3072K)
[0.616s][info][gc,heap,exit ]  Metaspace       used 8336K, capacity 8498K, committed 8832K, reserved 1056768K
[0.616s][info][gc,heap,exit ]   class space    used 768K, capacity 802K, committed 896K, reserved 1048576K

查看 Graal 在 JDK11 上的编译效果,能够履行下述指令:

java -XX:+PrintCompilation -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -cp /Users/xxx/IdeaProjects/java_deep_learning/src/main/java/com/msdn/java/javac/escape ScalarTest > out-jvmci.txt

5.Minor GC、Major GC和Full GC比照与GC日志剖析

5.1 Minor GC、Major GC和Full GC比照
GC类型 GC区域 触发条件 Stop The World时刻
Minor GC Eden 和 Survivor 区域 Eden区域 > 设定内存阈值 关于大部分应用程序,**Minor GC中止导致的推迟都是能够忽略不计的。**大部分 Eden 区中的目标都能被以为是废物,永远也不会被复制到 Survivor 区或许老时代空间。假如Eden 区大部分重生目标不符合 GC 条件,Minor GC 履行时暂停的时刻将会长许多。
Major GC Old区域 依据不同的GC装备由Minor GC触发 MajorGC 的速度一般会比 Minor GC 慢 10倍以上。
Full GC 整个Heap空间包括年青代和永久代 1. 调用System.gc时 Old老时代空间缺乏;办法区空间缺乏;经过Minor GC后进入老时代的均匀巨细大于老时代的可用内存 。
5.2 GC日志剖析
5.2.1 GC 日志能帮咱们做什么

GC 日志是由 JVM 产生的对废物收回活动进行描绘的日志文件。

经过 GC 日志,咱们能够直观的看到内存收回的状况及进程,是能够快速判别内存是否存在故障的重要依据。

5.2.2 怎么生成 GC 日志

在 JAVA 指令中增加 GC 相关的参数,以生成 GC日志:

JVM 参数 参数阐明 补白
-XX:+PrintGC 打印 GC 日志
-XX:+PrintGCDetails 打印具体的 GC 日志 装备此参数时,-XX:+PrintGC 可省掉
-XX:+PrintGCTimeStamps 以基准办法记载时刻(即发动后多少秒,如:21.148:) 默许的时刻记载办法,可省掉
-XX:+PrintGCDateStamps 以日期办法记载时刻(如:2022-05-27T18:01:37.545+0800: 30.122:) 当以日期办法记载时刻时,日期后其实还带有基准办法的时刻
-XX:+PrintHeapAtGC 打印堆的具体信息
-Xloggc:gc.log 装备 GC 日志文件的路径

常用的选项组合:

java -Xms512m -Xmx2g -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar xxx.jar
5.3 读懂 GC 日志

前文咱们介绍了经过 -XX:+UseSerialGC-XX:+UseParallelGC (同 -XX:+UseParallelOldGC )、 -XX:+UseParNewGC-XX:+UseConcMarkSweepGC-XX:+UseG1GC 这些参数,来指定运用不同的废物收回器组合。不同的废物收回器所生成的 GC 日志也是有差异的,尤其是 CMS 、 G1 所生成的日志,会比 Serial 、Parallel 所生成的日志复杂许多。

这儿咱们以 JDK1.8 默许运用的 -XX:+UseParallelGC (即 Parallel Scavenge + Parallel Old )为例,解说其 GC 日志。

最初部分的环境信息

重学Android基础系列篇(五):Android虚拟机类和对象的结构

上图是 GC 日志的最初部分:

  1. 第 1 部分是 Java 环境信息:
  • Java HotSpot(TM) 64-Bit Server VM (25.144-b01) 是 JVM 版别信息;
  • linux-amd64 JRE (1.8.0_144-b01) 是 JDK 版别信息;
  1. 第 2 部分是服务器内存信息:
  • physical 32611948k(7181112k free) 是服务器物理内存总巨细与闲暇巨细;
  • swap 67108860k(66625832k free) 是服务器 swap 交流区的总巨细与闲暇巨细;
  1. 第 3 部分打印出与 GC 相关的 JVM 发动参数,其间:
  • -XX:InitialHeapSize=1073741824 -XX:MaxHeapSize=2147483648 指定了堆巨细的初始化值与最大值;
  • -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 为 GC 日志的相关设置;
  • -XX:+UseCompressedClassPointers -XX:+UseCompressedOops 敞开了一般目标和类的指针紧缩,能够进步内存功用
  • -XX:+UseParallelGC 则体现出 JDK1.8 默许运用的废物收回器。

Young GC

重学Android基础系列篇(五):Android虚拟机类和对象的结构

上图描绘的是 Young GC 活动:

  1. 第 1 部分是日志时刻:
  • 2022-06-01T11:10:59.126+0800: 是日期格局的时刻;
  • 86.281: 是基准格局的时刻,也就是应用发动后的 N 秒;
  1. 第 2 部分是 GC 的类型与产生 GC 的原因:
  • GC 表明当时 GC 类型为 Young GC ;
  • Allocation Failure 是产生 GC 的原因,这儿是年青代中没有足够的空间来存储新数据了;
  1. 第 3 部分是 GC 活动的概况:
  • [PSYoungGen: 639473K->35837K(649728K)] ,从左到右别离是重生代废物收回前的巨细、重生代废物收回后的巨细、重生代总巨细;
  • 686696K->87816K(1671680K) ,中括号外的这三个数字,从左到右别离是堆废物收回前的巨细、堆废物收回后的巨细、堆总巨细;
  • 0.0192314 secs 是本次重生代废物收回的耗时;
  1. 第 4 部分是 GC 耗时的概况:
  • user=0.10 是用户耗时(即应用耗时);
  • sys=0.00 是体系内核耗时;
  • real=0.01 是实践耗时,因为多核的原因,实践耗时或许会小于用户耗时 + 体系耗时。

Full GC

重学Android基础系列篇(五):Android虚拟机类和对象的结构

上图描绘的是 Full GC 活动:

  1. 第 1 部分是日志时刻,与 Minor GC 日志相同,不再赘述;
  2. 第 2 部分是 GC 的类型与产生 GC 的原因:
  • Full GC 表明当时 GC 类型为 Full GC ;
  • Metadata GC Threshold 是产生 GC 的原因,这儿是元空间的占用巨细到达了 GC 阈值;
  1. 第 3 部分是 GC 活动的概况:
  • [PSYoungGen: 33776K->0K(647680K)] ,从左到右别离是重生代废物收回前的巨细、重生代废物收回后的巨细、重生代总巨细;
  • [ParOldGen: 51986K->59081K(1324544K)] ,从左到右别离是老时代废物收回前的巨细、老时代废物收回后的巨细、老时代总巨细;
  • 85762K->59081K(1972224K) ,中括号外的这三个数字,从左到右别离是堆废物收回前的巨细、堆废物收回后的巨细、堆总巨细;
  • [Metaspace: 92860K->92312K(1134592K)] ,从左到右别离是元空间废物收回前的巨细、元空间废物收回后的巨细、元空间总巨细;
  • 0.1185325 secs 是本次重生代废物收回的耗时;
  1. 第 4 部分是 GC 耗时的概况,与 Minor GC 日志相同,不再赘述。

以上就是 JDK1.8 下 -XX:+UseParallelGC (即 Parallel Scavenge + Parallel Old )形式下, GC 日志的具体解读。不同的形式下的日志,关于重生代、老时代的别称也是不同的,咱们将上一篇文章中的一张特征信息总结表格拿过来,再次加深一下形象:

JVM 参数 日志中对重生代的别称 日志中对老时代的别称
-XX:+UseSerialGC DefNew Tenured
-XX:+UseParallelGC PSYoungGen ParOldGen
-XX:+UseParallelOldGC PSYoungGen ParOldGen
-XX:+UseParNewGC ParNew Tenured
-XX:+UseConcMarkSweepGC ParNew CMS
-XX:+UseG1GC 没有专门的重生代描绘,有 Eden 和 Survivors 没有专门的老时代描绘,有 Heap
5.4 GC 日志的图形化剖析东西

接下来咱们需求配合一些图形化的东西来剖析 GC 日志。

5.4.1 GCeasy

GCeasy 是笔者最为引荐的 GC 日志剖析东西,它是一个在线东西,声称业界第一个以机器学习算法为根底的 GC 日志剖析东西。它不仅能够生成多元化的剖析图例(这是免费的),还能够引荐 JVM 装备、提出存在的内存问题并引荐对应的解决计划(后边这些功用是付费的)。

咱们来看一下免费的功用:

  • 内存计算,包括重生代、老时代、耐久代的最大分配值与峰值:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

  • 要害功用指标计算,包括吞吐量、应用暂停时刻(含均匀暂停时刻、最大暂停时刻、按暂停时刻范围计算 GC 次数):

重学Android基础系列篇(五):Android虚拟机类和对象的结构

  • 多元化的图例,包括 GC 前后的堆的巨细、 GC 散布、收回空间状况、重生代/老时代/耐久代的内存状况、重生代向耐久代转化状况:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

  • GC 具体数据计算:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

5.4.2 GCViewer

GCViewer 是一个离线的 GC 可视化剖析东西,在同类离线东西中,能够说是功用最为强大的了。

  • GCViewer 供给了一个交互式的图例区,能够依据个人需求来展现所挑选的指标:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

  • GCViewer 关于数据的计算也是相当之完善的:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

5.4.3 GChisto

GChisto 也是一个离线东西,功用相较于 GCViewer 显得比较简略,下载地址:github.com/jewes/gchis…

  • GC 数据计算,包括各类 GC 的次数、耗时、开销占比等:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

  • 供给 GC 不一同刻范围内次数计算、耗时计算等图例:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

重学Android基础系列篇(五):Android虚拟机类和对象的结构

5.4.4 GCLogViewer

GCLogViewer 也是一个离线东西,官方地址国内无法翻开(code.google.com/p/gclogview…),想要下载的话能够到 CSDN 上找一找。

其功用与 GChisto 比较类似,效果图如下:

重学Android基础系列篇(五):Android虚拟机类和对象的结构

重学Android基础系列篇(五):Android虚拟机类和对象的结构