关于jvm:JVM学习笔记二JVM基本结构

7次阅读

共计 7561 个字符,预计需要花费 19 分钟才能阅读完成。

1 起源

  • 起源:《Java 虚拟机 JVM 故障诊断与性能优化》——葛一鸣
  • 章节:第二章

本文是第二章的一些笔记整顿。

2 JVM基本参数-Xmx

java命令的个别模式如下:

java [-options] class [args..]

其中 -options 示意 JVM 启动参数,class为带有 main()Java类,args示意传递给 main() 的参数,也就是 main(String [] args) 中的参数。

个别设置参数在 -optinos 处设置,先看一段简略的代码:

public class Main {public static void main(String[] args) {for(int i=0;i<args.length;++i) {System.out.println("argument"+(i+1)+":"+args[i]);
        }
        System.out.println("-Xmx"+Runtime.getRuntime().maxMemory()/1024/1024+"M");
    }
}

设置应用程序参数以及 JVM 参数:

输入:

能够看到 -Xmx32m 传递给 JVM,使得最大可用堆空间为32MB,参数a 作为应用程序参数,传递给 main(),此时args.length 的值为 1。

3 JVM根本构造

各局部介绍如下:

  • 类加载子系统 :负责从文件系统或者网络中加载Class 信息,加载的类信息寄存在一个叫 办法区 的内存空间中
  • 办法区:除了蕴含加载的类信息之外,还蕴含运行时常量池信息,包含字符串字面量以及数字常量
  • Java 堆 :在虚拟机启动时建设,是最次要的内存工作区域,简直所有的Java 对象实例都存在于 Java 堆 中,堆空间是所有线程共享的
  • 间接内存 :是在Java 堆 外的,间接向零碎申请的内存区域。NIO库容许 Java 程序应用 间接内存 ,通常 间接内存 的访问速度要优于 Java 堆。另外因为 间接内存 在堆外,大小不会受限于 -Xmx 指定的堆大小,然而会受到操作系统总内存大小的限度
  • 垃圾回收零碎 :能够对 办法区 Java 堆间接内存 进行回收,Java 堆 是垃圾收集器的工作重点。对于不再应用的垃圾对象,垃圾回收零碎 会在后盾默默工作、默默查找,标识并开释垃圾对象
  • Java 栈 :每个JVM 线程都有一个公有的 Java 栈,一个线程的Java 栈 在线程创立时被创立,保留着帧信息、局部变量、办法参数等
  • 本地办法栈 :与Java 栈 相似,不同的是 Java 栈 用于 Java 办法调用,本地办法栈 用于本地办法(native method)调用,JVM容许 Java 间接调用本地办法
  • PC 寄存器 :每个线程公有的空间,JVM 会为每个线程创立 PC 寄存器,在任意时刻一个Java 线程总是执行一个叫做 以后办法 的办法,如果 以后办法 不是本地办法,PC寄存器就会指向以后正在被执行的指令,如果 以后办法 是本地办法,那么 PC 寄存器 的值就是undefined
  • 执行引擎 :负责执行JVM 的字节码,古代 JVM 为了进步执行效率,会应用即时编译技术将办法编译成机器码后执行

上面重点说三局部:Java 堆 Java 栈 以及 “

4 Java 堆

简直所有的对象都存在 Java 堆 中,依据垃圾回收机制的不同,Java 堆 可能领有不同的构造,最常见的一种是将整个 Java 堆 分为 新生代 老年代

  • 新生代 :寄存新生对象或年龄不大的对象,有可能分为edens0s1,其中s0s1别离被称为 fromto区域,它们是两块大小相等、能够调换角色的内存空间
  • 老年代 :寄存老年对象,绝大多数状况下,对象首先在eden 调配,在一次新生代回收后,如果对象还存活,会进入 s0s1,之后每通过一次新生代回收,如果对象存活则年龄加 1。当对象年龄达到肯定条件后,会被认为是老年对象,就会进入老年代

5 Java 栈

