3、运行时数据区线程共享区域(堆)

堆的概述

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域;
  • Java堆在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间;
    • 堆内存的大小是可以调节的;
  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但是在逻辑上,它应该被视为连续的;
  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer、TLAB);
package org.westos.heap;

/**
 * -Xms10m -Xmx10m
 * @author lwj
 * @date 2020/9/14 13:15
 */
public class HeapDemo1 {
    public static void main(String[] args) {
        System.out.println("start");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }
}
package org.westos.heap;

/**
 * -Xms20m -Xmx20m
 * @author lwj
 * @date 2020/9/14 13:16
 */
public class HeadDemo2 {
    public static void main(String[] args) {
        System.out.println("start");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }
}
-Xms10m		# 初始堆大小
-Xmx10m		# 最大堆大小

跑两个程序,启动两个main方法(两个进程,两个JVM实例),分别设置堆内存大小,用下面的工具看。

双击打开JDK安装目录/bin目录下的jvisualvm.exe程序。

在这里插入图片描述

给jdk自带的jvisualvm安装Visual GC插件。

https://visualvm.github.io/pluginscenters.html

选择Visual GC,下载。

在上面那张图片中的工具—>插件,点击添加插件,

在这里插入图片描述

选择安装,重启jvisualvm。即可看到Visual GC。

  • 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上
  • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置;
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除;
  • 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

在这里插入图片描述

public class SimpleHeap {
    private int id;

    public SimpleHeap(int id) {
        this.id = id;
    }

    public static void main(String[] args) {
        SimpleHeap simpleHeap1 = new SimpleHeap(1);
        SimpleHeap simpleHeap2 = new SimpleHeap(2);

        int[] arr = new int[10];
        Object[] objs = new Object[20];
    }
}
 0 new #3 <org/westos/heap/SimpleHeap>
 3 dup
 4 iconst_1
 5 invokespecial #4 <org/westos/heap/SimpleHeap.<init>>
 8 astore_1
 9 new #3 <org/westos/heap/SimpleHeap>
12 dup
13 iconst_2
14 invokespecial #4 <org/westos/heap/SimpleHeap.<init>>
17 astore_2
18 bipush 10
20 newarray 10 (int)
22 astore_3
23 bipush 20
25 anewarray #5 <java/lang/Object>
28 astore 4
30 return

堆的细分内存结构

1、Java7及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区

新生区

  • Eden区(伊甸园) + Survivor区(幸存区)

养老区

永久区(Permanent Space)

2、Java8及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间

新生区

  • Eden区 + Survivor区

养老区

元空间(Meta Space)

在这里插入图片描述

在这里插入图片描述

注意:s0和s1在同一个时刻,只有一个区存在对象。

设置堆内存大小和OOM

Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项-Xms-Xmx来进行设置。

  • -Xms用于表示堆区的起始内存,等价于-XX:InitialHeapSpace
  • -Xmx则用于表示堆区的最大内存,等价于-XX:MaxHeapSize

一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出OutOfMemoryError异常。

通常会将-Xms和-Xmx两个参数配置相同的值,其目的在于为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,提高性能。

默认情况下,初始内存:物理电脑内存大小 / 64;

​ 最大内存大小:物理电脑内存大小 / 4。

package org.westos.heap;

/**
 * -Xms:用来设置堆空间(年轻代 + 老年代)的初始内存大小
 *  -X:JVM运行参数
 *  ms:memory start
 * -Xmx:用来设置堆空间能获取到的最大内存
 * @author lwj
 * @date 2020/9/14 18:14
 */
public class HeapSpaceInitial {
    public static void main(String[] args) {
        //返回Java虚拟机中的堆内存总量
        long totalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms:" + totalMemory + "M");
        System.out.println("-Xmx:" + maxMemory + "M");
        //默认情况下
        //-Xms:123M 物理内存1/64 1/8G
        //-Xmx:1804M 物理内存1/4 2G
    }
}

查看堆空间大小:

方式一:

jps
# 查看进程id
jstat -gc 进程id
# 查看对应进程的堆空间大小

以HeapDemo1为例,设置-Xms10m -Xmx10m。

在这里插入图片描述

方式二:加参数

-XX:+PrintGCDetails

HeapSpaceInitial的例子(参数:-Xms10m -Xmx10m -XX:+PrintGCDetails)

public class HeapSpaceInitial {
    public static void main(String[] args) {
        //返回Java虚拟机中的堆内存总量
        double totalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024.0;
        double maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024.0;

        System.out.println("-Xms:" + totalMemory + "M");
        System.out.println("-Xmx:" + maxMemory + "M");

        //当设置参数-Xms10m -Xmx10m
        //-Xms:9.5M
        //-Xmx:9.5M

        //为什么是9.5M呢?因为survivor0区和survivor1区同时只能有一个区域存储对象
    }
}

