JVM调优二:对象可达性分析及内存分配机制

JVM调优二:对象可达性分析及内存分配机制

上一篇文章简单介绍了JVM内存的运行模型,了解了JVM内存的基本结构,以及各个部分的基本作用。本篇文章将介绍JVM主要的垃圾回收算法以及JVM的内存分配机制。

一、如何判断对象是否需要被回收

在了解JVM垃圾回收算法前,要先了解JVM如何判断一个对象已经成为垃圾对象,需要回收,主要分为引用计数法;可达性分析算法两种。

1、引用计数法

顾名思义,引用计数法是给对象添加一个引用计数器,每当一个地方引用该对象,计数器就加1;当取消引用时,计数器就减1。当对象的计数器为0时,意味着该对象不再被引用,可以被回收。

计数法虽然简单高效,但是目前主流的虚拟机并没有使用该方法,主要原因是引用计数法很难解决对象之间循环引用的问题。如以下代码所示:

public class ClassA {
    private ClassB attrB;
    //setter and getter
}

public class ClassB {
    private ClassA attrA;
		//setter and getter
}

ClassA classA = new ClassA();
ClassB classB = new ClassB();
classA.setAttrB(classB);
classB.setAttrA(classA);

2、GC Roots(可达性分析算法)

可达性分析算法的基本思路就是以一系列“GC Roots”的对象为起点,从这些节点开始向下搜索,找到的对象(被Roots直接引用或间接引用)都标记为非垃圾对象,其余未标记的对象都认定为垃圾对象,即没有被GC Roots直接引用或间接引用的对象,都认定为垃圾对象。

GC Roots的根节点:线程栈的本地变量、静态变量、本地方法栈的变量等。

在这里插入图片描述

3、如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? 类需要同时满足下面3个条件才能算是 “无用的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何 实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地 方通过反射访问该类的方法。

4、引用类型

除了判断对象是否已经是垃圾对象的算法,我们在声明对象时所使用的引用类型,也将影响到对象的回收。

java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用。

强引用

我们最常使用的引用类型,比如:

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收

强引用的对象我们可以直接访问;

强引用所指向的对象在任何时候都不会被系统回收(我们可以通过人为将指针指向NULL来让JVM尽早的回收无用的对象);

因为JVM不会回收被强引用的对象,所以强引用会导致OOM;

软引用

将对象用SoftReference软引用类型的对象包裹,正常情况不会被回 收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。如果回收了软引用的对象之后仍然没有足够的空间,才会抛出OOM。

public static SoftReference<Object>user=newSoftReference<Object>(new Object());

因为软引用不会妨碍垃圾回收的特性,所以经常使用软引用来构建缓存,可以有效防止因为缓存内容过大造成的OOM。

弱引用

弱引用比软引用要弱一些,在系统GC时,只要发现软引用,无论剩余空间是否充足,都会回收软引用的对象。

public static WeakReference<Object>user=newWeakReference<Object>(new Object());

因为弱引用只要GC时就会回收,所以在系统内的存活时间会明显短于软引用,所以一般用来缓存一些无关紧要但是又能提升系统性能的数据。

虚引用

虚引用也称为幽灵引用或者幻影引用,是java所有引用类型中最弱的一个,随时可能被回收几乎不使用。

ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference sf = new PhantomReference<>(obj,referenceQueue);

我们发现虚引用的构造函数多出一个参数(referenceQueue),因为虚引用的get()方法实现如下:

		public T get() {
        return null;
    }

所以虚引用无论何种情况,调用get()方法,获取到的引用对象都是null。虚引用必须和引用队列(ReferenceQueue)配合使用,本文不做详细讨论。

二、JVM内存分配机制

1、JVM垃圾回收

在了解JVM内存分配机制之前,先来了解一下JVM的垃圾回收流程。

在这里插入图片描述

大多数情况下,新建的对象在Young Generation的Eden区被分配。当Eden区没有足够空间分配时,JVM将发起一次Minor GC(Eden区和Survivor区内From的对象复制到To,下次Minor GC时,From和To的身份会互换)。

对象每经历过一次Minor GC,对象的对象年龄会加1(对象经历过Minor GC后仍存活的情况下),当它的年龄增加到一定程度(默认为15),就会被晋升到Old Generation中。

当Old Generation中空间不足时,则会触发Full GC。

对象从Young Generation晋升到Old Generation的晋升年龄阈值,可以通过设置JVM参数**-XX:MaxTenuringThreshold**来设置,该参数默认为15。

虽然JVM默认帮我们将PretenureSizeThreshold设置为15,大多数情况下我们都不需要修改该参数,但是如果我们的某个业务子模块是以类似于缓存功能为主的模块,我们可以适当调小该参数,使得我们的缓存内容提前进入老年代,减少我们已经确定会长期使用的对象,因为多经历了几轮Minor GC而造成性能损耗。

