Java — JVM 实操剖析内部结构

16 篇文章 0 订阅

Java — JVM 实操剖析内部结构

本篇文章主要以实操剖析 JVM 环节为主,辅助于理解 JVM 相关理论知识。注:本文代码部分所采用的 JDK 环境为 1.8。

一、JVM 相关

(一) JVM运行原理

  • .java源文件通过编译器编译产生 .class字节码文件,.class字节码文件通过JVM当中的字节码解释器,编译成特定机器上的 机器码

  • 每一种平台的解释器不同,但是实现的JVM时相同的。(JVM会根据不同的系统生成不同的机器码,每个操作系统都有一个不同的JVM)。

  • 查看本机 JVM 型号:

    // 通过 java -version 命令,可以查看我们本机的 JVM 为 HotSpot
    java -version
    
    在这里插入图片描述

(二) JVM 体系结构

JVM体系结构:类加载器,运行时数据区,执行引擎

在这里插入图片描述
1. 类加载器
  • 作用:负责加载.class文件

  • 代码分析:

    • 测试代码:

      public class JvmTest {
      
          /**
           * JVM体系结构1:类加载器
           * 类加载器:负责加载.class文件
           *
           * @param object
           */
          public void classLoaderTest(Object object) {
              // 返回此 {@code Object} 的运行时类
              final Class<?> objectClass = object.getClass();
              System.out.println(objectClass);
      
              // 获取当前类加载器
              final ClassLoader classLoader = objectClass.getClassLoader();
              System.out.println("当前类加载器:" + classLoader);
      
              // 父类类加载器(设计双亲委派机制)
              System.out.println("EXC类加载器:" + classLoader.getParent());
              System.out.println("BOOT类加载器:" + classLoader.getParent().getParent());
          }
      
      
          public static void main(String[] args) {
              JvmTest jvmTest = new JvmTest();
      
              // 查看 JVM类加载器
              jvmTest.classLoaderTest(jvmTest);
          }
      }
      
    • 代码输出:

      当前类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
      EXC类加载器:sun.misc.Launcher$ExtClassLoader@75a1cd57
      BOOT类加载器:null
      

      我们可以看到程序中对象使用的类加载器为:AppClassLoader。
      那我们为什么还要输出父级的类加载器呢?这儿涉及类加载器一个机制:双亲委派机制,解释如下:

      双亲委派机制的工作过程:

      • 类加载器收到类加载的请求;
      • 把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;(APP -> EXC -> BOOT)
      • 启动类加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载。
      • 重复步骤三;
2. 运行时数据区
1) 组成结构
  • 方法区:一个JVM只有一个方法区,所有线程共享,存储:已经被虚拟机加载的类信息,常量(final),静态变量(static),常量池
  • 堆:一个JVM只有一个堆,所有线程共享,所有对象实例都在堆上分配,堆是垃圾收集器管理的区域
    • 新生区(分为伊甸园区、幸存区from区、幸存区to区),类诞生和成长的地方,甚至死亡
    • 养老区
    • 永久存储区:JDK8以后,永久存储区改了个名字(元空间)
  • 虚拟机栈:每个方法执行的时候,会同步创建一个栈用于存放局部变量等信息
  • 程序计数器:当前线程执行的字节码的行号指示器,线程私有
  • 本地方法栈:为本地方法服务,调用本地方法接口,【nativate关键字】
2) 代码分析
  • 为了能够看到直观 JVM 效果,我们需要提前在 IDEA 中进行如下简单配置:

    -XX:+PrintGCDetails
    

    在这里插入图片描述

