堆的内存划分以及GC回收

堆的内存划分

Java堆的内存划分如图所示,分别为年轻代、Old Memory(老年代)、Perm(永久代)。在jdk1.8中,永久代被移除,使用MetaSpace代替。
在这里插入图片描述

新生代

  1. 使用复制清除算法(Copying算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survicor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
    • 分为Eden、Survivor From、Survivor To,比例默认为8:1:1
    • 内存不足时发声Minor GC
  2. 新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代的区间位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。
  3. 再执行机制上,JVM提供了串行GC(SerialGC)、并行回收GC(ParrellScavenge)和并行GC(ParNew)
    • 串行GC:在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小和暂停时间要求不是很高的应用上,是clieng级别的默认的GC方式,可以通过:-XX:+UseSerialGC来强制指定。
    • 并行回收GC:在整个扫描和复制的过程采用多线程的方式进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可以用:-XX:ParalleGCThreads=4来指定线程数。
    • 并行GC:与老年代的并发GC配合使用。

老年代

老年代对象存活时间比较长、比较稳定。
4. 采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。
所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。
5. 在执行机制上JVM提供了串行GC(Serial MSC)、并行GC(Parallel MSC)和并发GC(CMS)
- 串行GC(SerialGC):client模式下的默认的GC方式。可以通过-XX:+UseSerialGC强制指定,每次进行全部回收,进行Compact,非常耗费时间。
- 并行GC(Paralle GC)吞吐量大,但是GC的时候响应很慢:server模式下的默认GC方式,也可用-XX:+UseParallelGC=强制指定。可以在选项后加等号来制定并行的线程数。
- 并发GC(CMS)(响应比并行gc快很多,但是牺牲了一定的吞吐量):使用CMS是为了减少GC执行时的停顿时间,垃圾回收线程和应用线程同时执行,可以使用-XX:+UseConcMarkSweepGC=指定使用,后边接等号指定并发线程数。CMS每次回收只停顿很短的时间,分别在开始的时候(Initial Marking),和中间(Final Marking)的时候,第二次时间略长。CMS一个比较大的问题是碎片和浮动垃圾问题(Floating Gabage)。碎片是由于CMS默认不对内存进行Compact所致,可以通过-XX:+UseCMSCompactAtFullCollection。
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor区,并将对象念经设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当他的年龄增加到一定程度(默认为15)时,就会被晋升到老年代中。对象晋升老年代的阈值,可以通过参数-XX:MaxTenuringThreshold来设置

永久代

Perm:用来存储类的元数据,也就是方法区。
6. Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代经常内存不够用,或者发生内存泄漏;
7. MetaSpace(元空间):元空间的本质和永久代类似,多是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间并不在虚拟机中,而是使用本次内存。

堆内存的划分在JVM中的示意图

在这里插入图片描述

GC垃圾回收

  1. 判断对象是否要回收的方法:可达性分析法(GC Roots Tracing)
  • GC Roots Tracing:以一系列叫“GC Roots”的对象为起点开始向下搜索,走过的路径称为引用链(Reference Chain),当一个对象没有和任何引用链相连时,证明此对象是不可用的,用图论的说法是不可达的。那么它就会被判定为是可回收的对象。。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以回复,GC不会回收他的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)
  • 以下对象会被认为是root对象:
    • 虚拟机栈(栈帧中本地变量表)中引用的对象
    • 方法区中静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中Native方法引用的对象
      -对象被判定可被回收,需要经历两个阶段:
    • 阶段1: 第一个阶段是可达性分析,分析该对象是否可达
    • 阶段2:第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会)
  • 方法区中的垃圾回收
    • 常量池中一些常量、符号引用没有被引用,则会被清理出常量池
    • 无用的类:被判定为无用的类,会被清理出方法区。判定方法如下:
      • 该类的所有实例被回收
      • 加载该类的ClassLoader被回收
      • 该类的Class对象没有被引用
  • finalize():
    • GC垃圾回收要回收一个对象的时候,调用该对象的finalize()方法。然后在下一次垃圾回收的时候,才去回收这个对象的内存。
    • 可以在该方法里面,指定一些对象在释放前必须执行的操作。

发现虚拟机频繁full GC时应该怎么办

full GC指的是清理整个堆空间,包括年轻代和永久代

  • 首先用命令查看触发GC的原因是什么jstat –gccause 进程id
  • 如果是System.gc(),则看下代码哪里调用了这个方法
  • 如果是heap inspection(内存检查),可能是哪里执行jmap –histo[:live]命令
  • 如果是GC locker,可能是程序依赖的JNI库的原因

常见的垃圾回收算法

  • Copying(复制清除算法):
    -思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。
    -优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。
  • Mark-Sweep(标记-清除算法):
    • 思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
    • 优缺点:实现简单,容易产生内存碎片
  • Mark-Compact(标记-整理算法):
    • 思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。
    • 优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下
  • 分代收集算法:(目前大部分JVM的垃圾收集器所采用的算法):
    思想:把堆分成新生代和老年代。(永久代指的是方法区)
    • 因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
    • 由于老年代每次只回收少量的对象,因此采用mark-compact算法。
    • 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量

GC使用时对程序的影响

垃圾回收会影响程序的性能,Java虚拟机必须要追踪运行程序中的有用对象,然后释放没用对象,这个过程消耗处理器时间

