Java堆、设置堆内存、年轻代与老年代

堆的概述

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。

  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,堆是JVM管理的最大一块内存空间,并且堆内存的大小是可以调节的。

  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。

  • 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)

    • 从实际使用角度看:“几乎”所有的对象实例都在堆分配内存,但并非全部。因为还有一些对象是在栈上分配的(逃逸分析,标量替换)
    • 数组和对象可能永远不会存储在栈上(不一定),因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。也就是触发了GC的时候,才会进行回收

  • 如果堆中对象马上被回收,那么用户线程就会收到影响,因为有stop the word

  • 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

  • 随着JVM的迭代升级,原来一些绝对的事情,在后续版本中也开始有了特例,变的不再那么绝对。
    在这里插入图片描述

堆内存细分

现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

  • Java7 及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
    • Young Generation Space 新生区 Young/New,又被划分为Eden区和Survivor区
    • Old generation space 养老区 Old/Tenure
    • Permanent Space 永久区 Perm
  • Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
    • Young Generation Space 新生区,又被划分为Eden区和Survivor区
    • Old generation space 养老区
    • Meta Space 元空间 Meta
      约定:新生区 = 新生代 =年轻代 、 养老区 =老年区=老年代、 永久区=永久代

在这里插入图片描述
jdk8将永久代换成了元空间
在这里插入图片描述

JVisualVM可视化查看堆内存

运行下面代码

public class HeapDemo {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            TimeUnit.MINUTES.sleep(30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("end...");
    }

}
  1. 双击jdk目录下的jvisualvm.exe程序
    在这里插入图片描述
  2. 工具 -> 插件 -> 安装Visual GC插件
    在这里插入图片描述
    我的因为已经安装,所以没有显示
    如果显示安装失败

在这里插入图片描述
解决方法:访问页面:https://visualvm.github.io/pluginscenters.html
找到对应的版本,我的是jdk8 131所以直接复制地址就行
在这里插入图片描述
覆盖后再次点击安装就可成功在这里插入图片描述
3. 运行上面的代码,可以看到堆中的内存变化
在这里插入图片描述

设置堆内存大小与 OOM

设置堆内存

  • Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项”-Xms”和”-Xmx”来进行设置。
    • -Xms用于表示堆区的起始内存,等价于**-XX:InitialHeapSize**
    • -Xmx则用于表示堆区的最大内存,等价于**-XX:MaxHeapSize**
  • 一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutofMemoryError异常。
  • 通常会将-Xms和-Xmx两个参数配置相同的值
    • 原因:假设两个不一样,初始内存小,最大内存大。在运行期间如果堆内存不够用了,会一直扩容直到最大内存。如果内存够用且多了,也会不断的缩容释放。频繁的扩容和释放造成不必要的压力,避免在GC之后调整堆内存给服务器带来压力。
    • 如果两个设置一样的就少了频繁扩容和缩容的步骤。内存不够了就直接报OOM-
  • 默认情况下:
    • 初始内存大小:物理电脑内存大小/64
    • 最大内存大小:物理电脑内存大小/4
/**
 * 1. 设置堆空间大小的参数
 * -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
 *      -X 是jvm的运行参数
 *      ms 是memory start
 * -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
 *
 * 2. 默认堆空间的大小
 *    初始内存大小:物理电脑内存大小 / 64
 *             最大内存大小:物理电脑内存大小 / 4
 * 3. 手动设置:-Xms600m -Xmx600m
 *     开发中建议将初始堆内存和最大的堆内存设置成相同的值。
 *
 * 4. 查看设置的参数:方式一: jps   /  jstat -gc 进程id
 *                  方式二:-XX:+PrintGCDetails
 */
public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

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

        System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
        System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
电脑内存大小是16G,不足16G的原因是操作系统自身还占据了一些。

两个不一样的原因待会再说

将虚拟机的堆内存设置为-Xms600m -Xmx600m时,再次运行上面代码:
在这里插入图片描述
在这里插入图片描述
为什么对内存为575M,会少25M

方式一: 首先使用jps, 然后使用 jstat -gc 进程id在这里插入图片描述

SOC: S0区总共容量
S1C: S1区总共容量
S0U: S0区使用的量
S1U: S1区使用的量
EC: 伊甸园区总共容量
EU: 伊甸园区使用的量
OC: 老年代总共容量
OU: 老年代使用的量

1、

25600+25600+153600+409600 = 614400K

614400 /1024 = 600M

2、

25600+153600+409600 = 588800K

588800 /1024 = 575M

3、并非巧合,S0区和S1区两个只有一个能使用,另一个用不了(后面会详解)

方式二:-XX:+PrintGCDetails
在idea 中加入参数
在这里插入图片描述
会显示堆内存的使用情况
在这里插入图片描述

OOM

public class OOMTest {
    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while(true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 不断的创建大内存的对象
            list.add(new Picture(new Random().nextInt(1024 * 1024)));
        }
    }
}

class Picture{
    private byte[] pixels;
	
    public Picture(int length) {
        this.pixels = new byte[length];
    }
}

设置堆内存为600M