2.2.1 JVM内存
  • 首先,我们总体查看一下 JVM 内存 的情况,即对应于 JVM 运行时数据区 的情况:

    • 代码如下:

      public class JvmTest {
      
          /**
           * JVM体系结构2:运行时数据区大小情况
           */
          public void jvmInfo() {
              // 返回虚拟机试图使用的最大内存
              long max = Runtime.getRuntime().maxMemory();
              System.out.println("JVM 最大内存:" + (double) max / (1024 * 1024));
      
              // 返回 JVM 初始化的总内存
              long total = Runtime.getRuntime().totalMemory();
              System.out.println("JVM 初始化总内存:" + (double) total / (1024 * 1024));
          }
      
          public static void main(String[] args) {
              JvmTest jvmTest = new JvmTest();
      
              // 查看 JVM 内存信息
              jvmTest.jvmInfo();
          }
      }
      
    • 代码输出:

      JVM 最大内存:3623.5
      JVM 初始化总内存:245.5
      Heap
       PSYoungGen      total 76288K, used 11190K [0x000000076b180000, 0x0000000770680000, 0x00000007c0000000)
        eden space 65536K, 17% used [0x000000076b180000,0x000000076bc6dba0,0x000000076f180000)
        from space 10752K, 0% used [0x000000076fc00000,0x000000076fc00000,0x0000000770680000)
        to   space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000)
       ParOldGen       total 175104K, used 0K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000)
        object space 175104K, 0% used [0x00000006c1400000,0x00000006c1400000,0x00000006cbf00000)
       Metaspace       used 3533K, capacity 4500K, committed 4864K, reserved 1056768K
        class space    used 379K, capacity 388K, committed 512K, reserved 1048576K
      

      本人电脑是:16G 内存。
      结论:JVM运行时数据区(JVM 内存),默认情况下:JVM分配的总最大内存是电脑内存的 1/4,初始化内存是 1/64。

2.2.2 虚拟机栈
  • JVM 虚拟机栈 主要用来管理程序调用,比如我们程序整体为:在 main 函数中调用 函数A,则该程序的虚拟机栈为:
    在这里插入图片描述

  • 代码模拟:
    我们模拟一个简单的 栈溢出 的错误代码:

    public class JvmTest {
    
        /**
         * JVM体系结构2:运行时数据区 —— 栈
         * 一个JVM只有一个堆,所有线程共享,所有对象实例都在堆上分配,堆是垃圾收集器管理的区域
         */
        public void jvmStackOverflowError() {
            // 循环调用,模拟栈溢出:StackOverflowError
            A();
        }
    
        /**
         * 闭环调用,触发栈溢出:StackOverflowError
         */
        public void A() {
            B();
        }
    
        public void B() {
            A();
        }
    
        public static void main(String[] args) {
            JvmTest jvmTest = new JvmTest();
    
            // 模拟 JVM 中栈溢出
            jvmTest.jvmStackOverflowError();
        }
    }
    
  • 代码输出:

    Exception in thread "main" java.lang.StackOverflowError
    
  • 分析:
    以上程序的运行过程如下图所示:在不停的将 A,B函数 压入 JVM的虚拟机栈,最后肯定会撑爆虚拟机栈,导致栈内存溢出的问题。
    (暗示:JVM的虚拟栈中肯定不可能存在垃圾,不然程序都是异常的,垃圾如果在栈中,垃圾下面的方法肯定无法被执行。
    所以我们常说的 JVM垃圾回收,是回收的 JVM 堆中的垃圾)

    在这里插入图片描述
