android专属JVM讲解—【对象分配过程完全解析】

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

前言

在上一篇中,讲解了JVM对应的运行时数据区详解,继续上一篇的话题,本篇讲解JVM对应的对象分配过程完全解析

在开始之前,首先介绍一下HSDB工具使用

1、HSDB工具应用

1.png

如图所示

进入对应的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命令

2.png

如图所示

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

3.png

如图所示

如果说对应的HSDB一直出现加载情况,那么就得查看打开HSDB对应的dos命令页面上是否报错。

如果说报 UnsatisfiedLinkError异常

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

4.png

如图所示

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

5.png

如图所示

在这里选择对应的main线程,Stack Memory 就能看到对应Stack详细信息!

6.png

如图所示

打开对应的Tools -heap parametes 就能看到对应的年轻代,老年代对应的起始点!

7.png

如图所示

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

OK!到这里,相信你对 年轻代和老年代里面具体划分有了一定的认知!!!

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

因此迎来了本篇重点:对象的分配过程,前面都是引子!

2、堆的核心结构解析

那么堆是什么呢?

2.1 堆概述:

  1. 一个JVM进程存在一个堆内存,堆是JVM内存管理的核心区域
  2. java 堆区在JVM启动是被创建,其空间大小也被确定,是JVM管理的 最大一块内存(堆内存大小可以调整)
  3. 本质上堆是一组在物理上不连续的内存空间,但是逻辑上是连续的 空间(参考上面HSDB分析的内存结构)
  4. 所有线程共享堆,但是堆内对于线程处理还是做了一个线程私有的 部分(TLAB)

那么堆的对象分配、管理又是怎么的呢?

2.2 堆的对象管理

  • 在《JAVA虚拟机规范》中对Java堆的描述是:所有的对象示例以及数 组都应当在运行时分配在堆上
  • 但是从实际使用角度来看,不是绝对,存在某些特殊情况下的对象产 生是不在堆上分配
  • 这里请注意,规范上是绝对、实际上是相对
  • 方法结束后,堆中的对象不会马上移除,需要通过GC执行垃圾回收后 才会回收

2.3 堆的内存细分

9.png

如图所示

  • 堆区结构最外层分为:年轻代和老年代,比例为 1:2
  • 年轻代里面又分为:Eden区和Survivo区,比例为8:2
  • Survivo区,又分为From区和To区,比例为1:1

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

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

2.4 分代思想

  1. 不同对象的生命周期不一致,但是在具体使用过程中70%- 90的对象是临时对象
  2. 分代唯一的理由是优化GC性能。如果没有分代,那么所有对象在一块空间,GC想要回收扫描他就必须扫描所有的对象,分代之后,长期持有的对象可以挑出,短期持有的对象可以固定在一个位置进行回收,省掉很 大一部分空间利用

10.png

如图所示

  • 那些临时对象就会放在年轻代里面,当对应临时对象,生命周期执行完毕时,将会触发临时对象的GC回收;
  • 而老年代存放的是:生命周期长的对象,将不再由临时对象GC回收,而是由老年代对应的GC负责回收
  • 如果这里没有分代,那么每次回收时,将会全员检测,相当耗费资源

2.5 堆的默认大小

默认空间大小:

  • 初始大小:物理内存大小 / 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命令查看

11.png

如图所示

这里简单的提一下这里面的类型表示什么意思,[更多jstat命令查看](blog.csdn.net/u010399248/…l)

  • 结尾C 代表总量
  • 结尾U代表已使用量
  • S0 S1代表 survivor区的From 与 To
  • E代表的是 Eden区
  • OC代表 老年总量 OU代表老年使用量

3、对象分配过程

到这里才开始讲解本篇的重点

注意:Java 阈值是15,Android阈值是6,这里就拿Android举例

3.1 正常分配过程

12.png

如图所示

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

13.png

如图所示

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

14.png

如图所示

当下一次Eden区满了后,对应minorGC,将会带同From区、Eden区一起,标记对象

15.png

如图所示

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

同理,当下一次Eden满了后,对应To区和Eden区都会被对应minorGC标记,正在使用中的对象又全部移动至From区,一直来回交替!对应的阈值也会自增

16.png

如图所示

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

当然在老年代里面,如果内存满了,也会触发Full GC,未被回收的对象阈值+1

为了加深印象,这里用一段小故事来描述整段过程!

  1. 我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们 在Eden区中玩了挺长时间。

  2. 有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区, 我就开始了我漂泊的人生,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所

  3. 直到我18岁(阈值达到老年代)的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代 里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次 GC加一岁),然后被回收。

这就是一整段很标准的内存分配过程,那么如果存在特殊情况将会是怎样的呢?

比如说,产生的对象Eden直接装不下的那种

3.2 非正常分配过程

17.png

如图所示

进入老年代的方式有四种方式:

  • 正常的阈值达到老年代要求

  • 在From/To区放不下时也会晋升老年代(就是阈值没达到老年代,但是Eden产生的正在使用的对象过多)

  • 对象申请时,Eden区直接放不下,将会直接进入老年代判断

    • 如果Old区放的下,那就直接晋升老年代

    • 如果Old区放不下,那就触发Major GC,如果放得下就晋升,否则就OOM

3.3 验证对象分配过程

3.3.1 短生命周期分配过程

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

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 生命周期仅限于当前循环里,属于短生命周期对象,那么我们来看看具体是对象是如何分配的!

18.png

如图所示

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

注意:

  • JDK11以上好像没有对应exe

  • 首次打开该exe时,需要安装对应插件,然后关闭,再次打开即可!

一切准备就绪后,运行上面代码,然后打开该exe,就能看到

19.png

如图所示

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

因为在上面代码解析的时候就已经说了,产生的对象生命周期仅限于For循环里,并非长生命周期对象

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

3.3.2 长生命周期分配过程

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

20.png

如图所示

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

21.png

如图所示

因为老年代里面的对象一直持有,并没有未使用的对象,当老年代满了时,就会触发OOM异常!!

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

3.4 MinorGc/MajorGC/FullGC的区别

JVM在进行GC时,并非每次都对上面三个内存区域一起回收,大部分的只会针对于Eden区进行 在JVM标准中,他里面的GC按照回收区域划分为两种:

  • 一种是部分采集(Partial GC ):

    • 新生代采集(Minor GC / YongGC):(只采集新生代数据)
    • 老年代采集(Major GC / Old GC):(只采集老年代数据,目前只有CMS会单独采集老年代)
    • 混合采集(Mixed GC)(采集新生代与老年代部分数据,目前只有G1使用)
  • 一种是整堆采集(Full GC):

    • 收集整个堆与方法区的所有垃圾

3.4.1 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 是开发或者调优中尽量要避开的

3.4.2 GC日志查看

22.png

如图所示

在这里添加:-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日志了。

结束语

OK!到这里对象的分配过程已经讲完了!相信看到这的小伙伴已经对对象分配过程有了清晰的认知!在下一篇中,将会重点讲解GC与调优!

猜你喜欢

转载自juejin.im/post/7061587127699669028