JVM深入学习(六)-运行时数据区之堆

简介: JVM深入学习(六)-运行时数据区之堆

4.1 堆的概述

  1. 一个jvm实例只存在一个堆内存,一个进程对应一个jvm实例,堆也是jvm内存管理的核心区域
  2. 堆在jvm启动的时候就创建好了,是JVM管理最大的一块内存空间
  1. 堆大小可以调节  -Xms初始堆大小  -Xmx最大堆大小
  2. 代码说明

package com.zy.study05;


/**

* @Author: Zy

* @Date: 2021/7/29 15:47

* -Xms20M -Xmx20M

*/

public class HeapSizeTest {

   public static void main(String[] args) {

       try {

           System.out.println("start............");

           Thread.sleep(1000000);

       } catch (InterruptedException e) {

           e.printStackTrace();

       }finally {

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

       }

   }

}


package com.zy.study05;


/**

* @Author: Zy

* @Date: 2021/7/29 15:47

* -Xms10M -Xmx10M

*/

public class HeapSizeTest1 {

   public static void main(String[] args) {

       try {

           System.out.println("start............");

           Thread.sleep(1000000);

       } catch (InterruptedException e) {

           e.printStackTrace();

       }finally {

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

       }

   }

}


分别设置两个进程的jvm堆大小,然后通过jdk自带的jvisualvm来查看堆

  1. java虚拟机规范规定,堆可以处于物理上不连续的内存,但在逻辑上应该被视为连续的(物理内存可以与虚拟内存建立映射)
  2. 所有的线程共享java堆
  1. TLAB,堆中划分线程私有的缓冲区 提升线程并发性
  1. java虚拟机规范中对堆的描述是: 所有对象实例以及数组都应该在运行时分配在堆上
  2. 数组和对象永远不会存储在栈上,因为栈帧中保存引用,引用指向堆中的对象/数组的实际位置
  3. 方法结束后,堆中的对象不会马上被回收,而是等到GC的时候才会回收
  4. 堆是垃圾回收的重点区域

4.1.1 内存细分

现代垃圾收集器都是基于分代收集理论设计:

jdk7及以前:

新生区+养老区+永久区

jdk8及以后:

新生区+养老区+元空间

jdk8中主要就是变化了元空间,元空间替代了永久区

使用-XX:+PrintGCDetails 参数打印gc回收明细

package com.zy.study06;


import java.util.Date;


/**

* @Author: Zy

* @Date: 2021/7/30 15:04

* 测试-XX:+PrintGCDetails

*/

public class HeapGCPrintTest {

   public static void main(String[] args) {

       int num = 1;

       num = 2;

       Date data = new Date();

       System.out.println(data);

   }

}


如图所示:

PSYongGen新生代

ParOldGen老年代

MetaSpace元空间


4.1.2 堆空间大小的设置

-Xms 设置初始堆空间的大小  等价于 -XX:InitialHeapSize

-Xmx 设置堆空间的最大内存  等价于 -XX:MaxHeapSize

通过代码查看堆大小:

package com.zy.study06;


/**

* @Author: Zy

* @Date: 2021/7/30 15:34

* 默认情况下

*  堆内存初始大小是物理内存的1/64

*  堆内存最大是物理内存的1/4

* 手动设置情况下:

*  -Xms memory start 设置初始内存

*  -Xmx memory max 设置最大内存

* 查看内存使用情况的两种方式

*  1.

*      jps查看jvm进程实例id

*      jstat -gc pid 查看jvm堆内存详情

*  2.

*      -XX:+PrintGCDetails 使用jvm参数打印出gc详情,从而查看堆内存的大小

*

*/

public class HeadSetTest {

   public static void main(String[] args) {

       long initialMemory = Runtime.getRuntime().totalMemory();

       long maxMemory = Runtime.getRuntime().maxMemory();

       System.out.println("初始化内存大小:"+initialMemory/1024/1024+"M");

       System.out.println("最大内存大小:"+maxMemory/1024/1024+"M");


       try {

           Thread.sleep(1000000);

       } catch (InterruptedException e) {

           e.printStackTrace();

       }

   }

}


手动设置-Xms300M -Xmx300M后,输出结果:

