JVM学习-堆空间(三)

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混淆使用,需要具体分辨是老年代回收还是整堆回收
    • 混合收集:收集整个新生代和部分老年代的垃圾收集
      • 目前,只有G1 GC有这种行为
  • 整堆收集:收集整个java堆和方法区的垃圾收集
年轻代GC(Minor GC)触发机制
  • 当年轻代空间不足时,就会触发Minor GC,年轻代满指的是Eden区满,Survior满不会引发GC(每次Minor GC会清理年轻代的内存)
  • 因为Java对象大多具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度比较快
  • Minor GC会引发STW(Stop The World),暂停其它用户线程,等垃圾回收结束,用户线程才恢复执行
老年代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触发机制
  • 调用System.gc()时,系统建议执行Full GC,不必须执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区、survivor space0(From space)区向survivor space1(To space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
    注:Full GC是开发或调优中尽量避免的
java 复制代码
/**
 * Administrator
 * 2024/5/18
 * 测试Minor GC,Major GC,Full GC
 * 执行参数 -Xms512m -Xmx512m -XX:+PrintGCDetails
 */
public class GCTest {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String str = "lotus.com";
            while (true) {
                list.add(str);
                str += str;
                i++;
            }
        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println("遍历次数:" + i);
        }
    }
}

//执行结果
[GC (Allocation Failure) [PSYoungGen: 117623K->20420K(153088K)] 117623K->74572K(502784K), 0.0395143 secs] [Times: user=0.03 sys=0.03, real=0.05 secs] 
[GC (Allocation Failure) [PSYoungGen: 133555K->1908K(153088K)] 408891K->314108K(502784K), 0.0160892 secs] [Times: user=0.08 sys=0.00, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1908K->0K(153088K)] [ParOldGen: 312200K->221844K(349696K)] 314108K->221844K(502784K), [Metaspace: 3492K->3492K(1056768K)], 0.0218373 secs] [Times: user=0.16 sys=0.00, real=0.02 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(153088K)] 221844K->221844K(502784K), 0.0010237 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(153088K)] [ParOldGen: 221844K->221826K(349696K)] 221844K->221826K(502784K), [Metaspace: 3492K->3492K(1056768K)], 0.0046937 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
遍历次数:22
Heap
 PSYoungGen      total 153088K, used 6522K [0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
  eden space 131584K, 4% used [0x00000000f5580000,0x00000000f5bde8c0,0x00000000fd600000)
  from space 21504K, 0% used [0x00000000fd600000,0x00000000fd600000,0x00000000feb00000)
  to   space 21504K, 0% used [0x00000000feb00000,0x00000000feb00000,0x0000000100000000)
 ParOldGen       total 349696K, used 221826K [0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
  object space 349696K, 63% used [0x00000000e0000000,0x00000000ed8a08f0,0x00000000f5580000)
 Metaspace       used 3525K, capacity 4502K, committed 4864K, reserved 1056768K
  class space    used 391K, capacity 394K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at com.chapter06.GCTest.main(GCTest.java:19)
堆空间分代思想

经研究,不同对象的生命周期不同,70%-99%对象是临时对象

  • 新生代:有Eden、两块大小相同的Survivor(又称为from/to,s0/s1)构成,to总为空
  • 老年代:存放新生代中经历多次GC仍然存活的对象

为什么需要把Java堆分代?不分代不能正常工作吗?
  • 其实不分代完全可以,分代的唯一理由就是优化GC性能,如果没有分代,那所有的对象都在一块,就如同把学校的所有人关在一个教室,GC的时候要找到哪些对象没用,这样就会对全堆所有区域进行扫描,而很多对象朝生夕死,如果分代的话,新创建的对象放在某一地方,当GC的时候先把这块存储"朝生夕死"对象的区域进行回收,就会腾出很大空间
内存分配策略

如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survior容纳的话,将被移动到Survivor空间中,并将对象年龄设置为1,对象在Survivor区中每熬过一次MinorGC,年龄就加1岁,当它的年龄增加到一定程度(默认15岁,其实每个JVM,每个GC都有所不同)时,会被晋升到老年代中

  • 对象晋升老年代的年龄阈值,可以通过选项 -XX:MaxTenuringThreshold来设置
    针对不同年龄对象分配原则如下:
  • 优先分配到Eden
  • 大对象直接分配到老年代
    • 尽量避免程序中出现过多大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
  • 空间分配担保
    • -XX:HandlePromotionFailure
java 复制代码
//测试大对象直接进入老年代
/**
 * Administrator
 * 2024/5/18
 * -Xms60m -Xmx60m -XX:+PrintGCDetails -XX:NewRatio=2 -XX:SurvivorRatio=8
 */
public class YoungOldAreaTest {
    public static void main(String[] args) {
        byte[] buffer = new byte[1024*1024*20];
    }
}
//执行结果
Heap
 PSYoungGen      total 18432K, used 2624K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
  eden space 16384K, 16% used [0x00000000fec00000,0x00000000fee90218,0x00000000ffc00000)
  from space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)
  to   space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000)
 ParOldGen       total 40960K, used 20480K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)
  object space 40960K, 50% used [0x00000000fc400000,0x00000000fd800010,0x00000000fec00000)
 Metaspace       used 3496K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K
对象分配过程(TLAB)
  • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
  • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略
  • 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计
为什么要有TLAB(Thread Local Allocation Buffer)
  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区划分内存空间是线程不安全的
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
  • 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
  • 在程序中,开发人员可以通过选项"-XX:UseTLAB"设置是否开启TLAB空间
  • 默认情况下,TLAB空间内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项"-XX:TLABWasteTargetPercent"设置TLAB空间所占用Eden空间的百分比大小
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制 确保数据操作的原子性,从而直接在Eden空间中分配内存

堆空间参数
  • -XX:+PrintFlagsInitial:查看所有参数的默认初始值
  • -XX:+PrintFlagsFinal:查看所有的参数的最终值(可能存在修改,不再是初始值)
  • -Xms:初始堆空间大小(默认为物理内存的1/64)
  • -Xmx:最大堆空间大小(默认为物理内存的1/4)
  • -Xmn:设置新生代的大小(初始值及最大值)
  • -XX:NewRatio:配置新生代与老年代在堆结构的占比
  • -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
  • -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
  • -XX:+PrintGCDetails:输出详细的GC处理日志
    • 打印gc简要信息:-XX:+PrintGC | -verbose:gc
  • -XX:HandlePromotionFailure:是否设置空间分配担保
相关推荐
曙曙学编程几秒前
初级数据结构——树
android·java·数据结构
BestandW1shEs6 分钟前
彻底理解消息队列的作用及如何选择
java·kafka·rabbitmq·rocketmq
爱吃烤鸡翅的酸菜鱼8 分钟前
Java算法OJ(8)随机选择算法
java·数据结构·算法·排序算法
码蜂窝编程官方12 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
Viktor_Ye28 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm30 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
阿伟*rui32 分钟前
jvm入门
jvm
一二小选手34 分钟前
【Maven】IDEA创建Maven项目 Maven配置
java·maven
J老熊40 分钟前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
猿java1 小时前
什么是 Hystrix?它的工作原理是什么?
java·微服务·面试