JVM从零开始(二) -垃圾回收机制以及内存分代模型

本文深入探讨JVM中的垃圾回收机制,包括可达性算法、内存相关参数设置,以及年轻代、老年代、永久代的内存分配模型。通过具体案例,分析了不同垃圾回收算法的应用场景,以及如何通过调整内存分配比例实现JVM优化。
摘要由CSDN通过智能技术生成

JVM中垃圾回收的判定标准

最终目的是将内存中无用的对象回收掉。具体的判定方法有:

  • 引用计数法,不采用,指的是维护对象被引用的次数,次数为0则意味着是垃圾。
  • 可达性算法-GC Roots tracing,指的是从GC Roots开始往下遍历所有引用的对象,(每个GC Root就是一个树状图),所有被引用到的对象就是需要存活的对象,其他对象可以被回收。GC Root指的是,虚拟机栈(栈帧中的本地变量表)中引用的对象,方法区中非基本类型的类静态变量(一个地址)所引用的对象,本地方法栈中JNI(即一般说的Native方法)引用的对象。

JVM中内存相关参数

  • -Xms Java堆内存初始大小
  • -Xmx Java堆内存最大大小
  • -Xmn Java堆内存中的新生代大小,扣除它就是老年代大小
  • -XX:PermSize(1.8之后:-XX:MetaspaceSize) 永久代初始大小
  • -XX:MaxPerSize(1.8之后:-XX:MaxMetaspaceSize) 永久代最大大小
  • -Xss 每个线程的栈内存大小

注:通常情况下,Xms和Xmx,-XX:PermSize和-XX:MaxPerSize都会设置为一样。

  • -XX:MaxTenuringThreshold 多少岁进入老年代-默认15
  • -XX:PretenureSizeThreshold 超过多少字节的大对象直接进入老年代
  • -XX:HandlePromotionFailure MinorGC时,如果老年代剩余空间小于新生代对象总大小,但是如果大于之前平均进入老年代对象的大小,是否尝试进行MinorGC(默认开启)
  • -XX:SurvivorRatio=8 Eden区的比例
  • -XX:+UseParNewGC 指定使用ParNewGC垃圾回收器
  • -XX:ParallelGCThreads,指定ParNewGC线程数量,一般不指定ParNewGC会自动根据CPU核数适配

上面看不懂的参数不要深究,等下提到回过头再来看,这里只是将所有参数罗列出来方便查找。

JVM中的内存分代模型

JVM中,将对象在内存中分为了三代:

  • 年轻代:很快被回收的对象,存在于堆,具体还在内存中分为了1个eden区,和2个survivor区。
  • 老年代:长期存在的对象,存在于堆
  • 永久代:指的就是方法区(存放Class元数据),回收条件较苛刻,需满足:该类所有实例对象所有已经从堆内存被回收,该类classLoader已经被回收,该类Class对象没有任何引用

为什么要分代勒,因为针对每个年龄代,都有不同的垃圾回收算法,以及内存分配机制。如果将所有对象放在一起,第一是会造成频繁遍历判断回收的开销,第二是会造成复制、移动的开销,为什么会有复制、移动,因为回收内存必然会造成内存碎片,而内存碎片会导致空间浪费,所以必须通过复制、移动来清理随便,使得空闲内存连续。

JVM中具体的内存分配模型

如上图,至于年轻代为什么要如此分配,与特定的回收算法有关。

对象在内存分代中如何流转

年轻代

大部分对象刚创建的时候都会分配在年轻代的Eden区,只要年轻代空间不够就会触发MinorGC(只回收年轻代内存),minorGC采取复制算法进行回收,当JVM运行触发第一轮minorGC时,会将eden区存活的对象先复制到一个suprivor区。然后删除eden区对象,当触发下一轮minorGC时,又把suprivor区和eden区的存活的对象转移到另一个suprivor区,然后删除这两个区的所有对象。依次类推。至于为什么要用复制算法,包括老年代的标记整理算法,这是考虑到了避免内存碎片。如果对象内存不连续,会造成很多的空间浪费。

老年代