与设置的不符合的原因是: 这里的统计会统计新生代(伊甸园区+surviver(0/1))+老年代的堆内存大小

新生代中surviver0/1只会计算一个的堆内存


通过jps和jstat的方式查看堆:


4.1.3 OOM的举例和说明

package com.zy.study06;


import java.util.ArrayList;

import java.util.Random;


/**

* @Author: Zy

* @Date: 2021/7/30 17:05

* -Xms600M -Xmx600M

* 死循环创建byte[]数组对象,将堆空间撑满

*/

public class OOMTest {


   static class ByteTest{

       byte[] bytes;


       public ByteTest(byte[] bytes) {

           this.bytes = bytes;

       }

   }


   public static void main(String[] args) {

       ArrayList arrayList = new ArrayList<>();

       while (true){

           try {

               Thread.sleep(20);

           } catch (InterruptedException e) {

               e.printStackTrace();

           }

           arrayList.add(new ByteTest(new byte[new Random().nextInt(1024*1024)]));

       }

   }

}


随着堆内存的不断占用,等老年代和新生代都被占用完了,就会出现错误

内存溢出

4.2 年轻代和老年代

  1. 存储在jvm中的java对象分为两类
  1. 生命周期较短,创建和消亡非常快速
  2. 生命周期较长,某些情况下甚至与jvm的生命周期相同,比如数据库连接等对象
  1. 堆区细分为新生代和老年代
  1. 新生代分为伊甸园区(Eden),Survivor0,Survivor1
  1. Eden:S0:S1默认8:1:1 但是因为内存自适应策略默认是6:1:1可以使用-XX:-UseAdaptiveSizePolicy关闭自适应策略(此参数暂时没用)
  2. 使用-XX:SurvivorRatio调整 必须显示指定8:1:1 才能达到默认效果
  1. -XX:NewRatio 设置新生代和老年代的大小比例(默认1:2 老年代2 新生代1,一般不用调整)
  1. 例子印证第二点

package com.zy.study07;


/**

* @Author: Zy

* @Date: 2021/8/3 11:42

* -XX:NewRatio 设置老年代:新生代比例 默认2:1

* -XX:SurvivorRatio 设置新生代中伊甸园区:S0:S1比例 默认8:1:1

* -XX:-UseAdaptSizePolicy 禁用新生代中伊甸园区:S0:S1比例内存自适应策略(暂时无用)

*/

public class HeapEdenSurvivorRadioTest {


   public static void main(String[] args) {

       try {

           Thread.sleep(10000000);

       } catch (InterruptedException e) {

           e.printStackTrace();

       }

   }

}


  1. 设置-Xms600M -Xmx600M参数后,查看新生代:老年代
  1. 可以看到 老年代:新生代=400:150+25+25 =2:1
  2. 可以看到 Eden:S0:S1 = 150:25:25 = 6:1:1


4.3 对象分配的过程

4.3.1 对象分配的一般过程

  1. 对象创建都在eden区中,每当eden占用达到100%时,触发一次YGC(young gc),此时会将eden中的不使用的对象进行垃圾回收,然后将仍要使用的对象放到to区,并将对象的age+1
  1. s0和s1是动态的from区和to区,谁为空谁就是to区,这两个区起到一个缓冲的作用
  2. 举例 第一次将对象在s0区,此时s0区就是to区,当触发第二次YGC时,此时s0不为空,s1为空,此时s0就变成了from区,s1就变成了to区,将s0中的对象转到s1并将age+1
  1. s0/s1区不会触发YGC,而是在eden触发YGC的情况下,才会一并对s0/s1区进行垃圾回收
  2. 正常情况下当对象的age=15时,会将对象移入old gen(老年代)中,15为默认参数,可以通过jvm参数调整
  3. gc多为新生代采集,很少对老年代采集,基本不对永久代/元空间采集

图解

4.3.2 对象分配的特殊过程