[GC (Allocation Failure) [PSYoungGen: 153395K->25589K(179200K)] 153395K->123342K(588800K), 0.0147738 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 178894K->25583K(179200K)] 276647K->269472K(588800K), 0.0196213 secs] [Times: user=0.00 sys=0.08, real=0.02 secs] 
[GC (Allocation Failure) [PSYoungGen: 178598K->25580K(179200K)] 422487K->418217K(588800K), 0.0272733 secs] [Times: user=0.01 sys=0.14, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 25580K->8801K(179200K)] [ParOldGen: 392637K->409204K(409600K)] 418217K->418005K(588800K), [Metaspace: 9044K->9044K(1058816K)], 0.0530591 secs] [Times: user=0.02 sys=0.02, real=0.05 secs] 
[Full GC (Ergonomics) [PSYoungGen: 162238K->159030K(179200K)] [ParOldGen: 409204K->409200K(409600K)] 571443K->568231K(588800K), [Metaspace: 9101K->9101K(1058816K)], 0.0176950 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 159030K->159027K(179200K)] [ParOldGen: 409200K->408839K(409600K)] 568231K->567866K(588800K), [Metaspace: 9101K->8958K(1058816K)], 0.0464870 secs] [Times: user=0.05 sys=0.00, real=0.05 secs] 
Heap
 PSYoungGen      total 179200K, used 159290K [0x00000000f3800000, 0x0000000100000000, 0x0000000100000000)
  eden space 153600K, 100% used [0x00000000f3800000,0x00000000fce00000,0x00000000fce00000)
  from space 25600K, 22% used [0x00000000fce00000,0x00000000fd38ea28,0x00000000fe700000)
  to   space 25600K, 0% used [0x00000000fe700000,0x00000000fe700000,0x0000000100000000)
 ParOldGen       total 409600K, used 408843K [0x00000000da800000, 0x00000000f3800000, 0x00000000f3800000)
  object space 409600K, 99% used [0x00000000da800000,0x00000000f3742fa8,0x00000000f3800000)
 Metaspace       used 8977K, capacity 9244K, committed 9728K, reserved 1058816K
  class space    used 1051K, capacity 1121K, committed 1280K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.atguigu.java.Picture.<init>(OOMTest.java:29)
	at com.atguigu.java.OOMTest.main(OOMTest.java:20)

大对象把堆内存占满了,产生OutOfMemoryError异常

年轻代与老年代

  1. 存储在JVM中的Java对象可以被划分为两类:

    • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
    • 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致
  2. Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)

  3. 其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)
    在这里插入图片描述

  • 配置新生代与老年代在堆结构的占比

    • 默认**-XX:NewRatio**=2,表示新生代占1,老年代占2,新生代占整个堆的1/3

    可以修改**-XX:NewRatio**=4,表示新生代占1,老年代占4,新生代占整个堆的1/5

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

    • 当然开发人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比如-XX:SurvivorRatio=8
  • 几乎所有的Java对象都是在Eden区被new出来的。

  • 绝大部分的Java对象的销毁都在新生代进行了(有些大的对象在Eden区无法存储时候,将直接进入老年代),IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。

  • 可以使用选项”-Xmn”设置新生代最大内存大小,但这个参数一般使用默认值就可以了。

创建对象流程:

  1. new的对象先放伊甸园区。此区有大小限制。
  2. 如果伊甸园区放得下,就放在伊甸园区。如果放不下,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁,未被销毁的对象就放入幸存者0区。然后再加载新的对象放到伊甸园区。
  3. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
  4. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。(幸存者0区和1区不断的相互交换)
  5. 啥时候能去养老区呢?可以设置次数。默认是15次。可以设置新生区进入养老区的年龄限制
    • JVM 参数:-XX:MaxTenuringThreshold=N 进行设置
  6. 在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理
  7. 若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常。

对象分配的特殊情况

  • 如果来了一个新对象,先看看 Eden 是否放的下?
    • 如果 Eden 放得下,则直接放到 Eden 区
    • 如果 Eden 放不下,则触发 YGC ,执行垃圾回收,看看还能不能放下?
  • 将对象放到老年区又有两种情况:
    • 如果 Eden 执行了 YGC 还是无法放不下该对象,那没得办法,只能说明是超大对象,只能直接放到老年代
    • 那万一老年代都放不下,则先触发FullGC ,再看看能不能放下,放得下最好,但如果还是放不下,那只能报 OOM
  • 如果 Eden 区满了,将对象往幸存区拷贝时,发现幸存区放不下啦,那只能便宜了某些新对象,让他们直接晋升至老年区
    具体流程图如下:
    在这里插入图片描述
    Minor GC会一直重复这样的过程,直到“To”区被填满即Eden区存活的对象和from区存活的对象很多了,被复制到to区域时,to区域一下子接收装不下了,则“To”区被填满时,就不会再进行角色互换变成from了, 而是“To”区被填满之后,会将所有对象移动到年老代中,则“To”区是空的了,即“To”区变成空有两种方式,一是对象从from移动到to区后,角色互换,为空的区域from变成to,to就变成空了的;二是Eden和 from中未达到15岁的对象两者加起来太多,移动到to区填满了,则把填满了to区的对象移动到老年代,此时eden区和from区对象变少了,to区也没经过角色互换,变成空的。
    (在将Eden区剩余对象放入to区时,如果放了一部分to区满了就会先将to区中的对象存到老年代从而清空to区,然后再继续往to区放对象)
    有个问题,to区域被填满了,to区中的有的对象年龄还没被复制15次,也会被移动到年老代中吗?
    答:Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。默认情况下,如果对象年龄达到15岁,就会移动到老年代中。
  • 24
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值