【JVM】【第六章】【JVM 运行时数据区】【堆】【01】堆内存的设置

1.堆的核心概述

在这里插入图片描述

1.1 认识堆内存

1.1.1 堆与进程

  • 一个java程序运行起来对应一个JVM进程
  • 一个JVM进程对应一个JVM实例,一个JVM实例中只有一个运行时数据区;
  • 一个运行时数据区只有一个方法区和堆,而一个进程中有多个线程,这就意味着一个进程中的多个线程就要共享方法区和堆;每个线程独享程序计数器、本地方法栈、虚拟机栈

1.1.2 对堆的认识

  1. 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  2. Java堆区在JVM启动的时候即被创建,JVM启动后其空间大小也就确定了【类比数组】。
堆是JVM管理的最大一块内存空间;
堆内存的大小是可以调节的,在启动java程序前对jvm堆内存大小进行配置;

jvm什么时候启动?
	java程序启动起来的时候,jvm实例就会通过引导类加载器帮我们启动起来
  1. 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

  2. 一个进程中的所有的线程共享Java堆,但是在这里堆内存还可以划分线程私有的缓冲区[Thread Local Allocation Buffer,TLAB]
    面试问题:堆空间一定是所有线程共享的么?不是,TLAB缓冲区在堆中,是线程独有的)

  3. 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)
    老师说:从实际使用角度看的,“几乎”所有的对象实例都在这里分配内存。因为还有一些对象是在栈上分配的(逃逸分析,标量替换)

  4. 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。

public class SimpleHeap {
    private int id;//属性、成员变量

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

    public void show() {
        System.out.println("My ID is " + id);
    }
    
    public static void main(String[] args) {
        SimpleHeap sl = new SimpleHeap(1);
        SimpleHeap s2 = new SimpleHeap(2);
        int[] arr = new int[10];
        Object[] arr1 = new Object[10];
    }
}

在这里插入图片描述

  1. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
    (1)也就是触发了GC的时候,才会进行回收
    (2)如果堆中对象马上被回收,那么用户线程就会收到影响,因为有stop the word
    如上面的代码示例中,main方法执行结束了,但是堆内存中的s1和s2实例不会被立马回收;
    因为方法是用来被调用的,如果方法中存在对实例的引用,方法弹出栈就回收,那么GC的频率太高了,会影响使用体验

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

1.2堆内存结构细分

  • Java 7及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区
  1. Young/New Generation Space 新生区(Young/new),又被细划分为Eden区和Survivor区
  2. Old/Tenure generation space 养老区(Old)
  3. Permanent Space永久区 (Perm)
  • Java 8及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间
  1. Young/New Generation Space 新生区,又被划分为Eden区和Survivor区
  2. Old/Tenure generation space 养老区
  3. Meta Space 元空间 Meta
  • 约定:新生区= 新生代=年轻代 、 养老区=老年区 =老年代、 永久区 = 永久代
  • 堆空间内部结构,JDK1.8开始从永久代 替换成 元空间
    在这里插入图片描述

2.堆内存大小的设置

  • 由于一个java程序运行起来对应一个JVM进程,也就是一个jvm实例,因此可以针对每一个java程序来设置其jvm参数
  • Jvm参数的设置是在java运行环节加入进来的,和编译没关系,因此可以先将java代码编译成字节码文件,在执行java指令准备运行代码的时候,将jvm参数加入进来

2.1 堆内存的 JVM参数

-Xms和-Xmx

  • 参数影响的内存范围
    逻辑上分为新生区+养老区的总大小(即Xms/Xmx分配的内存物理上没有涉及方法区)

  • -Xms:用于表示堆区的起始内存,等价于 -XX:InitialHeapSize
    -X:jvm 的运行参数
    ms:memory start

  • -Xmx:用于表示堆区的最大内存,等价于 -XX:MaxHeapSize
    一旦堆区中的内存大小超过 -Xmx 所指定的最大内存时,将会抛出 OutOfMemoryError 异常。

  • 生产环境中,通常会将 -Xms 和 -Xmx 两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能【避免频繁扩容、释放,避免GC,让系统承担额外压力】

  • 配置大小的单位默认是byte
    在这里插入图片描述

2.2 IDEA中设置堆内存大小示例

1.程序demo

package chapter08;


/**
 * -Xms10m
 * -Xmx10m
 */
