【JVM】JVM调优(基础篇)

目录

 

一、概述

二、知识点划分

1)JVM内存划分

2)回收器算法

3)回收器


一、概述

先来说下JVM调优主要是在调啥?

调优就是调节JVM运行时内存大小+gc垃圾回收细节,要想调整JVM运行时内存大小

需要我们知道JVM内存划分知识以及要想调整gc垃圾回收的细节;

需要我们知道垃圾回收器工作原理以及它们使用的垃圾回收算法;

需要我们知道垃圾回收的一个流程以及调优的基本原则(能在年轻代回收掉的不要留到老年代,减少Full GC 次数);

二、知识点划分

1)JVM内存划分

                                                          JVM运行时内存划分图

堆(heap):这块区域主要存放对象的,比如说我们程序中new User()对象它就会被分配到这块区域,这块区域是共享的,也就是所有线程都可以访问该区域的对象,堆也是垃圾回收主要区域。

java虚拟机栈 (vm stack): 这块区域存放我们线程运行时的一些数据,它是每个线程私有的,可以理解为一个线程就对应的着一个java虚拟机栈,它里面就是一个个的栈帧组成的,一个方法就是一个栈帧,栈帧里面有局部变量表,操作数栈,常量池的引用,方法返回地址等;

比如下面这段代码:

public static void main(String[] args) {

        int a=1;

        int b=2;

        int c= a+b;

        System.out.println(c);

        c=0;

}

这段代码就是计算a+b 为c,然后调用println方法打印c的值。

这里面 a,b,c 就在局部变量表中,在算c的时候就是使用的操作数栈算的,然后调用println方法打印c其实就是往java 虚拟机栈 压入一个println 方法的栈帧。

println栈帧中会保存着一个返回地址(也就是方法返回地址),当println方法执行完成后,会进行弹栈,然后程序跳到返回地址位置,也就是c=0 位置继续执行。

本地方法栈(native stack):它与java虚拟机栈功能差不多,只不过java虚拟机栈是运行的java方法,而本地方法栈主要是执行的native方法。在hotspot虚拟机中将本地方法栈与java虚拟机栈合二为一。

元数据(Metaspace):这个是java8 的新概念,以前叫方法区,主要是存放着我们的class类信息,我们常说的类加载器其实就是将class文件加载到这个区域,这个区域还保存着我们定义的常量,也就是常量池。

程序计数器(Program Counter Register):它也叫做pc寄存器,每个线程一个程序计数器,它保存着当前线程执行的指令地址,当执行的是java 方法的时候,程序计数器保存的是当前需要执行的指令地址,当执行的是native 方法的时候,程序计数器保存的就是undefined。

 

2)回收器算法

1.什么是垃圾回收?

比如说我们在代码新起的线程中new 了以User对象,看下面这段代码

public static void main(String[] args) {

  new Thread(new Runnable() {

    @Override

    public void run() {

      User user = new User();

      //do something

    }

  }).start();

 

  //do something

  //.........

}

private static class  User{

  private String name;

  private int age;

}

 

然后它就会在堆(heap)区中为这个User对象划分一块内存,当这个线程执行完成之后,这个User对象就用不到了,就成了垃圾,你把它占用的这块内存释放掉就是垃圾回收。垃圾回收就是释放那些用不着的对象(这个用不着的对象就是我们平常说的垃圾)占用的内存。

怎样辨别垃圾?

通俗点说就是一个对象没有地方用着了就算是垃圾。怎样判断一个对象有没有被用着了呢? 其实最早的时候就是给某个对象引用计数器,有一个地方引用它,计数器就+1,不引用了就-1,这样做有个好处就是快,垃圾回收的时候只需要判断一下它这个引用计数器是不是0就可以了,但是有个问题就是那种相互引用的对象没法回收,比如说A对象 里面有个成员是B对象,B对象里面有个成员是A对象,然后A,B对象的引用计数器都是1,但是实际上已经没有地方用到这两个对象,还无法被回收掉,这个引用计数的方法就是引用计数算法。

再来了解下另一个辨别垃圾的方法,可达性分析算法,就是从一个点开始,然后顺着往下找,沿途的对象就是有用的,剩下的就是无用的,这个点就是GCRoot。

这个GCRoot都有哪些呢?

虚拟机栈方法中引用的对象(栈帧的局部变量表)

静态成员引用的对象

常量引用的对象

本地方法栈中(Native方法)引用的对象

我们举个例子(虚拟机栈方法中引用的对象) :

public class GCTest {