对象分配也是存在特殊情况的,即可能直接从伊甸园区到达老年代等情况

  1. 对象创建判断Eden是否放下,如果能放下,就在Eden为对象分配内存,如果放不下,开始触发YGC
  2. YGC分为两步
  1. 垃圾回收S0/S1区,并将Eden区的不回收对象放到S0/S1区,此时也要进行判断,S0/S1是否能放下,如果能放下就放,放不下就晋升老年代
  2. 垃圾回收Eden区,后再次进行判断Eden是否能放下,如果能放下就放,放不下就晋升老年代
  1. 在晋升老年代时,也要进行判断,如果老年代放不下,就会触发FGC
  2. FGC后,再次判断老年代是否能放下,如果能放下就放,放不下就报异常OOM

图解



4.3.3 Minor GC, Major Gc和Full Gc

JVM的gc分为部分收集和整堆收集

  1. 部分收集
  1. MinorGC/YoungGC/YGC,只针对新生代(Eden/S0/S1)的垃圾回收
  2. MajorGC/OldGC,只针对老年代(OldGen)的垃圾回收 CMS GC
  3. MixedGC,混合收集只针对整个新生代以及部分老年代的垃圾回收 只有G1 GC有
  1. 整堆收集
  1. FullGC 对整个堆空间以及方法区进行垃圾回收


MinorGC:

  1. 当年轻代占用100%的时候会触发MinorGc,此处年轻代主要指的是Eden,并不是Survivor(S0/S1),也就是只有Eden区占用满的时候才会触发YGC垃圾回收,而Survivor并不会触发YGC
  2. Java对象大部分都是朝生夕死的对象,所以MinGc非常频繁
  3. MinorGC会触发STW,暂停其他用户线程,等垃圾回收完毕,才能恢复其他用户线程,但是MinorGC一般都比较快,并不会占用很多时间.

MajorGC:

  1. 当老年代占用100%的时候就会触发MajorGC
  2. 一般情况下MajorGC都会伴随着一次MinorGC(并非必然情况,有些垃圾回收器就会直接进行MajorGC),也就是说当回收老年代的对象时,一般都会回收一次新生代对象
  3. MajorGC速度要比MinorGC慢10倍以上,STW的时间也就会更长,所以要尽量避免MajorGC
  4. MajorGC之后如果空间还是不足,就会发生OOM

FullGC:

  1. 当出现一下几种情况时会调用FullGC
  1. System.gc()时
  2. 老年代空间不足
  1. MinorGC后进入老年代的内存>老年代可用内存
  2. Eden向S0/S1复制时,对象大小>to区的可用大小,即将其转入老年代,此时对象大小>老年代可用内存时,会触发FullGC
  1. 方法区空间不足
  1. 尽量避免FullGC


代码查看gc情况:

package com.zy.study08;


/**

* @Author: Zy

* @Date: 2021/8/26 17:30

* 测试打印 YGC FullGC

* 设置堆内存最大为10M -Xms10M -Xmx10M

* 打印gc详细信息 -XX:+PrintGCDetails

*/

public class GcTest {

   public static void main(String[] args) {

       int i = 0;

       String temp = "test";

       try {

           while (true){

               temp += temp;

               i++;

           }

       }catch (Throwable e){

           e.printStackTrace();

           System.out.println(i);

       }

   }

}


执行结果如图:

查看内存溢出情况,多次YGC后,会进行一次FullGC,最终OOM之前会执行一次FullGC,但是由于对象大小超过老年代可用大小,所以OOM



4.4 堆空间分代思想

分代主要为了将不同生命周期的对象区分开,便于GC回收,提高性能

新生代主要存放生命周期较短的对象,GC主要针对这块区域

老年代存放生命周期较长的对象,这块区域的GC就不会太频繁


4.5 内存分配策略

  1. 优先分配Eden区,即对象多直接放在新生代
  2. 大对象直接放到老年代,即新生代放不下的情况
  1. 因此要避免大对象的创建
  2. 避免大对象创建完后gc
  1. 长期存活的对象放到老年代 ,s0/s1区对象年龄超过阈值则放到老年代
  2. 动态对象年龄判断 如果s0/s1区内的相同年龄的对象的大小总和超过survivor区的一半大小,则将>=该年龄的对象都放入老年代
  3. 空间分配担保
  1. 参数 -XX:HandlePromotionFailure
  2. 当s0/s1内的对象超过年龄阈值时,能放入老年代,这个操作是需要老年代来为survivor区提供担保的

