JVM调优 前必学文章

JVM是运行在操作系统之上的 ,与硬件没有直接的交互

img

  • 注意:我们平时说的栈是指的Java栈,native method stack 里面装的都是native方法。见下文

  • 类装载器ClassLoader 负责加载class文件,c1ass文件在文件开头有特定的文件标示, 将c1ass文件字节码内容加载到内存中,并将这些内容转换成方法区中的 运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否 可以运行,则由Execution Engine决定

  • image-20201019135017176

注意:

  • 方法区并不是存放方法的区域,其是存放类的描述信息(模板)的地方

  • Class loader只是负责class文件的加载,相当于快递员,这个“快递员”并不是只有一家,Class loader有多种

  • 加载之前是“小class”,加载之后就变成了“大Class”,这是安装java.lang.Class模板生成了一个实例。“大Class”就装载在方法区,模板实例化之后就得到n个相同的对象

  • JVM并不是通过检查文件后缀是不是.class来判断是否需要加载的,而是通过文件开头的特定文件标志

    文件开头的特殊标识

img

注意:

  • Class loader有多种,可以说三个,也可以说是四个(第四个为自己定义的>加载器,继承 ClassLoader),系统自带的三个分别为:

  1. 启动类加载器(Bootstrap) ,C++所写

  2. 扩展类加载器(Extension) ,Java所写

  3. 应用程序类加载器(AppClassLoader)。

我们自己new的时候创建的是应用程序类加载器(AppClassLoader)。

import com.gmail.fxding2019.T;

public class Test{
  //Test:查看类加载器
  public static void main(String[] args) {

      Object object = new Object();
      //查看是那个“ClassLoader”(快递员把Object加载进来的)
      System.out.println(object.getClass().getClassLoader());
      //查看Object的加载器的上一层
      // error Exception in thread "main" java.lang.NullPointerException(已经是祖先了)
      //System.out.println(object.getClass().getClassLoader().getParent());

      System.out.println();

      Test t = new Test();
      System.out.println(t.getClass().getClassLoader().getParent().getParent());
      System.out.println(t.getClass().getClassLoader().getParent());
      System.out.println(t.getClass().getClassLoader());
  }
}

/*
*output:
* null
*
* null
* sun.misc.Launcher$ExtClassLoader@4554617c
* sun.misc.Launcher$AppClassLoader@18b4aac2
* */

注意:

  • 如果是JDK自带的类(Object、String、ArrayList等),其使用的加载器是Bootstrap加载器;如果自己写的类,使用的是AppClassLoader加载器;Extension加载器是负责将把java更新的程序包的类加载进行

  • 输出中,sun.misc.Launcher是JVM相关调用的入口程序

  • Java加载器个数为3+1。前三个是系统自带的,用户可以定制类的加载方式,通过继承Java. lang. ClassLoader

双亲委派机制

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。 采用双亲委派的一一个好处是比如加载位于rt,jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

注意:

  • 双亲委派机制:“我爸是李刚,有事找我爹”。 例如:需要用一个A.java这个类,首先去顶部Bootstrap根加载器去找,找得到你就用,找不到再下降一层,去Extension加载器去找,找得到就用,找不到再将一层,去AppClassLoader加载器去找,找得到就用,找不到就会报"CLASS NOT FOUND EXCEPTION"。

//测试加载器的加载顺序
package java.lang;

public class String {

  public static void main(String[] args) {

      System.out.println("hello world!");

  }
}

/*
* output:
* 错误: 在类 java.lang.String 中找不到 main 方法
* */

上面代码是为了测试加载器的顺序:首先加载的是Bootstrap加载器,由于JVM中有java.lang.String这个类,所以会首先加载这个类,而不是自己写的类,而这个类中并无main方法,所以会报“在类 java.lang.String 中找不到 main 方法”。

这个问题就涉及到,如果有两个相同的类,那么java到底会用哪一个?如果使用用户自己定义的java.lang.String,那么别使用这个类的程序会去全部出错,所以,为了保证用户写的源代码不污染java出场自带的源代码,而提供了一种“双亲委派”机制,保证“沙箱安全”。即先找到先使用。

Execution Engine执行引擎负责解释命令,提交操作系统执行。

Native Interface本地接口

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java 诞生的时候是C/C++横行的时候,要想立足,必须有调用C/C++程序, 于是就在内存中专门开辟了-块区域处理标记为native的代码,它的具体做法是NativeMethod Stack中登记nattive方法,在Execut ion Engine执行时加载native libraies。

目前该方法使用的越来越少了,除非是与硬有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。

Native Method Stack

它的具体做法是Nat ive Method Stack中登记native方法,在Execut ionEngine执行时加载本地方法库。

Thread类的start方法如下:

public synchronized void start() {
      /**
        * This method is not invoked for the main method thread or "system"
        * group threads created/set up by the VM. Any new functionality added
        * to this method in the future may have to also be added to the VM.
        *
        * A zero status value corresponds to state "NEW".
        */
      if (threadStatus != 0)
          throw new IllegalThreadStateException();

      /* Notify the group that this thread is about to be started
        * so that it can be added to the group's list of threads
        * and the group's unstarted count can be decremented. */
      group.add(this);

      boolean started = false;
      try {
          start0();
          started = true;
      } finally {
          try {
              if (!started) {
                  group.threadStartFailed(this);
              }
          } catch (Throwable ignore) {
              /* do nothing. If start0 threw a Throwable then
                it will be passed up the call stack */
          }
      }
  }

  private native void start0();

Thread类中竟然有一个只有声明没有实现的方法,并使用native关键字。用native表示,也此方法是系统级(底层操作系统或第三方C语言)的,而不是语言级的,java并不能对其进行操作。native方法装载在native method stack中。

PC寄存器 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将 要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

如果执行的是一-个Native方法,那这个计数器是空的。

用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(0ut0fMemory=0OM)错误

  • 注意:native方法不归java管,所以计数器是空的

img

上面图中是亮色的地方有两个特点:

    1. 所有线程共享(灰色是线程私有)

    1. 亮色地方存在垃圾回收

Method Area方法区 供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space) 和元空间(Metaspace)。 But 实例变量存在堆内存中,和方法区无关

注意:

  • 方法区:绝对不是放方法的地方,他是存储的每一个类的结构信息(比如static)

  • 永久代和元空间的解释:

    方法区是一种规范,类似于接口定义的规范:

    List list = new ArrayList();
    

    把这种比喻用到方法区则有:

    1. java 7中:方法区 f = new 永久代();

    2. java 8中:方法去 f = new 元空间();

Stack栈 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它 的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说 不存在垃圾回收问题,只要线程一结束该栈就0ver,生命周期和线程一 致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是 在函数的栈内存中分配。 栈存储什么? 栈帧中主要保存3类数据: 本地变量(Loca1 Variables) :输入参数和输出参数以及方法内的变量; 栈操作(Operand Stack) :记录出栈、入栈的操作; 栈帧数据(Frame Data) : 包括类文件、方法等等。

注意:

  • 栈管运行,堆管存储

  • 栈是线程私有,不存在垃圾回收

  • 栈帧的概念:java中的方法被扔进虚拟机的栈空间之后就成为“栈帧”,比如main方法,是程序的入口,被压栈之后就成为栈帧。

栈运行原理: 栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,

A方法又调用了B方法,于是产生栈帧F2也被压入栈, B方法又调用了c方法,于是产生栈帧F3也被压入栈, 执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F 1栈帧.....遵循“先进后出”/“后进先出”原则。 每个方法执行的同时都会创建一个栈帧, 用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K^ 756K之间,与等于1Mb左右。

img

img

public class  Test{

  public static void m(){
      m();
  }

  public static void main(String[] args) {

      System.out.println("111");
      //Exception in thread "main" java.lang.StackOverflowError
      m();
      System.out.println("222");

  }
}

/*
*output:
* 111
* Exception in thread "main" java.lang.StackOverflowError
* */

注意:

  • StackOverflowError是一个“”错误,而不是“异常”

    img

img

注意:

  • HotSpot:如果没有明确指明,JDK的名字就叫HotSpot

    img

  • 元数据:描述数据的数据(即模板,也就是“大Class”) 上面的关系图的一个实例为下图:

    img

7 Heap堆 一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保 存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分: ●Young GenerationSpace 新生区 Young/New ●Tenure generationspace 老年区 Old/ Tenure ● Permanent Space 永久区 Perm

img

img

注意:

  • Java 7之前和图上一模一样,Java 8永久区换成了元空间

  • 堆逻辑上由”新生+养老+元空间“三个部分组成,物理上由”新生+养老“两个部分组成

  • 当执行new Person();时,其实是new在新生区的伊甸园区,然后往下走,走到养老区,但是并未到元空间。

新生区是类的诞生、成长、消亡的区域,-一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。

