JVM垃圾回收器与JVM简单的调优

目录

什么是垃圾?

怎么判定是垃圾?

常用的垃圾回收算法

JVM内存分代模型(用于分代垃圾回收算法)

堆内存逻辑分区

新生代:

老年代:

常见的垃圾回收器

Epsilon:

Serial+Serial Old

PS+PO

ParNew+CMS

CMS使用的算法:三色标记(并发标记时使用的算法)

CMS的解决方案

G1的解决方案:SATB (Snapshot At the Begining)

ZGC和Shenandoah的解决方法:颜色指针

为什么ZGC管理4个T的内存?

JVM调优

调优前的基础概念

什么是调优

调优所使用的基本指令

设定垃圾回收器打印日志参数时需要注意的事项


什么是垃圾?

在c语言中,分配内存空间是手动的,用完释放也是需要手动删除的
在java中,你new了一个对象就会在堆内存中创建内存空间,如果该对象没有其它引用指向,就视为垃圾。

可以理解为,创建了一个对象,该对象在堆中的地址是会在栈中被引用,相当于有一跟线在牵着,如果那根线断了,就说明它是个垃圾了

怎么判定是垃圾?

引用计算器

        每个对象都有一个计算器,被引用时就计算器就加一,为零时说明就是个垃圾了;但是会出现一堆垃圾出现的情况,也就是说对象之间循环指向,但是没有一个对象是被栈中所指向的;

根可达算法(Root Searching)

        如上图所示,找到根对象,如果根对象中的线断了,那么其它的都视为垃圾。解决上面产生的问题

常用的垃圾回收算法

  • 标记清除(Mark-Sweep)
  • 拷贝(Copying)
  • 标记压缩(Mark-Compact)

标记清除

内存中的角色:存活对象、可回收(垃圾)、未使用

找到垃圾,标记垃圾,清除垃圾

造成的问题:内存碎片化严重,这里清一块,那里清一块,就会造成内存的不连续性

拷贝

将内存一分为二,一边使用,一边用于拷贝,在第一块区域中,找到存活对象,并将它拷贝到另一块区域中,然后就可以把第一块区域的数据全部清掉了;相当于腾了个地方吧

好处:没有碎片

坏处:浪费空间  例如:10个g的内存只能当5个g的内存使用

标记压缩

标记垃圾时,同时将内存中的所有存活对象放在一端,然后清除所标记的垃圾;这样清理完成后就不会产生碎片;

好处:不会碎片化,没有浪费空间

坏处:效率较低