4.6 TLAB(Thread Local Allocation Buffer)

TLAB是堆内线程私有空间,正常来说堆是所有线程共享的,但是为了加快内存的分配速度,所以增加了TLAB机制来存放线程私有的对象.

TLAB是JVM在Eden区分配的一块线程私有的缓存区域,默认占用Eden区的1%大小,可以使用JVM参数-XX:修改

使用TLAB的作用:

jvm在并发环境下分配堆空间是有危险的,所以要通过加锁机制保证数据的有效性,有了TLAB后就会优先将TLAB作为内存分配的空间.



4.7 堆空间参数总结

oracle官网文档地址: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

-XX:printFlagsInitial

参数名

作用

-XX:+PrintFlagsInitial

查看所有参数初始默认值

-XX:+PrintFlagsFinal

查看所有参数最终值(可能会修改,显示修改后的值,不再是初始值)

-Xms

初始堆空间内存

-Xmx

最大堆空间内存

-Xmn

设置新生代的大小(初始值以及最大值)

-XX:NewRatio

配置新生代与老年代的比例 默认1:2

-XX:SurvivorRatio

设置新生代中Eden与Survivor的比例 默认8:1:1 (实际6:1:1)

-XX:MaxTenuringThreshold

设置新生代垃圾的最大年龄(阈值),超过此阈值进入老年代,默认15

-XX:+PrintGCDetails

输出详细gc日志

-XX:+PrintGC/-verbose:gc

打印简要gc日志

-XX:HandlePromotionFailure

是否设置空间分配担保

解释:

YGC之前,会先判断老年代可用空间是否大于要回收对象的总大小:

    大于:进行YGC/MinorGC

    小于:判断-XX:HandlePromotionFailure参数

         此参数为false:进行FullGC

         此参数为true:则判断此次回收对象大小是否大于历代对象晋升到老年代的平均大小

             大于: 进行YGC(冒险进行,有可能失败)

             小于: FullGC

上述操作在jdk7以前

jdk7以后此参数不在生效,只要老年代可用空间大于要回收对象的总小于 或者 此次回收对象大小是否大于历代对象晋升到老年代的平均大小,就可以进行YGC


4.8 堆是分配对象的唯一选择?

经过逃逸分析后的对象,如果对象没有逃逸出方法的话,就可以将对象进行栈上分配,分配到栈上的对象就不用考虑gc了,由此提高性能

TAOBAOJVM 创新了GCIH(GC invisible heap)技术实现off_heap,就是将生命周期较长的对象从heap中移到堆外,GC不用管理GCIH中的对象,达到降低gc频率,提高性能的效果

由上也可以看出对jvm的优化主要是针对gc,而对gc的优化主要是降低gc频率,提高gc效率


4.8.1 逃逸分析

逃逸分析是将对象分配到栈上要用到的技术

要想把对象分配到栈上,首先要保证对象不会发生逃逸:

逃逸分析: 只在方法内部使用的对象就被称为不会逃逸,在其他地方被引用的对象就被称之会发生逃逸

jdk6 之后就默认开启了逃逸分析


结论: 能使用方法局部变量 就不要使用全部变量,避免发生逃逸


4.8.2 代码优化

不会发生逃逸的对象就可以进行编译优化

  1. 栈上分配
  1. 逃逸分析后,就可以将没有发生逃逸的对象由堆分配转换为栈分配,由栈分配后就不存在gc问题,当方法执行完,方法出栈后,栈空间被回收,局部变量也被随之回收

package com.zy.study09;


import com.zy.study08.GcTest;


/**

* @Author: Zy

* @Date: 2021/8/30 20:20

* 逃逸分析后不会逃逸的对象就可以进行栈上分配

* 测试栈上分配

* -Xms1G -Xmx1G -XX:-DoEscapeAnalysis 开启/关闭逃逸分析 -XX:+PrintGCDetails

*/

public class StackAllocationTest {


   public static void testCreateClass(){

       GcTest gcTest = new GcTest();

   }