5.1 简介

Java 栈 是一块线程公有的内存空间,如果是 Java 堆 与程序数据密切相关,那么 Java 栈 和线程执行密切相关,线程执行的根本行为是函数调用,每次函数调用都是通过 Java 栈 传递的。

Java 栈 与数据结构中的 相似,有 FIFO 的特点,在 Java 栈中保留的次要内容为 栈帧 ,每次函数调用都会有一个对应的 栈帧 入栈,每次调用完结就有一个对应的 栈帧 出栈。栈顶总是以后的帧(以后执行的函数所对应的帧)。栈帧保留着 局部变量表 操作数栈 帧数据 等。

这里说一下题外话,置信很多读者对 StackOverflowError 不生疏,这是因为函数调用过多造成的,因为每次函数调用都会生成对应的栈帧,会占用肯定的栈空间,如果栈空间有余,函数调用就无奈进行,当申请栈深度大于最大可用栈深度时,就会抛出StackOverflowError

JVM提供了 -Xss 来指定线程的最大栈空间。

比方,上面这个递归调用的程序:

public class Main {
    private static int count = 0;

    public static void recursion(){
        ++count;
        recursion();}

    public static void main(String[] args) {
        try{recursion();
        }catch (StackOverflowError e){System.out.println("Deep of calling ="+count);
        }
    }
}

指定-Xss1m,后果:

指定-Xss2m

指定-Xss3m

能够看到调用深度随着 -Xss 的减少而减少。

5.2 局部变量表

局部变量表是栈帧的重要组成部分之一,用于保留函数的参数及局部变量。局部变量表中的变量只在以后函数调用中无效,函数调用完结后,函数栈帧销毁,局部变量表也会随之销毁。

5.2.1 参数数量对局部变量表的影响

因为局部变量表在栈帧中,如果函数的参数和局部变量表较多,会使局部变量表收缩,导致栈帧会占用更多的栈空间,最终缩小了函数嵌套调用次数。

比方:

public class Main {
    private static int count = 0;

    public static void recursion(long a,long b,long c){
        long e=1,f=2,g=3,h=4,i=5,k=6,q=7;

        count++;
        recursion(a,b,c);
    }

    public static void recursion(){
        ++count;
        recursion();}

    public static void main(String[] args) {
        try{//            recursion();
            recursion(0L,1L,2L);
        }catch (StackOverflowError e){System.out.println("Deep of calling ="+count);
            count = 0;
        }
    }
}

无参数的调用次数(-Xss1m):

带参数的调用次数(-Xss1m):

能够看到次数显著缩小了,起因正是因为局部变量表变大,导致栈帧变大,从而次数缩小。

上面应用 jclasslib 进一步查看,先在 IDEA 装置如下插件:

装置后应用插件查看状况:

第一个函数是带参数的,能够看到最大局部变量表的大小为 20 字(留神不是字节),Long 在局部变量表中须要占用 2 字。而相比之下不带参数的函数最大局部变量表大小为 0:

5.2.2 槽位复用

局部变量表中的槽位是能够复用的,如果一个局部变量超过了其作用域,则在其作用域之后的局部变量就有可能复用该变量的槽位,这样可能节俭资源,比方:

public static void localVar1(){
    int a = 0;
    System.out.println(a);
    int b = 0;
}

public static void localVar2(){
    {
        int a = 0;
        System.out.println(a);
    }
    int b = 0;
}

同样应用 jclasslib 查看:

能够看到少了 localVar2 的最大局部变量大小为 1 字,相比 localVar1 少了 1 字,持续剖析,localVar1第 0 个槽位为变量 a,第 1 个槽位为变量 b:

localVar2 中的 b 复用了 a 的槽位,因而最大变量大小为 1 字,节约了空间。

5.2.3 对 GC 的影响

上面再来看一下局部变量表对垃圾回收的影响,示例:

public class Main {public static void localGC1(){byte [] a = new byte[6*1024*1024];
        System.gc();}