老年代的对象都是从年轻代根据一定的规则流转过来的。 具体有几类流转方式:

  • 超过指定年龄(参数-XX:MaxTenuringThreshold 配置,默认15),这里年龄指的是没有被垃圾回收,存活下来一次理解为增加一岁。流转到老年代。

  • 大对象直接进入,超过参数指定字节数(-XX:PretenureSizeThreshold)设置的字节数的大对象会直接进入老年代,这是因为对象越大,复制开销就越大。

  • 动态年龄判断规则进入,意思是不一定要到指定年龄再流转到15,如果某一年龄以上的对象到达一定大小,也会提前进入老年代。当躲过一轮GC的对象加起来超过surrvivor区50%,如年龄1+年龄2+年龄n一直累加,直到年龄n的时候发现加起来超过了surrvivor空间的50%,则年龄n以上的对象直接进入老年代

  • minorGC发生时,suprivor区放不下,超过的部分转移到老年代。这里涉及一个老年代分配担保规则,指的是每次MinorGC发生时,都会判断老年代可用内存大不大于,年轻代存活对象内存之和,如果大于则直接进行minorGC,如果小于则要看参数XX:HandlePromotionFailure是否启用(默认启用),如果启用则对老年代这次需要承载的转移对象内存进行预估(取前面minorGC被转移的平均内存大小),若大于则也进行MinorGC,若意料状况外转移内存超出了老年代可用空间,则进行FullGC,若fullGC还是不够,则抛出OOM错误。FullGC是采取的标记整理算法,指的是移动存活对象,让内存连续,然后删除需要回收的对象,为什么使用标记整理?因为认为老年代对象存活几率高,复制算法不划算。

永久代

永久代存放的是元数据信息,当类加载时,类元数据信息写入永久代,fullGC时永久代数据被回收,回收条件是:该类所有实例对象所有已经从堆内存被回收,该类classLoader已经被回收,该类Class对象没有任何引用。

附图:

谈一个JVM优化实例

现一个日处理量上亿数据的计算系统,不断从Mysql和其他数据中间件中提取数据进行计算处理。每分钟执行500次数据提取和计算任务,每次任务处理耗时10秒,每次处理1万条数据(每条数据20个字段),但是集群部署,共5台机器,1台机器每分钟处理100次任务,每台机器是4核8G内配置,JVM分了4G,3G堆内存,1.5G年轻代,1.5G老年代。

我们先来估算一下内存占用:

  • 每条数据20个字段,可以估算一条数据为1KB大小左右
  • 每次计算1W条,那么一个任务占用内存就是1KB*1W=10MB数据左右,一台机器每分钟处理100次任务,暂用内存约为1G左右,基本上一分钟多点后Eden区就被占满了。

实际生产环境是怎么样的勒?

  • 一分钟之后的第一次GC,此时的内存情况:

每个任务处理10S,意味着还有大概六分之一的数据应该存活,算200M,但是200M放不进Survivor区,所以会尝试往老年代放,老年代现在大于1.2G所以直接放就是了。

  • 每次MinorGC都会有大概200M进入老年代,当进行到第三次时

此时老年代可用容量小于年轻代对象总内存,默认判断老年代剩余空间是否大于平均每次MinorGC转移过来的老年代对象容量,这里是大于,所以还是继续MInorGC。

  • 当进行到第八次时

此时会触发FullGC清理老年代,于是老年代的对象被全部清理掉了:

  • 然后继续回到原来的第一次,每8次进行一次FullGC,也就是8分钟进行一次

优化策略

  • 重新调整新生代老年代比例,扩大新生代内存为2GB,老年代1GB

此时一个Survivor区有200MB,每次MinorGC后都能存放的下存活对象,不用往老年代转移(当然还是有转移,这只是避免了suprivor区过小被迫转移的对象)。JVM优化的策略最核心的就是减少FullGC次数,因为扫描对象多了一个老年代和永久代、永久代标记算法略微复杂、老年代整理时由于对象较多比较慢的原因,FullGC效率是远远低于MinorGC的。

转载于:https://my.oschina.net/u/4101481/blog/3101091

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值