几种不同的垃圾回收类型

  • Minor GC:从年轻代(包括Eden、Survivor区)回收内存。
    A、当JVM无法为一个新的对象分配内存的时候,越容易触发Minor GC。所以分配率越高,内存越来越少,越频繁执行Minor GC
    执行Minor GC操作的时候,不会影响到永久代(Tenured)。从永久代到年轻代的引用,被当成GC Roots,从年轻代到老年代的引用在标记阶段直接被忽略掉。
  • Major GC:清理整个老年代,当eden区内存不足时触发。
  • Full GC:清理整个堆空间,包括年轻代和老年代。当老年代内存不足时触发

JVM优化

  • 一般来说,当survivor区不够大或者占用量达到50%,就会把一些对象放到老年区。通过设置合理的eden区,survivor区及使用率,可以将年轻对象保存在年轻代,从而避免full GC,使用-Xmn设置年轻代的大小
  • 对于占用内存比较多的大对象,一般会选择在老年代分配内存。如果在年轻代给大对象分配内存,年轻代内存不够了,就要在eden区移动大量对象到老年代,然后这些移动的对象可能很快消亡,因此导致full GC。通过设置参数:-XX:PetenureSizeThreshold=1000000,单位为B,标明对象大小超过1M时,在老年代(tenured)分配内存空间。
  • 一般情况下,年轻对象放在eden区,当第一次GC后,如果对象还存活,放到survivor区,此后,每GC一次,年龄增加1,当对象的年龄达到阈值,就被放到tenured老年区。这个阈值可以通过-XX:MaxTenuringThreshold设置,默认为15。如果想让对象留在年轻代,可以设置比较大的阈值。
    -设置最小堆和最大堆:-Xmx和-Xms稳定的堆大小堆垃圾回收是有利的,获得一个稳定的堆大小的方法是设置-Xms和-Xmx的值一样,即最大堆和最小堆一样,如果这样子设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少GC次数,因此,很多服务端都会将这两个参数设置为一样的数值。稳定的堆大小虽然减少GC次数,但是增加每次GC的时间,因为每次GC要把堆的大小维持在一个区间内。
  • 一个不稳定的堆并非毫无用处。在系统不需要使用大内存的时候,压缩堆空间,使得GC每次应对一个较小的堆空间,加快单次GC次数。基于这种考虑,JVM提供两个参数,用于压缩和扩展堆空间。一个不稳定的堆并非毫无用处。在系统不需要使用大内存的时候,压缩堆空间,使得GC每次应对一个较小的堆空间,加快单次GC次数。基于这种考虑,JVM提供两个参数,用于压缩和扩展堆空间。
    • -XX:MinHeapFreeRatio参数用于设置堆空间的最小空闲比率。默认值是40,当堆空间的空闲内存比率小于40,JVM便会扩展堆空间
    • -XX:MaxHeapFreeRatio 参数用于设置堆空间的最大空闲比率。默认值是70, 当堆空间的空闲内存比率大于70,JVM便会压缩堆空间。
    • 当-Xmx和-Xmx相等时,上面两个参数无效
  • 通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。
    --XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能的减少垃圾回收时间。
    • -XX:+UseParallelOldGC:设置老年代使用并行垃圾回收收集器。
  • 尝试使用大的内存分页:使用大的内存分页增加CPU的内存寻址能力,从而提升系统的性能。-XX:+LargePageSizeInBytes 设置内存页的大小
  • 使用非占用的垃圾收集器。-XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停顿。
  • -XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden = 2:3
  • JVM性能调优的工具:
    • jps(Java Process Status):输出jvm中运行的进程状态信息(jconsole)
    • jstack:查看java进程内的线程的堆栈信息。示例jstack 169885169885是pid
    • jmap:用于生成堆转存快照。
    • jhat:用于分析jmap生成的堆转存快照(一般不推荐使用)
    • jstat是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。
    • VisualVM:故障处理工具
      IDEA中可以在terminal中执行jvisaulvm查看程序运行状况

如何判断对象是否可回收

引用计数

引用计数法的逻辑非常简单,但是存在问题,java并不采用这种方式进行对象存活判断。
引用计数法的逻辑是:在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++。如果一个引用关系失效则counter–。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态。
使用此方式效率确实很高,但是有个致命的缺点,无法解决循环引用的问,例如下面这段代码:

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
        object1.object = object2;
        object2.object = object1;
        object1 = null;
        object2 = null;
    }
}
 
class MyObject{
    public Object object = null;
}

看到将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。因此在Java中并没有采用这种方式。

正向可达

为了解决循环引用的问题,Java采取了正向可达的方式,主要是通过Roots对象作为起点进行搜索,搜索走过的路程称为引用链(Reference Chain),当一个对象到Roots没有任何引用链相连时,证明此对象不可用,当然被判定为不可达对象不一定就会称为回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了,能否被回收其实主要还是要看finalize()方法有没有与引用链上的对象关联,如果在finalize()方法中有关联则自救成功,改对象不可被回收,反之如果没有关联则成功被二次标记成功,就可以称为要被回收的垃圾了。
正向可达解决循环引用示意图:
在这里插入图片描述
object5、6、7虽然引用计数都不为0,但是到Roots对象是不可达的,最终还是会被判定为可回收对象。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

诗织_王大大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值