    public static void localGC2(){byte [] a = new byte[6*1024*1024];
        a = null;
        System.gc();}

    public static void localGC3(){
        {byte [] a = new byte[6*1024*1024];
        }
        System.gc();}

    public static void localGC4(){
        {byte [] a = new byte[6*1024*1024];
        }
        int c = 10;
        System.gc();}

    public static void localGC5(){localGC1();
        System.gc();}

    public static void main(String[] args) {System.out.println("-------------localGC1------------");
        localGC1();
        System.out.println();
        System.out.println("-------------localGC2------------");
        localGC2();
        System.out.println();
        System.out.println("-------------localGC3------------");
        localGC3();
        System.out.println();
        System.out.println("-------------localGC4------------");
        localGC4();
        System.out.println();
        System.out.println("-------------localGC5------------");
        localGC5();
        System.out.println();}
}

输入(请加上 -Xlog:gc 参数):

[0.004s][info][gc] Using G1
-------------localGC1------------
[0.128s][info][gc] GC(0) Pause Full (System.gc()) 10M->8M(40M) 12.081ms

-------------localGC2------------
[0.128s][info][gc] GC(1) Pause Young (Concurrent Start) (G1 Humongous Allocation) 9M->8M(40M) 0.264ms
[0.128s][info][gc] GC(2) Concurrent Cycle
[0.133s][info][gc] GC(3) Pause Full (System.gc()) 16M->0M(14M) 2.799ms
[0.133s][info][gc] GC(2) Concurrent Cycle 4.701ms

-------------localGC3------------
[0.133s][info][gc] GC(4) Pause Young (Concurrent Start) (G1 Humongous Allocation) 0M->0M(14M) 0.203ms
[0.133s][info][gc] GC(5) Concurrent Cycle
[0.135s][info][gc] GC(5) Pause Remark 8M->8M(22M) 0.499ms
[0.138s][info][gc] GC(6) Pause Full (System.gc()) 8M->8M(22M) 2.510ms
[0.138s][info][gc] GC(5) Concurrent Cycle 4.823ms

-------------localGC4------------
[0.138s][info][gc] GC(7) Pause Young (Concurrent Start) (G1 Humongous Allocation) 8M->8M(22M) 0.202ms
[0.138s][info][gc] GC(8) Concurrent Cycle
[0.142s][info][gc] GC(9) Pause Full (System.gc()) 16M->0M(8M) 2.861ms
[0.142s][info][gc] GC(8) Concurrent Cycle 3.953ms

-------------localGC5------------
[0.143s][info][gc] GC(10) Pause Young (Concurrent Start) (G1 Humongous Allocation) 0M->0M(8M) 0.324ms
[0.143s][info][gc] GC(11) Concurrent Cycle
[0.145s][info][gc] GC(11) Pause Remark 8M->8M(16M) 0.316ms
[0.147s][info][gc] GC(12) Pause Full (System.gc()) 8M->8M(18M) 2.402ms
[0.149s][info][gc] GC(13) Pause Full (System.gc()) 8M->0M(8M) 2.462ms
[0.149s][info][gc] GC(11) Concurrent Cycle 6.843ms

首行输入示意应用G1,上面一一进行剖析:

  • localGC1:并没有回收内存,因为此时 byte 数组被变量 a 援用,因而无奈回收
  • localGC2:回收了内存,因为 a 被设置为了 nullbyte 数组失去强援用
  • localGC3:没有回收内存,尽管此时 a 变量曾经生效,然而依然存在于局部变量表中,并且指向 byte 数组,因而无奈回收
  • localGC4:回收了内存,因为申明了变量 c,复用了a 的槽位,导致 byte 数组失去援用,顺利回收
  • localGC5:回收了内存,尽管 localGC1 中没有开释内存,然而返回到 localGC5 后,localGC1的栈帧被销毁,也包含其中的 byte 数组失去了援用,因而在 localGC5 中被回收

5.3 操作数栈与帧数据区