public class HeapDemo {
    public static void main(String[] args) {
        System.out.println("start.....");

        try{
            Thread.sleep(1000000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("end....");
    }
}

2.编译

在这里插入图片描述

3.配置jvm参数

在这里插入图片描述

4.运行java程序

2.2 堆内存的默认大小

  • 默认情况下:
    初始内存大小:物理内存大小 / 64
    最大内存大小:物理内存大小 / 4
package chapter08;

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");  //-Xms: 243M
        System.out.println("-Xmx: " + maxMemory + "M");     //-Xmx: 3604M

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

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

3.查看java程序对应的jvm内存的四种方式

3.1 代码打印

  • 手动设置下面的程序的JVM参数:-Xms600m -Xmx600m
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");          


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

打印结果为:
-Xms: 575M
-Xmx: 575M
这里减去了一个s区,所以没有600m

3.2 命令行方式

通过CMD进入终端jps + jstat -gc 进程id

  • jps 指令可以查看当前进行的java进程
  • jstat -gc 进程id: 查看进程内存使用情况
    在这里插入图片描述

各个参数说明:

  • 单位是Kbyte;
  • S0和S1为幸存者区,EC为Eden区;
  • OC为老年代;
  • 新生代总内存 = S0C+S1C+EC (25m+25m+150m = 200m)
  • 新生代已用内存 = S0U + S1U + EU
  • 老年代总内存 = OC (400m)
  • 老年代已用内存 = OU

堆总内存 = S0C + S1C + OC +EC

设置堆大小为600m,为什么打印结果为575m?

  • Runtime.getRuntime().totalMemory() 和 Runtime.getRuntime().maxMemory()的打印结果为575M
  • 因为幸存者区S0和S1各占据了25m,但是他们始终有一个是空的; 新生代存放对象的是伊甸园区和一个幸存者区。
  • 通过Runtime获取的Memory的大小为: S0(或者S1) + Eden区 + old区;少了一个S区,所以计算结果比实际的堆内存要小

3.3 VM参数 控制台打印详细

在VM Options 添加 -XX:+PrintGCDetails
在这里插入图片描述
在这里插入图片描述
注意:这里新生代总大小179200k 仍然是只计算了eden+一个S区

3.4 Java VisualVM

  • Java VisualVM是jdk自带的工具,在 jdk目录下的bin目录下的jvisualvm.exe
    在这里插入图片描述
    在这里插入图片描述
  • 需要安装额外的Visual GC插件来看堆内存明细
    在这里插入图片描述
    在这里插入图片描述

4.OOM

OOM原因

在对老年代进行GC之后,GC不意味着清空,再往老年代添加对象,放不下了,C了,就

OOM Demo代码

/**
 * -Xms600m -Xmx600m
 * @author shkstart  shkstart@126.com
 * @create 2020  21:12
 */
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)));
        }
    }
}

在内存中创建一个list,不断地添加元素;执行一段时间爆OOM

minor gc后,如果s区放不下了,会放进老年代,因此每次gc会导致老年代增加

5.年轻代与老年代的内存分配设置

  • 存储在堆内存中的java对象可以被分为两类:
  • 生命周期短暂,这类对象的创建和消亡都非常迅速
  • 另一类对象生命周期长,极端情况下还能与JVM生命周期保持一致。
  • 对应的Java堆区进一步细分可以分为年轻代(YoungGen)和老年代(OldGen)
  • 其中年轻代可以分为Eden空间、Survivor0空间和Survivor1空间(有时也叫frmo区,to区)
    在这里插入图片描述

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

Demo代码

/**
 * -Xms600m -Xmx600m
 *
 * -XX:NewRatio : 设置新生代与老年代的比例。默认值是2.
 * -XX:SurvivorRatio :设置新生代中Eden区与Survivor区的比例。默认值是8
 * -XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略  (暂时用不到)
 * -Xmn:设置新生代的空间的大小。 (一般不设置)
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  17:23
 */
public class EdenSurvivorTest {
    public static void main(String[] args) {
        System.out.println("我只是来打个酱油~");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 配置参数 -XX:NewRatio

  • 一般不会调这个比例,当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优

  • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
    在这里插入图片描述

5.2 配置Eden和S在新生代中的占比

在这里插入图片描述

  • 配置参数
    -XX:SurvivorRatio
    -XX:SurvivorRatio= 8 即 8:1:1
  • 在hotSpot中,默认的设置为:Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
  • 开发人员可以通过选项 -XX:SurvivorRatio 调整空间比例,如-XX:SurvivorRatio=4

配置演示

(1) 设置堆内存为600M:
在这里插入图片描述
(2) 此时不设置-XX:SurvivorRatio= 8 查看堆内存情况:

在这里插入图片描述
会发现Eden只有150M,S区25M【eden和s区不是按照8:1:1来的,而是6:1:1】

(3) 要想实现真正的8:1:1 必须显示的设置:
如下所示 设置虚拟机参数:
在这里插入图片描述
在这里插入图片描述
可以看出实现了8:1:1

5.3 -XX:-UseAdaptiveSizePolicy

  • -XX:-UseAdaptiveSizePolicy:关闭自适应的内存分配策略
  • -XX:+UseAdaptiveSizePolicy:开启自适应的内存分配策略
    内存分配策略即使关闭了,Eden:S仍然是6:1:1,所以必须显式设置Eden和S的比例才能设置为8:1:1

5.4 -Xmn 设置新生代最大内存

  • -Xmn设置新生代最大内存,如果和-XX:NewRatio 冲突的时候,以-Xmn为准。
  • 这个参数一般使用默认值

5.5 通过命令行查看各种比例

  1. 查看新生代与老年代的比例
    jps
    jinfo -flag NewRatios 进程id

  2. 查看新生区中伊甸园区与幸存者区的比例
    jps
    jinfo -flag SurvivorRatio 进程id

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值