    public static void main(String[] args) {

        createUser();

        try {

            TimeUnit.SECONDS.sleep(60);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

    private static void createUser() {

        User user = new User();

    }

}

class User{

    private String name;

    private String des;

}

 

main方法中,先调用了createUser方法,创建了User对象,我们用图来表示下:

这时候createUser 的user变量就是个GCRoot,顺着它往下找就能找到在堆中的User对象,这时候User对象是个有用的对象,一旦createUser执行完,也就是createUser栈帧弹栈,这个User对象没有GCRoot能够到达它了,他就成了无用的,也就是我们常说的垃圾。

再来个静态成员引用的例子:

public class GCTest {

    private static User user =new User();

}

class User{

    private String name;

    private String des;

}

我们知道,static修饰的成员属于类成员,在这里也就是GCTest.class的成员,那么GCTest.class存在哪呢,我们在介绍2.1JVM运行时内存划分的时候说过元数据区域是存放class对象的,GCTest.class 就是存放在这。

这个静态成员user就是个GCRoot,user引用着User对象,如果User对象里面还引用着对象A,A引用着对象B…

那么这一串都属于有用对象,而且这个静态成员引用的对象一般不会被回收,元数据区这块回收的条件非常苛刻,等着我们在介绍永久代回收的时候讲到。

常量引用的对象已经native栈引用的对象也同理,这里就不展开介绍了。

 

标记清除

标记清除算法分为先根据GCRoot找出存活的对象,并进行标记,标记完成后,扫描整个空间,然后对未标记的对象进行回收。

由于user 引用着User对象,bytes引用着byte[]数组,所以这两个对象被标记。标记完成后就会扫描整个空间,然后回收未标记的对象

由于直接回收未标记的对象,会导致内存碎片的产生,为什么这么说呢,如果直接回收的对象占空间很小,然后程序新创建的对象占用的空间比较大,就没法重复利用这块小内存,如果这种小内存非常多的话,就会浪费很多内存。

 

标记整理

标记整理也分两部分,先是标记,根据GCRoot找出活着的对象并标记

然后就是将这些活着的对象靠左边的空闲空间移动,并更新对应的指针,并清除未被标记的对象。

我们可以看到它跟标记清除算法差不多,但是在清除之前多了一步整理的工作。这个整理的工作增加了成本,但是解决了标记清除算法的内存碎片问题

复制算法

将内存分为两块,一块内存存放创建的对象,一块内存空着,回收的时候,根据GCRoot找到活着的对象,然后将它们复制到那块空着的内存上,之后把存放创建对象那块内存清了。之后分配对象在存活对象的那块内存上分配。这样子解决了内存碎片问题,同时效率能够提升,唯一不好的地方就是费内存。

分代回收

通过这个JVM运行时数据区域图我们可以看到,堆区被分成了年轻代与老年代两部分,通过字面意思可以猜到,年轻代主要就是存放新创建的对象的,然后老年代主要是存放一些老对象的,我们还可以看到这个我们这个元数据区域变成了永久代,这个永久代好像是存放永久存活的对象的。可以看到年轻代被划分成了Eden, From ,to三个区域,这三个区域默认大小比例是8:1:1 , 这三个区域是怎么回事呢,这里其实是个复制算法的优化版,前面我们说了复制算法会浪费内存,然后这里对它进行了优化,当我们在代码中执行new User(创建对象)的时候,JVM会帮我们在Eden申请一块内存存放User对象,当Eden和From满了的时候,就会触发年轻代的垃圾回收,这时候会根据GCRoot找到存活的对象,然后将这些存活的对象复制到to区域,然后清除Eden与From区域,当有新建对象的时候,会被分配到Eden或to区域,等着Eden与to区域满了的时候,还是根据GCRoot找出存活的对象,然后复制到From区域,最后将Eden与to清空。我们可以看到From与to这两块内存总有一块内存会空着,也就是不使用,这样浪费一小块内存,从而达到高效率的收益。

为什么三个区域默认大小比例为啥是8:1:1呢?

因为,我们创建的对象大多数都是朝生夕死的,所以在每次GC的时候使用一小块内存来存放活着的对象,来个官方一点的说法:

IBM论文里说据他们统计95%的对象朝生夕死一样存活时间极短,为了保险默认实际使用了90%。

3)回收器

Serial收集器
Serial收集器是最古老的收集器,它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即stop the world。到现在为止,它依然是虚拟机运行在client模式下的默认新生代收集器,与其他收集器相比,对于限定在单个CPU的运行环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程收集效率。

ParNew

ParNew收集器是Serial收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会stop the world,只是相比较Serial收集器而言它会运行多条进程进行垃圾回收。

ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百的保证能超越Serial收集器。当然,随着可以使用的CPU的数量增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

CMS收集器

工作流程

CMS的特点是低延迟,很适合于与用户交互的应用。这款垃圾收集器是一第一款真正意义上的并发垃圾回收器,它使用的是标记-清除算法,所以也会有"stop the world"时间,但是时间会很短。

初识标记:

         初始标记时要停止所有用户的线程,会进入"stop the world"时间,这时仅仅标记与GCRoots直接相连的对象(假设为集合A),所以初始标记的时间会很短。

并发标记:

        此时标记的是与集合A直接或间接相连的对象(假设为集合B),跃然并发标记的时间较长,时长大约比初始标记大一个数量级,但是,此时是与用户程序并发执行的,并不会出现"stop the world"情况。

重新标记:

        因并发标记时用户的线程还在执行,此时会产生新的连接对象(假设为集合C),重新标记就是标记集合C的对象。那么为了防止重新标记过程产生新的对象,所以此时要停止所有用户的线程,从而进入"stop the world"时间,此过程的用时也很短。

并发清除:

        重新标记后,所以确定是垃圾的对象(假设为集合D),随后垃圾回收器会将集合D清除掉。并不会移动对象,所以此过程是可以和用户线程并发执行的。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小旋哥^^

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

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

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

打赏作者

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

抵扣说明:

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

余额充值