JVM--07--堆1---简介、对象分配过程、GC


堆和方法区针对一个 JVM 进程来说是唯一的,也就是一个进程只有一个 JVM ,但是进程包含多个线程,他们是共享同一堆和方法区空间的,每个线程各自包含一套程序计数器、本地方法栈和虚拟机栈。

  • 一个进程 一个jvm 一个堆和方法区 一个Runtime实例

Runtime

每个Java应用程序都有一个类的实例Runtime (jvm运行时环境) 单例
在这里插入图片描述

Java VisualVM

下图就是使用:Java VisualVM 查看堆空间的内容,通过 jdk/bin提供的插件
在这里插入图片描述

Java VisualVM 插件地址

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

在这里插入图片描述

参数设置

-Xms10m:最小堆内存
-Xmx10m:最大堆内存

在这里插入图片描述

堆的核心概念

在这里插入图片描述

一个 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(Garbage Collection 垃圾收集器)执行垃圾回收的重点区域

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

  • 也就是触发了 GC 的时候,才会进行回收
  • 如果堆中对象马上被回收,那么用户线程就会收到影响,因为有 Stop The World
    在这里插入图片描述

堆内存细分

Java 7 及之前:

堆内存逻辑上分为三部分:新生区+老年区+永久区

  • Young Generation Space 新生区 Young/New 又被划分为 Eden 区和 Survivor 区
  • Tenure Generation Space 养老区 Old/Tenure
  • Permanent Space 永久区 Perm

Java 8 及之后:

堆内存逻辑上分为三部分:新生区+老年区+元空间

  • Young Generation Space 新生区 Young/New 又被划分为 Eden 区Survivor 区 -
  • Tenure Generation Space 养老区 Old/Tenure
  • Meta Space 元空间 Meta

约定:新生区 <-> 新生代 <-> 年轻代 、 养老区 <-> 老年区 <-> 老年代、 永久区 <-> 永久代

在这里插入图片描述
堆空间内部结构,JDK 1.8 时从永久代替换成元空间

在这里插入图片描述

设置堆内存大小与 OOM

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

“-Xms" 用于表示堆区的起始内存,等价于 -XX:InitialHeapSize

“-Xmx" 则用于表示堆区的最大内存,等价于 -XX:MaxHeapSize

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

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

开发中建议将初始堆内存和最大堆内存设置成相同的值

默认情况下:

  • 初始内存大小:物理电脑内存大小/64
  • 最大内存大小:物理电脑内存大小/4
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");
    }
}

输出结果

-Xms:245M
-Xmx:3623M

如何查看堆内存的内存分配情况

  • jps -> jstat -gc 进程id
  • -XX:+PrintGCDetails

为什么设置初始堆内存为600M,实际只有575M?

答:因为在新生代中,数据存放在 Eden 区和 Survivor 区,其中 Survivor0 和 Survivor1 区只能二选一存放,少了一个25600 / 1024 = 25M。

OutOfMemory 举例

package com.cy.java;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class HeapTest {

  byte[] buffer=new byte[new Random().nextInt(1024*200)];

    public static void main(String[] args) {

       List<HeapTest> list = new ArrayList<>();
        while(true) {
            list.add(new HeapTest());

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

        }
    }
}

在这里插入图片描述
在这里插入图片描述

轻年代与老年代

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

  • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速 - 生命周期短的,及时回收即可
  • 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与 JVM 的生命周期保持一致

Java 堆区进一步细分的话,可以划分为

  • 年轻代(YoungGen)
  • 老年代(OldGen)

年轻代又可以划分为

  • Eden 空间、
  • Survivor0 空间和 Survivor1 空间(有时也叫做 From 区、To 区)

在这里插入图片描述

下面默认参数开发中一般不会调:

在这里插入图片描述

  • Eden : From : To -> 8 : 1 : 1
  • 新生代 : 老年代 - > 1 : 2

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

  • 默认-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

为什么默认是8:1:1,而实际当中是6:1:1?
答:因为存在自适应机制,即-XX:-UseAdaptiveSizePolicy(+启用,-禁用),但这种方法一般不能生效,所以一般采用-XX:SurvivorRatio=8

可以使用选项"-Xmn"设置新生代最大内存大小(优先级高于-XX:NewRatio)

对象生命周期

  1. 几乎所有的 Java 对象都是在 Eden 区被 new 出来的
  2. 绝大部分的 Java 对象的销毁都在新生代进行了。
  3. 有些大的对象在 Eden 区无法存储时候,将直接进入老年代