JVM内存分代模型(用于分代垃圾回收算法)

  • 新生代(new -yong)
  • 老年代(old)
  • 部分垃圾回收器使用的模型,有些垃圾回收器是没有新老年代之分的,比如G1(注意区分垃圾回收器与垃圾回收算法
  • 在jdk1.7时还有一个永久代 ,1.8时叫元数据区
    • 永久代/元数据用来装什么的:Class对象
    • 永久代必须指定大小限制,所以可能会造成永久代的内存溢出,因为有大小设置,一旦动态代理对象时,产生的class大小很可能就会超出指定的大小
    • 元数据区就可以设置大小,也可以不设置,无上限(受限于物理内存)
    • 字符串常量:1.7是存在永久代中;1.8存在堆内存中
    • 方法区(MethodArea):是一个逻辑概率,1.7→永久代;1.8→元数据

堆内存逻辑分区

  • 分为新生代和老年代,它们在堆内存中所占的比例是1:2
  • 新生代使用拷贝垃圾回收算法,老年代使用标记压缩算法

新生代:

  • 分为三个区域:Eden、From Survivor、To Survivor  比例:8:1:1
  • 新new出来的对象如果大小合适,就存放在eden区,如果过大,直接放入tenured区
  • YGC:也就是清理eden区的垃圾;
  • 在第一次YGC时,会将eden区中所有的存活对象拷贝到From Survivo中,那么在eden区中就可以直接标记清除所有的垃圾了
  • 再YGC时,将eden+From Survivo区中的存活对象放入To Survivor区中,然后直接干掉eden和From Survivo区中的垃圾
  • 再YGC时,就会将eden+To Survivor中存活对象转入From Survivor中,然后在干掉垃圾,这样就构成了一个回收循环;也就是说,新生代中内存的使用率是90%,因为,总有一个Survivor是用不到的,它不参与垃圾回收,用于保存存活对象;

老年代:

怎样才会进入老年代呢?

答:每经过一次GC那么对象的"年龄"就会加一,当年龄达到15时就会进入老年代(CMS 16),或者当对象过大也会直接进入老年代;

老年代存放的都是顽固分子

如果老年代装不下了怎么办?

答:进行FGC,使用FGC会有STW,所以尽量不要产生FGC(无法避免)

FGC

  • 全局GC,也就是YGC+OGC,新生代和老年代一起回收

GC Tuning(Generation)调优

  1. 尽量减少FGC
  2. MinorGC=TGC
  3. MajorGC=FGC   

常见的垃圾回收器

十种垃圾回收器

新生代:ParNew、Serial、Parallel Scavenge(PS)

老年代:CMS、Serial Old、Parallel Old(PO)

其它四种不区分新老年代

新老年代的回收器都是配合使用的(图中有虚线相连的都是可以搭配使用的)

  • JDK1.0的时候默认使用的是:Serial+Serial Old
  • JDK1.8的默认使用:PS+PO
  • ParNew+CMS没有被默认使用过,因为CMS存在很大的缺陷 

线上运行的时候基本使用的就是上面这三种组合,像后面的4种很少使用

接下来就聊一聊垃圾回收器的实现

Epsilon:

  • 啥也不做,两种作用:调试和确认不用GC就可以干活

Serial+Serial Old

  • 当多个线程并发运行产生的垃圾达到一定量时,就会触发GC,当执行GC时,所有的线程都会停止,等待GC完毕才能继续执行;那么停止的这段时间就叫STW(Stop to World)停止整个世界
  • GC时使用的是单线程回收,所以在早期JDK时内存还不够大,可以使用。但是,到现在,内存都是十几几十个g的了,所以还使用单线程效率会非常的慢

PS+PO

  • 与上面的实现是相差不多的,最大的区别在于GC时不再是单线程,而是使用的多线程

ParNew+CMS

  • CMS在GC时分为四个阶段:初始标记→并发标记→最终标记→并发清理
  • 初始标记:会STW,但它只标记根上的对象,所以时间会很短
  • 并发标记:是运行线程与GC同时运行,所以会产生误标的情况,产生的问题:
    • 例如在这段时间中:开始标记一个对象不是垃圾,但在GC去标记其它对象的时候,该对象的引用没了,那么它就应该被标记为垃圾。这种情况属于漏标,问题不是很大,只要在下一轮GC找到并清除就可以,也就是浮动垃圾
    • 或者一开始标记是垃圾,在GC标记其它的对象时,又有对象指向了该对象,那么它就应该不是个垃圾。这种情况就比较严重,把不是垃圾的对象给清除了,会造成空指针异常。
  • 重新标记:STW,时间也会很短,因为在上个阶段就标记的差不多了,错误会很少。所以这样就能完整的标记垃圾
  • 并发清理:一边清理一边运行
  • 总结:最消耗时间的还是并发标记,因为做的事情最多的还是在这个阶段,但是是并发的,所以影响不大

CMS最大的问题在于:如果触发了FGC的话,他就会使用Serial Old清理,那么Serial Old的问题就在于它是单线程的,如果一旦执行Serial Old,那么整个程序很有可能就卡死不动了,因为是单进程的,所以GC所需要的时间可想而知。这也是它没被JDK有默认使用阶段的原因。

CMS使用的算法:三色标记(并发标记时使用的算法)

三色标记的颜色:白色、灰色、黑色

白色:还没有被标记的对象

灰色:标记过的对象,但没有完全被标记,该对象中的子对象可能还未被标记,在下一轮中会继续查找

黑色:完全标记过的对象,不会在标记

在并发标记的过程产生的问题,如下图所示:

第一种情况:

        在并发标记中A→B→C,如果B指向C的线突然断了,那么c就会成为垃圾,但是不影响,因为垃圾回收是一轮一轮的,只要下一次能回收掉就可以了。这种垃圾就称为浮动垃圾

第二种情况:

        A→B→C,B指向C的线断了,然后A直接指向了C;产生的问题:因为A是黑色的,所以C就不会被标记(漏标),那么C就会被回收掉,程序就会出错。

CMS的解决方案

        如果黑色有新的指向,那么就把黑色降级为灰色

但是该解决方案还是会产生漏标

        假设灰色标记中两个属性,当A线程在扫描完第一个属性后,该属性重新指向了一个白色标记,那么这时候灰色标记就应该还是灰色的,但是,当A线程扫描完所有属性后,发现没问题,就会把该标记变成黑色的,这样也产生了漏标现象。

所以CMS在最后阶段,就需要重新扫描,STW,所以效率有时候也不会很好

G1的解决方案:SATB (Snapshot At the Begining)

        当B指向C的指针消失时,会将该指针存储到栈中,当下一轮扫描的时候,就会在栈中找有没有指针,如果有,就拿出来在执行,如果指向存在就标记不是垃圾,否则就真是垃圾了。

ZGC和Shenandoah的解决方法:颜色指针

一个java的指针是64bit,其中42个是指向真正的java对象的,18个是没有用的,那么就还剩下4个bit,一个颜色指针就是一个bit,用来做状态区分,也就是当引用该指针时,根据颜色指针的状态做相对应的操作

为什么ZGC管理4个T的内存?

2^42=4T

用42位代表一个对象的地址

JVM调优

调优前的基础概念

  1. 吞吐量:用户代码时间 /(用户代码执行时间+垃圾回收时间)
  2. 响应时间:STW越短,响应时间越好

所谓的调优,就是看你需要调哪一方面的,是吞吐量啊还是响应时间,根据具体业务而定

什么是调优

  1. 根据需求进行JVM规范调优和预调优
  2. 优化运行JVM运行环境(慢或者卡顿)
  3. 解决JVM运行过程中出现的各种问题(OOM)

调优所使用的基本指令

标准: -开头,所有的HotSpot都支持;例如:java -version

非标准: -X开头,特定版本HotSpot支持特定命令

不稳定: -XX开头,下个版本可能取消

  • java -Xms...  :最小堆(用于指定堆的最小值)
  • java -Xmx... :最大堆(用于指定堆的最大值)
  • java -XX:+PrintFlagsWithComments   :打印所有的参数并带解释(只有JVMdebug版本能用)
  • java -XX:+PrintFlagsFinal | wc -l    :打印所有的jvm运行时需要的指令(七百多个指令/728)

调优场景 :liunx环境下

  • 项目背景:贷款公司中的风险评估
        根据风险评估判断可以贷多少钱给用户
        根据模型评估

模拟代码展示:

public class FullGC_Probleam {

    /**
     * 银行卡用户信息类
     */
    private static class CardInfo {
        /*
            数据有:用户花的钱,用户的姓名,用户的年龄,用户的生日
            根据这些数据计算该用户具体能代多少钱
         */
        //用户花的钱
        BigDecimal price = new BigDecimal(0.0);
        String name = "张三";
        int age = 5;
        //生日
        Date birthdate = new Date();

        //该方法想象成匹配的过程
        public void m() {}
    }

    //线程池new出来,
    private static ScheduledThreadPoolExecutor executor = new
            ScheduledThreadPoolExecutor(50,
            new ThreadPoolExecutor.DiscardOldestPolicy());

    public static void main(String[] args) throws Exception {
        executor.setMaximumPoolSize(50);
        //一直循环进行数据的模型匹配
        for (;;) {
            //每隔100ms,就评估一百个用户的风险值
            modelFit();
            Thread.sleep(100);

        }
    }

    /**
     * 模型匹配
     */
    private static void modelFit() {
        //从数据库中获取用户数据
        List<CardInfo> taskList = getAllCardInfo();
        //遍历查询到的所有用户信息
        taskList.forEach(info -> {
            //创建线程池,每过一段时间就执行m()方法
            executor.scheduleWithFixedDelay(() -> {
                //do sth with info
                info.m();
                //退2 3毫秒进行计算
            }, 2, 3, TimeUnit.SECONDS);
        });
    }

    //方法进行计算

    /**
     * 获取用户信息,相当于是从数据库中获取的数据
     * 然后封装到数据库实体类中,并放入集合中
     *
     * @return
     */
    private static List<CardInfo> getAllCardInfo() {
        List<CardInfo> taskList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            CardInfo ci = new CardInfo();
            taskList.add(ci);
        }
        return taskList;
    }
}
  • 执行指令启动程序:java -Xms100M -Xmx100M -XX:+PrintGC com.heima.article.controller.v1.FullGC_Probleam
  • jps:查看进程id
  • top:查询所有进程的使用情况
  • jstack pid(进程id):查看该进程具体运行的方法状态(改指令对这个调优没有关系,可以用于死锁的情况)
  • jinfo pid:查看进程的信息,堆大小,执行命令等等
  • jstat -gc pid:查看gc信息