   public static void main(String[] args) throws InterruptedException {

       long startTime = System.currentTimeMillis();


       for (int i = 0; i < 10000000; i++) {

           testCreateClass();

       }


       long endTime = System.currentTimeMillis();


       System.out.println(endTime-startTime+"ms");


       // 线程sleep

       Thread.sleep(1000000);

   }

}


可以看到,未开启逃逸分析的情况下,创建的1千万个对象都在堆中:

而开启了逃逸分析之后:

执行效率大幅度提升,并且没有那么多的对象存放在堆中了

  1. 同步省略: 如果一个对象被发现智能被一个线程访问到,那么这个对象就可以考虑不用同步操作了.
  1. 即如果一个加锁的对象被发现其实只有一个线程能访问,那么会在运行时jvm会将这个锁去掉,所以同步省略也叫锁消除
  1. 分离对象或标量替换: 有的对象如果被发现不需要作为一个连续的内存结构存储也可以被访问到,那么这个对象的全部/部分就可以不存储在内存中,而是存储在cpu寄存器(对java而言,是栈,因为java是基于栈的)中;
  1. 标量: 基本数据类型
  2. 聚合量: 引用数据类型
  3. 标量替换: 将聚合量替换为标量,如果一个对象不发生逃逸,那么就可以将这个对象替换成标量,存储在栈帧的局部变量表中,减少堆内存的占用
  4. 开启jvm参数: -XX:+EliminateAllocations

package com.zy.study09;


/**

* @Author: Zy

* @Date: 2021/8/30 21:00

* 标量替换测试

* jvm参数 -Xms256M -Xmx256M -XX:+PrintGCDetails -XX:-EliminateAllocations 开启/关闭标量替换

*/

public class ScalarReplaceTest {


   static class Point{

       int x;

       int y;


       public Point(int x, int y) {

           this.x = x;

           this.y = y;

       }

   }


   public static void main(String[] args) throws InterruptedException {

       long startTime = System.currentTimeMillis();


       for (int i = 0; i < 10000000; i++) {

           allocation();

       }


       long endTime = System.currentTimeMillis();


       System.out.println(endTime-startTime+"ms");


       // 线程sleep

       Thread.sleep(1000000);

   }


   public static void allocation(){

       Point point = new Point(2,3);

       int a = point.x + point.y;

   }

}


开启/关闭标量替换的效果显著

目录
相关文章
|
2月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
113 4
|
2月前
|
存储 Java Linux
32 位和 64 位 JVM 的最大堆大小是多少?
【8月更文挑战第22天】
115 0
|
2月前
|
存储 Java 程序员
JVM自动内存管理之运行时内存区
这篇文章详细解释了JVM运行时数据区的各个组成部分及其作用,有助于理解Java程序运行时的内存布局和管理机制。
JVM自动内存管理之运行时内存区
|
2月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
19 3
|
2月前
|
消息中间件 设计模式 安全
多线程魔法:揭秘一个JVM中如何同时运行多个消费者
【8月更文挑战第22天】在Java虚拟机(JVM)中探索多消费者模式,此模式解耦生产与消费过程,提升系统性能。通过`ExecutorService`和`BlockingQueue`构建含2个生产者及4个消费者的系统,实现实时消息处理。多消费者模式虽增强处理能力,但也引入线程安全与资源竞争等挑战,需谨慎设计以确保高效稳定运行。
69 2
|
2月前
|
存储 Java
JVM中的堆
这篇文章详细介绍了JVM中的堆内存,包括堆的核心概念、内存细分、堆空间大小设置以及Java 7和8版本堆内存逻辑上的不同划分。
JVM中的堆
|
2月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
64 0
|
2月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
6天前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
18 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
2月前
|
存储 算法 Oracle
不好意思!耽误你的十分钟,JVM内存布局还给你
先赞后看,南哥助你Java进阶一大半在2006年加州旧金山的JavaOne大会上,一个由顶级Java开发者组成的周年性研讨会,公司突然宣布将开放Java的源代码。于是,下一年顶级项目OpenJDK诞生。Java生态发展被打开了新的大门,Java 7的G1垃圾回收器、Java 8的Lambda表达式和流API…大家好,我是南哥。一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
不好意思!耽误你的十分钟,JVM内存布局还给你