IBM 公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。

在这里插入图片描述

图解对象分配过程

概述

为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑

  • 内存如何分配
  • 哪里分配等问题
  • 并且由于内存分配算法内存回收算法密切相关
  • 所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片

在这里插入图片描述

图解过程

我们创建的对象,一般都是存放在 Eden 区的,当我们 Eden 区满了后,就会触发 GC 操作,一般被称为 YGC / Minor GC 操作

在这里插入图片描述
当我们进行一次垃圾收集后,红色的将会被回收,而绿色的还会被占用着,存放在S0(Survivor From) 区。同时我们给每个对象设置了一个年龄计数器,一次回收后就是1。

同时 Eden 区继续存放对象,当 Eden 区再次存满的时候,又会触发一个 MinorGC 操作,此时 GC 将会把 Eden 和 Survivor From 中的对象进行一次收集,把存活的对象放到 Survivor To区,同时让年龄 + 1

在这里插入图片描述
我们继续不断的进行对象生成和垃圾回收,当 Survivor 中的对象的年龄达到15的时候,将会触发一次 Promotion 晋升的操作,也就是将年轻代中的对象晋升到老年代中

在这里插入图片描述

思考:幸存区区满了后?

特别注意,在 Eden 区满了的时候,才会触发 Minor GC,而 Survivor 区满了后,不会触发 Minor GC 操作

如果 Survivor 区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代

举例:以当兵为例,正常人的晋升可能是 : 新兵 -> 班长 -> 排长 -> 连长
但是也有可能有些人因为做了非常大的贡献,直接从 新兵 -> 排长

对象分配的特殊情况

在这里插入图片描述

常用的调优工具

在这里插入图片描述
在这里插入图片描述

总结

  • 针对幸存者 S0,S1 区的总结:复制之后有交换,谁空谁是 To
  • 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集
  • 新生代采用复制算法的目的:是为了减少内碎片

Minor GC,MajorGC、Full GC

  • Minor GC:新生代的 GC
  • Major GC:老年代的 GC
  • Full GC:整堆收集,收集整个 Java 堆和方法区的垃圾收集

我们都知道,JVM 的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现 STW 的问题 而
Major GC 和 Full GC 出现 STW 的时间,是 Minor GC 的10倍以上

JVM 在进行 GC 时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:

  • 一种是部分收集(Partial GC)
  • 一种是整堆收集(Full GC)

部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:

  • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
  • 老年代收集(Major GC/Old GC):只是老年代的圾收集。
    目前,只有 CMS GC 会有单独收集老年代的行为。
  • 注意,很多时候 Major GC会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
    目前,只有 G1 GC 会有这种行为

整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

Minor GC

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

  • 当年轻代空间不足时,就会触发 Minor GC ,这里的年轻代满指的是 Eden 代满,Survivor 满不会引发 GC 。(每次Minor GC 会清理年轻代的内存。)
  • 因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC 会引发 STW ,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

STW:Stop The World

在这里插入图片描述

Major GC

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

  • 指发生在老年代的 GC ,对象从老年代消失时,我们说 “Major GC” 或 “Full GC” 发生了
  • 出现了 Major GC ,经常会伴随至少一次的 Minor GC (但非绝对的,在 Parallel Scavenge
    收集器的收集策略里就有直接进行 Major GC 的策略选择过程)
  • 也就是在老年代空间不足时,会先尝试触发 Minor GC 。如果之后空间还不足,则触发 Major GC Major GC 的速度一般会比Minor GC 慢10倍以上, STW 的时间更长
  • 如果 Major GC 后,内存还不足,就报 OOM 了

Full GC

触发 Full GC 执行的情况有如下五种:

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

说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些

GC 举例

我们编写一个 OOM 的异常,因为我们在不断的创建字符串,是存放在元空间的

public class GCTest {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "mogu blog";
            while(true) {
                list.add(a);
                a = a + a;
                i++;
            }
        }catch (Exception e) {
            e.getStackTrace();
        }
    }
}

设置 JVM 启动参数

-Xms10m -Xmx10m -XX:+PrintGCDetails

打印出的日志

在这里插入图片描述
触发 OOM 的时候,一定是进行了一次 Full GC ,因为只有在老年代空间不足时候,才会爆出 OOM 异常

STW

STW ,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值