操作数栈也是栈帧的重要内容之一,次要用于保留计算过程的两头后果,同时作为计算过程中变量的长期存储空间,也是一个 FIFO 的数据结构。

而帧数据区则保留着常量池指针,不便程序拜访常量池,此外,帧数据区也保留着异样处理表,以便在出现异常后,找到解决异样的代码。

5.4 栈上调配

栈上调配是 JVM 提供的一项优化技术,根本思维是,将线程公有的对象打散调配到栈上,益处是函数调用完结后能够主动销毁,而不须要垃圾回收器的染指,从而进步零碎性能。

栈上调配的一个技术根底是逃逸剖析,逃逸剖析目标是判断对象的作用域是否会逃逸出函数体,例子如下:

public class Main {
    private static int count = 0;

    public static class User{
        public int id = 0;
        public String name = "";
    }

    public static void alloc(){User user = new User();
        user.id = 5;
        user.name = "test";
    }

    public static void main(String[] args) {long b = System.currentTimeMillis();
        for (int i = 0; i < 1000000000; i++) {alloc();
        }
        long e = System.currentTimeMillis();
        System.out.println(e-b);
    }
}

启动参数:

-server # 开启 Server 模式,此模式下能力开启逃逸剖析
-Xmx10m # 最大堆内存
-Xms10m # 初始化堆内存
-XX:+DoEscapeAnalysis # 开启逃逸剖析
-Xlog:gc # GC 日志
-XX:-UseTLAB # 敞开 TLAB
-XX:+EliminateAllocations # 开启标量替换,默认关上,容许将对象打散调配在栈上

输入如下,没有 GC 日志:

而如果敞开了标量替换,也就是增加-XX:-EliminateAllocations,就能够看到会频繁触发GC,因为这时候对象寄存在堆上而不是栈上,堆只有 10m 空间,会频繁进行GC

6 办法区

Java 堆 一样,办法区 是所有线程共享的内存区域,用于保留零碎的类信息,比方类字段、办法、常量池等,办法区 的大小决定了零碎能够保留多少个类,如果定义了过多的类,会导致 办法区 溢出,会间接OOM

JDK6/7办法区 能够了解成 永恒区 JDK8 后,永恒区 被移除,取而代之的是 元数据区 ,能够应用-XX:MaxMetaspaceSize 指定,这是一块堆外的间接内存,如果不指定大小,默认状况下 JVM 会耗尽所有可用的零碎内存。

如果 元数据区 产生溢出,JVM会抛出OOM

7 Java 堆 Java 栈 以及 办法区 的关系

看完了 Java 堆Java 栈 以及 办法区,最初来一段代码来简略剖析一下它们的关系:

class SimpleHeap{
    private int id;
    public SimpleHeap(int id){this.id = id;}

    public void show(){System.out.println("id is"+id);
    }

    public static void main(String[] args) {SimpleHeap s1 = new SimpleHeap(1);
        SimpleHeap s2 = new SimpleHeap(2);
        s1.show();
        s2.show();}
}

main中创立了两个局部变量 s1s2,则这两个局部变量寄存在Java 栈 中。同时这两个局部变量是 SimpleHeap 的实例,这两个实例寄存在 Java 堆 中,而其中的 show 办法,则寄存与 办法区 中,图示如下:

8 小结

本文次要讲述了 JVM 的根本构造以及一些根底参数,根本构造能够分成三局部:

  • 第一局部:类加载子系统 Java 堆 办法区 间接内存
  • 第二局部:Java 栈 本地办法栈PC 寄存器
  • 第三局部:执行引擎

而重点讲了三局部:

  • Java 堆 :常见的构造为 新生代 + 老年代 构造,其中新生代可分为edsns0s1
  • Java 栈 :包含局部变量表、操作数栈与帧数据区,还提到了一个JVM 优化技术栈上调配,能够通过 -XX:+EliminateAllocation 开启(默认开启)
  • 办法区:所有线程共享区域,用于保留类信息,比方类字段、办法、常量等
正文完
 0