补充说明:

  • Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。

  • Major GC/Full GC:一般会回收老年代,年轻代,方法区的垃圾, Major GC的速度一般会比Minor GC的慢10倍以上。

  • 年轻代与老年代默认2:1。可以通过参数**-XX:NewRatio=4**设置年轻代与老年代的比例,如果设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

  • Eden与Survivor区默认8:1:1 ,JVM参数**-XX:+UseAdaptiveSizePolicy**,会导致这个比例自动变化,如果不想这个比例有变化可以设置参数**-XX:-UseAdaptiveSizePolicy** 。

    -XX:SurvivorRatio=8参数可以设置年轻代中Eden区与Survivor区的比例,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

2、大对象直接进入老年代

当我们创建的对象是一个需要大量连续存储空间的对象时,JVM为了避免大对象分配内存、垃圾回收以及分代晋升(Young晋升到Old)等操作时,因为数据复制造成的效率降低问题,会直接将大对象分配到Old Generation。常见的大对象有字符串、数组等。

我们可以通过JVM参数 -XX:PretenureSizeThreshold,来设置大对象判定的阈值。

3、对象动态年龄判断机制

当Survivor区域内,From区域内一批对象的总大小大于这块区域内存大小的50%,那么此时年龄大于等于这批对象最大年龄的对象,都将直接进入老年代。

例如:

age1+age2+ …… +agen的总大小超过Survivor区域的50%(From或者To区域的50%),则对象年龄大于等于n的对象,即年龄等于n以及年龄大于n的对象都将直接进入老年代。

因为动态年龄判断机制的存在,使得我们不能无脑的增加Eden区的大小,要根据业务情况适当调整Eden和Survivor的比例。

例如:

假设我们的Eden区域为100MB,假设每次进行Minor GC大概有50%的对象为垃圾对象,即每次Minor GC后,Eden区产生50MB对象。这时我们要保证Survivor的From和To的大小要大于100MB。否则,因为动态年龄判断机制的存在,会导致每次Minor GC后都将触发对象动态年龄判断机制,导致所有年龄大于等于1的对象都直接进入老年代。老年代空间不足后就会触发Full GC,造成频繁的STW(Stop the world)。

4、老年代空间担保机制

顾名思义,老年代空间担保机制是为了保证每次GC后,老年代都有足够的空间存放进入老年代的对象的机制。

老年代空间担保机制流程如下图所示:

在这里插入图片描述

  1. Eden空间不足,准备出发Minor GC;
  2. 判断老年代剩余可用空间是否小于年轻代所有对象占用总空间(Eden和Survivor区域所有对象,包括垃圾对象);
  3. 如果老年代剩余可用空间大于年轻代所有对象占用总空间,则直接进行Minor GC
  4. 如果老年代剩余可用空间小于年轻代所有对象占用总空间,则判断JVM是否配置参数**-XX:-HandlePromotionFailure**;
  5. 未配置**-XX:-HandlePromotionFailure**参数,则触发Full GC;
  6. 配置**-XX:-HandlePromotionFailure**参数,则判断老年代剩余可用空间是否小于JVM历史每次Minor GC后进入老年代的对象的平均大小;
  7. 如果老年代剩余可用空间小于历史每次Minor GC后进入老年代的对象平均大小,则进行Full GC,否则则进行Minor GC。

注意:

如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老 年代可用空间,那么也会触发full gc,full gc完之后如果还是没用空间放minor gc之后的存活对象,则也会发生“OOM” 。

三、总结

由于FUll GC与Minor GC巨大的性能差异,所以我们在做JVM优化时第一目标便是尽可能的减少Full GC的次数,可以通过调整参数**-XX:NewRatio**(设置年轻代与老年代的比例)和**-XX:SurvivorRatio**(设置年轻代中Eden区与Survivor区的比例),让eden区尽量的大,survivor区够用即可

又因为JVM动态年龄判断机制的存在,导致我们不能无脑的增大Eden区域的大小,要根据业务情况,估算每次Minor GC后存活下来的对象大小,合理的设置Eden与Survivor的比例,避免Minor GC后过多的对象进入老年代。

同时根据业务需要,调整对象进入老年代的年龄阈值(-XX:MaxTenuringThreshold)以及大对象的大小(-XX:PretenureSizeThreshold),让长期存活的对象尽早的进入老年代,可以适当降低Minor GC的次数。

总之,一切的JVM调优都要根据项目的业务需要来进行设置。

四、参数汇总

-XX:MaxTenuringThreshold:对象从年轻代进入老年代的年龄阈值;

-XX:PretenureSizeThreshold:大对象大小的阈值,当创建的对象大小大于设置的阈值,则直接进入老年代;

-XX:NewRatio:设置年轻代与老年代的比例,如果设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5;

-XX:SurvivorRatio:设置年轻代中Eden区与Survivor区的比例,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10;

-XX:+UseAdaptiveSizePolicy:动态调整Eden与Survivor区比例;

-XX:-HandlePromotionFailure:老年代空间担保机制;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值