JVM内存管理及GC原理调优实战

一、前要

JVM调优是一个系统而又复杂的过程,由于Java虚拟机自动管理内存,在大多数情况下,我们基本上不用去调整JVM内存分配,因为一些初始化参数已经可以保证应用服务正常稳定地工作。但是当有性能问题的时候该怎么去调优,该去关注什么呢?在去做这项工作前就必须去了解JVM是怎么去管理内存的,GC是怎么完成的。

二、标记算法

垃圾回收是对已经分配出去的但又不再使用的内存进行回收,以便能够再次分配。JVM主要是对堆空间那些死亡对象所占据的空间进行回收。那么如何判别一个对象存亡呢?

1.引用计数法算法

该算法的做法是为每个对象添加一个引用计数器,用来统计指向改对象的引用个数。一旦某个对象的引用计数器为0,则说明该对象已经死亡,便可以被回收了。

具体实现:如果有一个引用被赋值为某一个对象,那么将该对象的引用计算器+1。如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计算器-1。也就是说,所有的引用更新操作,相应的就会发生对象引用计算器的增减。可以看出有两个问题,第一是需要额外的空间来存储计数器以及繁琐的更新操作,第二就是无法处理循环引用对象。如下图:a/b对象相互引用,同时没有其他引用指向a或者b,那么a、b实际上已经死亡了,但是由于计数器不为0,导致不可回收,从而造成内存泄露。
在这里插入图片描述

2.可达性分析算法

其实Java 虚拟机的主流垃圾回收期采用的是可达性分析算法来判断对象是否存活的。该算法的基本思路是将一系列GC Roots作为初始的存活对象集合,然后从该集合出发,探索所有能够被该集合引用的对象,并将其加入到该集合中,该过程称为标记(mark)。最终未被探索到的对象便是可以回收的。

其实该过程有两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链(Reference Chain),那它将会被第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法以及被虚拟机调用过,都视为“没有必要执行”。如果对象被判断为有必要执行finalize()方法,那么就会放置在一个叫F-Queued的队列中,同时虚拟机会自动建立低优先级的Finalizer线程去执行它,进行第二次标记。如果对象覆盖finalize()方法且成功逃逸,那么就会移除即将回收集合,否则被回收(具体可以翻阅《深入理解Java 虚拟机》,这里不做过多解释)

三、垃圾回收算法

1.清除(sweep)

把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中,当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,划分给新建的对象
在这里插入图片描述
但是该算法有两个不足之处:一是分配效率较低,只要是内存空间不连续,java 虚拟机需要逐个访问空闲列表中的项来查找是否能够放入新对象的空闲内存;另外一个是空间问题,清除之后会产生大量不连续的内存碎片,那么就是导致无法找到足够的连续内存分配空间,从而无法分配。

2、复制(copy)

该算法主要是为了解决分配效率问题。把内存区域分为两等分,分别用两个指针from和to来维护,并且只是用from指针指向内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到to指针指向的内存区域中,并且交换from指针和to指针的内容。该算法解决了内存碎片问题,但是直接导致了内存使用率降低。(该算法被采用来回收新生代:内存分为Eden和 from survivor、to survivor,由于存在分配担保(Handle Promotion)问题,所以老年代一般不能选用这种算法)
在这里插入图片描述

3、整理(compact)

即把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是性能开销
在这里插入图片描述

四、Java 虚拟机的堆划分

Java 虚拟机将堆划分为新生代和老年代,其中新生代被划分为Eden区以及两块大小相同的Survivor区。JDK 1.8默认 -XX:+UseAdaptiveSurvivorSizePolicy配置项,JVM会根据分配最小堆内存,年轻代和老年代按照默认比例1:2进行分配,年轻代中的Eden和Survivor则按照8:2进行分配
在这里插入图片描述

五、内存调优实战

通过上述介绍,大致了解了内存分配情况以及GC相关算法,下面通过一个例子演示Java 堆内存设置过小导致频繁GC,同时通过GCViewer工具分析GC日志定位问题
该案例采用jdk 为1.8,垃圾回收器采用ParNew+CMS收集器

扫描二维码关注公众号,回复: 9682931 查看本文章

1.新建一个Spring Boot应用,作为调优对象,代码如下

@RestController
@SpringBootApplication
public class App {

    private Queue<QueueObejct> queueCache = new ConcurrentLinkedDeque<>();