调优使用的工具是阿里的arthas图形化工具,他是使用JVM的JVMTI工具接口实现的

首先启动arthas:java -jar arthas-boot.jar

看到如下就说明启动成功了:

  • 使用:sc *xxx(可以指定包名)      查找需要查看的类
  • 使用:sm *xxx                                  查找需要查看类中的方法
  • trace                                                 查看一个方法的状态
  • monitor                                             可以查看一个方法传入的值与返回的值
  • jmap -histo 9547 | head -20             在jvm中可以查看指定线程数的线程具体数据信息,arthas中没有该指令

上面程序在运行一段时间后,会频繁的FGC,回收不掉对象,产生内存泄漏及内存溢出问题。注:产生多的内存泄漏就会造成内存溢出问题。内存泄漏是造成内存溢出的一个原因。

使用:jmap -histo 9547 | head -20  指令多次查看线程中内存的使用情况,看是哪个线程使用的内存变化较大,这样就可以快速定位到是那个对象出现的问题,在使用arthas中的指令就可以定位到是那个方法那个进程的问题了。

重点注意:jmap指令执行时会STW,如果项目已经发布了那么该指令是不好被使用的。因为如果内存很大,那么STW的时间就会很长,那么怎么程序就会出现卡顿现象。这种现象是不被允许的

可以这么说:

  1. 就说我们的项目是在集群状态下的,即使停了一台服务器对我们的业务也是没有影响的。
  2. 将系统放入测试环境进行压测。
  3. 在运行程序时设置参数HeapDump,OOM的时候会自动产生堆转储文件。但是,该方法太晚了,出现了OOM才执行,所以不太好。

设定垃圾回收器打印日志参数时需要注意的事项

不单单使用一个日志文件进行存储,这样会导致日志文件难以查看。不方便阅读,所以在设置日志文件时:需要指定多个日志文件循环存储(日志文件大小限制建议20M)。例如:A日志、B日志、C日志满了,那么就会覆盖A日志在进行存储。

指令:-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation - XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails - XX:+PrintGCDateStamps -XX:+PrintGCCause

在生产环境中日志文件,后面日志名字,按照系统时间产生,循环产生,日志个数5个,每个大小 20m,这样的好处在于整体大小100M。能控制整体文件大小。

注:一般记录日志的时候,记录日志文件 如果只有一个日志文件,肯定不行,有些服务器可能一天产生的日志文件就上T,连着30天就是30T,产生量大,如果查找问题,查找不到问题所在。 其实这个工作是运维干的活儿 。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值