幸存区有两个: 0区( Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC (Fu11GC),进行养老区的内存清理。若养老区执行了Fu11 GC之后发现依然无法进行对象的保存,就会产生00M异常“OutOfMemoryError”。 如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二: (1) Java虚 拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。 (2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

注意:

  • GC发生在伊甸园区,当对象快占满新生代时,就会发生YGC(Young GC,轻量级GC)操作,伊甸园区基本全部清空

  • 幸存者0区(S0),别名“from区”。伊甸园区没有被YGC清空的对象将移至幸存者0区,幸存者1区别名“to 区”

  • 每次进行YGC操作,幸存的对象就会从伊甸园区移到幸存者0区,如果幸存者0区满了,就会继续往下移,如果经历数次YGC操作对象还没有消亡,最终会来到养老区

  • 如果到最后,养老区也满了,那么就对养老区进行FGC(Full GC,重GC),对养老区进行清洗

  • 如果进行了多次FGC之后,还是无法腾出养老区的空间,就会报OOM(out of Memory)异常

  • from区和to区位置和名分不是固定的,每次GC过后都会交换,GC交换后,谁空谁是to区

img

注意:

  • 整个堆分为新生区和养老区,新生区占整个堆的1/3,养老区占2/3。新生区又分为3份:伊甸园区:幸存者0区(from区):幸存者1区(to区) = 8:1:1

  • 每次从伊甸园区经过GC幸存的对象,年龄会+1

img

实际而言,方法区(MethodArea) 和堆一样,是各个线程共享的内存区域,它用于存储虛拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有-一个别名叫做Non-Heap(非堆),目的就是要和堆分开。对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)”,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。

注意:

  • 临时对象就是说明,其在伊甸园区生,也在伊甸园区死。

  • 堆逻辑上由”新生+养老+元空间“三个部分组成,物理上由”新生+养老“两个部分组成,元空间也叫方法区

  • 永久代(方法区)几乎没有垃圾回收,里面存放的都是加载的rt.jar等,让你随时可用

永久区(java7之前有) 永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class. Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

Java8 JDK 1.8之后将最初的永久代取消了,由元空间取代。

注意

  • 上面的展示的是物理上的堆,分为两块,新生区和养老区。

  • 堆的参数主要有两个:

    -Xms,
    
    Xmx:
    
    1. -Xms堆的初始化的大小

    2. Xmx堆的最大化

  • Young Gen(新生代)有一个参数-Xmn,这个参数可以调新生区和养老区的比例。但是,这个参数一般不调。

  • 永久代也有两个参数:-XX:PermSize-XX:MaxPermSize,可以分别调永久带的初始值和最大值。Java 8 后没有这两个参数啦,因为Java 8后元空间不在虚拟机内啦,而是在本机物理内存中

Java8 在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。 元空间与永久代之间最大的区别在于: 永久带使用的JVM的堆内存,但是java8以后 的元空间并不在虚拟机中而是使用本机物理内存。 因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入nativememory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

img

//查看自己机器上的默认堆内存和最大堆内存
public class Test{

  public static void main(String[] args) {
      System.out.println(Runtime.getRuntime().availableProcessors());
      //返回 Java虚拟机试图使用的最大内存量。物理内存的1/4(-Xmx)
      long maxMemory = Runtime.getRuntime().maxMemory() ;
      //返回 Java虚拟机中的内存总量(初始值)。物理内存的1/64(-Xms)
      long totalMemory = Runtime.getRuntime().totalMemory() ;
      System.out.println("MAX_MEMORY =" + maxMemory +"(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
      System.out.println("DEFALUT_MEMORY = " + totalMemory + " (字节)、" + (totalMemory / (double)1024 / 1024) + "MB");

  }
}

/*
*   8
  MAX_MEMORY =1868038144(字节)、1781.5MB
  TOTAL_MEMORY = 126877696 (字节)、121.0MB
* */
  • 注意:JVM参数调优,平时可以随便挑初始大小和最大大小,但是实际工作中,初始大小和最大大小应该是一致的,原因是避免内存忽高忽低产生停顿

  • IDEA 的JVM内存配置

    1. 点击Run列表下的Edit Configuration

      img

    2. 在VM Options中输入以下参数:-Xms1024m -Xmx1024m -XX:+PrintGCDetails

      img

    3. 运行程序查看结果

      img

  • 把堆内存调成10M后,再一直new对象,导致Full GC也无法处理,直至撑爆堆内存,查看堆溢出错误(OOM),程序及结果如下:

    img
    img
    img

GC收集日志信息详解

  • 第一次进行YGC相关参数: [PSYoungGen: 2008K->482K(2560K)] 2008K->782K(9728K), 0.0011440 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    img

  • 最后一次进行FGC相关参数: [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4025K->4005K(7168K)] 4025K->4005K(9216K), [Metaspace: 3289K->3289K(1056768K)], 0.0082055 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

    img
    img

面试题:GC是什么(分代收集算法)

  • 次数上频繁收集Young区

  • 次数上较少收集Old区

  • 基本不动元空间

面试题:GC的四大算法(后有详解)

    1. 引用计数法

    1. 复制算法(Copying)

    1. 标记清除(Mark-Sweep)

    1. 标记压缩(Mark-Compact)

面试题:下面程序中,有几个线程在运行

img
Answer:有两个线程,一个是main线程,一个是后台的gc线程。

GC算法概述

知识点:

  • JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC or Young GC),一种是全局GC(major GC or Full GC)

  • Minor GC和Full GC的区别   普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。   全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上 (因为养老区比较大,占堆的2/3)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值