    /**
     * 模拟秒杀接口每次请产生1M对象使用1千并发进行模拟年轻代, Minor GC
     * 同时将对象放入队列中模拟老年代,每2w次清空,FUll GC
     * @return
     */
    @RequestMapping("/")
    public String index() {
        //List<Byte[]> temp = new ArrayList<>();
        //Byte[] b = new Byte[1024*1024];
        //temp.add(b);
        QueueObejct queueObejct = new QueueObejct("hello world!");
        if(queueCache.size()>200000){
            queueCache.clear();
        }else {
            queueCache.add(queueObejct);
        }
        return "success";
    }

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
    class QueueObejct{
        private String msg;
        public QueueObejct(String msg) {
            this.msg = msg;
        }
        public String getMsg() {
            return msg;
        }
    }
}

2、执行命令启动应用

java -Xms32m -Xmx32m  -Xss256k  -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/Users/xx/logs/gc.log -XX:CMSInitiatingOccupancyFraction=80  -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -jar /Users/xx/IdeaProjects/xx/first-spring-boot-application/target/spring-boot-application-1.0.0-SNAPSHOT.jar

在这里插入图片描述
参数说明:
-Xms1024m -Xmx1024m :堆大小
-XX:PrintGCTimeStamps:打印 GC 具体时间;
-XX:PrintGCDetails :打印出 GC 详细日志;
-Xloggc: path:GC 日志生成路径。
-XX:+UseConcMarkSweepGC :老年代垃圾回收器为CMS
-XX:+UseParNewGC :年轻代垃圾回收器为ParNew
-XX:CMSInitiatingOccupancyFraction=80 : CMS垃圾收集器,当老年代达到80%时,触发CMS垃圾回收
-XX:+UseCMSInitiatingOccupancyOnly 只是用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整

查看JVM 启动信息:

jinfo -flags 11184

在这里插入图片描述
查看堆分配信息:

jmap -heap 11184

在这里插入图片描述
GC日志如下:

在这里插入图片描述

3.使用Jmeter进行并发压测模拟

在这里插入图片描述

4、使用GCViewer进行日志分析(只选择3条线 堆使用线,GC,Full GC线)

在这里插入图片描述

1、20并发持续3min:

在这里插入图片描述

  • 图中蓝线表示已使用堆内存大小,周期性上下符合我们对象池达2w清理
  • 绿色先表示年轻代GC活动情况,从图中可以看出当堆使用率上去了,会触发频繁的GC活动
  • 图中黑线表示Full GC,从图中可以看出伴随着Full GC ,蓝色会下降,说明回收了老年代对象
2、分析结论:
  • GC活动频繁:年轻代GC和年老代GC都比较密集,而且gc 触发类型基本上都为Allocation Failure,说明内存空间不足
  • 从蓝色线的动态变化来看,对象在GC后是能够被回收的,说明不是内存泄露
3、GC pause

GC 停顿时间为34.46s
在这里插入图片描述

4、调大堆的大小
java -Xms1024m -Xmx1024m  -Xss256k  -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/Users/xx/logs/gc.log -XX:CMSInitiatingOccupancyFraction=80  -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -jar /Users/xxxx/first-spring-boot-application/target/spring-boot-application-1.0.0-SNAPSHOT.jar

新的GC 日志分析图如下:
在这里插入图片描述
同样20并发持续3min,年轻代的GC 频率很低,没有发生Full GC,并且累计GC暂停时间只有0.73s

5、总结

如果我们看年轻代的内存使用率处在高位,导致频繁的 Minor GC,而频繁 GC 的效率又不高,说明对象没那么快能被回收,这时年轻代可以适当调大一点。
如果我们看年老代的内存使用率处在高位,导致频繁的 Full GC,这样分两种情况:如果每次 Full GC 后年老代的内存占用率没有下来,可以怀疑是内存泄漏;如果 Full GC 后年老代的内存占用率下来了,说明不是内存泄漏,我们要考虑调大年老代。

上述优化只是一个简单指导案例实际生产环境优化会比这更复杂,调整的参数会更多,结合的指标也会很多,例如调整年轻代Eden和survivor比例,Young 和Tenured 比例以及垃圾回收器的设置、同时还需分析GC后回收情况是否有内存泄露,结合jmap -histo pid 查看内存实例情况等等

发布了6 篇原创文章 · 获赞 9 · 访问量 373

猜你喜欢

转载自blog.csdn.net/weixin_44397870/article/details/104632119