-Xms:9M
-Xmx:9M
Heap
 PSYoungGen      total 2560K, used 1591K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 77% used [0x00000000ffd00000,0x00000000ffe8dc48,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
 Metaspace       used 3076K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 334K, capacity 388K, committed 512K, reserved 1048576K
年轻代2560 = Eden space 2048 + from区 / to区
2048 + 512 + 512 + 7168 = 10240K
2048 + 512 + 7168 = 9.5m

OOM

Out Of Memory内存溢出。

在堆中new很多对象。

package org.westos.heap;

import java.util.ArrayList;

/**
 * -Xms30m -Xmx30m
 * @author lwj
 * @date 2020/9/14 19:07
 */
public class OOMTest {
    public static void main(String[] args) {
        ArrayList<RandomTest> list = new ArrayList<>();
        for (int i = 1; ; i++) {
            try {
                Thread.sleep(20);
                //为了能看到过程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new RandomTest(i));
        }
    }
}

class RandomTest {
    private byte[] b;

    public RandomTest(int length) {
        this.b = new byte[length];
    }
}

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at org.westos.heap.RandomTest.<init>(OOMTest.java:29)
	at org.westos.heap.OOMTest.main(OOMTest.java:20)

当老年代已经满了时,已经不能GC了,就爆了。

Eden space —> s0 / s1 —> Old space。当使用Visual GC时,可以观察到细节。

年轻代与老年代

Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen),其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫作from区、to区)。

在这里插入图片描述

默认新生代与老年代在堆结构的占比:

  • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3;
  • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5。

NewRatio:设置新生代与老年代的比例情况。

用指令查看某个进程的NewRatio比例。

jinfo -flag NewRatio 进程id

在这里插入图片描述

在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1。

当然开发人员可以通过选项-XX:SurvivorRatio调整这个空间比例。

比如-XX:SurvivorRatio=8

几乎所有的Java对象都是在Eden区被new出来的。

绝大部分的Java对象的销毁都在新生代进行了;

  • IBM公司的研究表明,新生代中80%的对象都是朝生夕死的;

可以使用选项-Xmn设置新生代最大内存大小。

  • 这个参数一般使用默认值就可以了。

在这里插入图片描述

事实上使用时,并不是8:1:1。

-XX:-UseAdaptiveSizePolicy:关闭自适应的内存分配策略。

图解对象分配和回收过程

可达性分析算法(Eden、Minor GC)

为每一个对象分配一个年龄计数器,从Eden区到幸存者区,赋值为1。

在这里插入图片描述

第二次Minor GC。(From S0 —> To S1)

在这里插入图片描述

第三次Minor GC。(From S1 —> To S0)

在这里插入图片描述

触发Minor GC的条件:Eden区满的时候。

当Eden区满的时候,触发Minor GC(Young GC),会将Eden区和幸存者区一起进行回收。

关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久代/元空间收集。

对象分配的特殊情况

在这里插入图片描述

常用调优工具

JDK命令行

  • jinfo
  • jstat
  • javap

Jconsole

VisualVM

Jprofiler(性能监控与调优)

Java Flight Recorder

GCViewer

Minor GC、Major GC、Full GC的对比

JVM在进行GC时,大部分时候回收的都是指新生代。

针对HotSpot VM的实现,它里面的GC按照回收区域分为两大种类型,一种是部分收集(Partial GC),一种是整堆收集(Full GC)。

  • 部分收集:不是完整收集整个Java堆的垃圾,其中又分为:
    • 新生代收集(Minor GC / Young GC):只是新生代(Eden、S0/S1)的垃圾收集;
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
      • 目前只有CMS GC会有单独收集老年代的行为;
      • 很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收;
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

年轻代GC(Minor GC)触发机制:

  • 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC;
  • 因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快;
  • Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。

老年代GC(Major GC / Full GC)触发机制:

  • 指发生在老年代的GC,对象从老年代消失时,我们说Major GC或Full GC发生了;
  • 出现了Major GC,经常会伴随至少一次的Minor GC;
  • Major GC的速度一般会比Minor GC满10倍以上,STW的时间更长;
  • 如果Major GC后,内存还不足,就报OOM。

Full GC触发机制:

  • 调用System.gc(),系统建议执行Full GC,但是不必然执行;
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
  • 由Eden区、S0区向S1区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

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

堆空间分代思想

为什么需要把Java堆分代?

  • 不同对象的生命周期不同,70%-99%的对象是临时对象。

其实不分代完全可以,分代的唯一理由就是优化GC性能。

内存分配策略

在这里插入图片描述

为对象分配内存:TLAB

为什么要有TLAB(Thread Local Allocation Buffer)?

  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据;
  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的;
  • 为了避免多个线程操作同一地址,需要使用加锁等机制。进而影响分配速度。

线程本地分配缓存区,这是一个线程专用的内存分配区域。

  • 对Eden区域继续进行划分,JVM为每个线程分配了一个私有的缓存区域,它包含在Eden空间;
  • JVM是将TLAB作为内存分配的首选;
  • 在程序中,开发人员可以通过选项-XX:UseTLAB设置是否开启TLAB空间,默认情况下是开启的;
  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%;
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

堆是分配对象的唯一选择吗

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收了。这也是最常见的堆外存储技术。

逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸;
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生了逃逸。例如作为调用参数传递到其他地方中。
public void method() {
    V v = new V();
    //use v
    v = null;
}

没有发生逃逸的对象,可以在栈上分配,随着方法执行的结束,栈空间就被移除。

在这里插入图片描述

在这里插入图片描述

同步省略:锁消除。

在这里插入图片描述

多个线程必须是同一把锁,同一个对象。

标量替换

标量是指一个无法再分解成更小数据的数据。Java中的原始数据类型就是标量。

相对的,那些还可以分解的数据叫做聚合量,Java中的对象就是聚合量。

如果经过逃逸分析,发现一个对象不会被外界访问,那么经过JIT优化,就会把这个对象拆解成若干个成员变量来代替,这个过程就是标量替换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值