2.2.3 JVM 堆
  • JVM堆,这部分是 运行时数据区的重点,并且JVM垃圾回收主要也是针对的是堆部分的内存。

  • 代码分析1:
    我们接合 2.2.1 JVM内存 的代码输出进行分析

    JVM 最大内存:3623.5
    JVM 初始化总内存:245.5
    Heap
     PSYoungGen      total 76288K, used 11190K [0x000000076b180000, 0x0000000770680000, 0x00000007c0000000)
      eden space 65536K, 17% used [0x000000076b180000,0x000000076bc6dba0,0x000000076f180000)
      from space 10752K, 0% used [0x000000076fc00000,0x000000076fc00000,0x0000000770680000)
      to   space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000)
     ParOldGen       total 175104K, used 0K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000)
      object space 175104K, 0% used [0x00000006c1400000,0x00000006c1400000,0x00000006cbf00000)
     Metaspace       used 3533K, capacity 4500K, committed 4864K, reserved 1056768K
      class space    used 379K, capacity 388K, committed 512K, reserved 1048576K
    
    • 首先、控制台的输出,已经很明显表明了,堆(Heap)的逻辑结构,分为:

      • PSYoungGen(新生区)
        • 伊甸园 区
        • from 区
        • to 区
      • ParOldGen(老年区)
      • Metaspace(元空间,相当于1.8之前的永久代)

      (细节:我们将 新生代 + 老年代的内存相加,发现刚好等于 JVM初始化内存的大小,得出结论:元空间逻辑上存在,但是在物理上不存在)

  • 代码分析2:

    • 前面,我们得出堆中主要的物理存储在新生代和老年代中
      我们结合新生区,和老年区的结构,分析垃圾回收算法(有:复制算法、标记-清除算法、标记-整理算法、分代收集算法)

    • 首先,我们模拟 堆内存溢出 的代码,我们来看一下 GC 是如何进行垃圾回收的过程:

      public class JvmTest {
      
          /**
           * JVM体系结构2:运行时数据区 —— 堆
           * 一个JVM只有一个堆,所有线程共享,所有对象实例都在堆上分配,堆是垃圾收集器管理的区域
           * 堆分为:新生代(伊甸园区,from,to),老年代,永久代(元空间,逻辑上存在,物理上不存在)
           */
          public void jvmHeapOutOfMemoryError() {
              // 模拟堆溢出问题 (OOM)
              while (true) {
                  int[] array1 = new int[999999999];
                  int[] array2 = new int[999999998];
              }
          }
      
          public static void main(String[] args) {
              JvmTest jvmTest = new JvmTest();
      
              // 模拟 JVM 中堆内存溢出
              jvmTest.jvmHeapOutOfMemoryError();
          }
      }
      
    • 输出:
      我截取了部分输出日志分析出:

      • 前 2 次发生了 轻GC 【因为新生区的 from / to内存大小为 10752,前两次 轻GC 只是在新生代的 from和to之间移动,归结为:新生区 -> 幸存区(from/to)的移动,该过程因为有两个频繁使用的区 from/to,所以在新生区采用 复制算法 进行垃圾回收】

      • 第 3 次发生了 重GC【当三次轻GC执行完毕,新生代中的 from/to 内存不够,需要进行重GC:新生区 -> 老年区的过程,该区域使用 标记-清除和标记整理 算法 进行垃圾回收】

      • 由此可以得出结论:JVM 根据堆划分的不同区域,整体采用 **分代搜集算法,**其次在不同的代里面根据代的结构和生命周期,采取不同的垃圾回收算法,新生区采用复制算法,老年区采用 标记-清除 和标记整理算法。

      [GC (Allocation Failure) [PSYoungGen: 9879K->1792K(76288K)] 9879K->1800K(2859008K), 0.0021556 secs] 
      [GC (Allocation Failure) [PSYoungGen: 1792K->1796K(76288K)] 1800K->1804K(2859008K), 0.0011227 secs] 
      [Full GC (Allocation Failure) [PSYoungGen: 1796K->0K(76288K)] [ParOldGen: 8K->1628K(72704K)] 1804K->1628K(148992K), [Metaspace: 3543K->3543K(1056768K)], 0.0327013 secs]
      
      [GC (Allocation Failure) [PSYoungGen: 0K->0K(76288K)] 1628K->1628K(2859008K), 0.0004414 secs]
      [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 1628K->1556K(108544K)] 1628K->1556K(184832K), [Metaspace: 3543K->3543K(1056768K)], 0.0343275 secs]
      
      Heap
       PSYoungGen      total 76288K, used 1966K [0x000000076b180000, 0x0000000774000000, 0x00000007c0000000)
        eden space 65536K, 3% used [0x000000076b180000,0x000000076b36b9e0,0x000000076f180000)
        from space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000)
        to   space 10752K, 0% used [0x0000000773580000,0x0000000773580000,0x0000000774000000)
       ParOldGen       total 2782720K, used 1556K [0x00000006c1400000, 0x000000076b180000, 0x000000076b180000)
        object space 2782720K, 0% used [0x00000006c1400000,0x00000006c1585290,0x000000076b180000)
       Metaspace       used 3581K, capacity 4500K, committed 4864K, reserved 1056768K
        class space    used 386K, capacity 388K, committed 512K, reserved 1048576K
        
      Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
      	at indi.pentiumcm.jvm.JvmTest.jvmHeapOutOfMemoryError(JvmTest.java:69)
      	at indi.pentiumcm.jvm.JvmTest.main(JvmTest.java:108)
      

二、JVM 调优

  • 经过上面的代码分析,其实 JVM 的调优,主要围绕 JVM堆内存的大小设置,轻重GC参数的设置(多少次进行一次重GC等)。
    该部分内容,等理解好之后再进行更新。

三、JVM 调优工具

  • 推荐:jprofiler
    IDEA 中的插件
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值