美文网首页Java知识点汇集
Java虚拟机基本结构

Java虚拟机基本结构

作者: vincent_0425 | 来源:发表于2019-05-25 23:14 被阅读0次

    Java虚拟机是JVM类语言的根基,其中动态内存管理和垃圾收集技术是JVM中最重要的特性。本节主要讲述其中的内存管理相关概念。

    一 Java虚拟机的基本结构

    Java虚拟机结构.jpg

    如图所示为Java虚拟机的基本结构,每个模块介绍如下:

    • 类加载子系统
      类加载子系统负责从文件或网络中加载Class字节码信息,然后存放于方法区。
    • 方法区
      方法区是各个线程共享的内存区域,用于存储虚拟机加载的类变量、常量、静态变量以及即时编译后的代码等数据。
    • Java堆
      在虚拟机启动的时候建立,是Java程序最主要的内存工作区域,几乎所有的对象实例和数组都在Java堆上分配。和方法区一样,是各个线程共享的内存区域。可通过-Xmx和Xms虚拟机参数控制Java堆大小。
    • 垃圾回收系统
      垃圾回收是Java虚拟机的重要组成部分,其中的垃圾回收器可以对Java堆、方法区和直接内存进行回收。同时Java堆是回收器的工作重点。
    • 直接内存
      在Java的NIO库中,允许Java程序使用直接内存,它是Java堆外直接向系统申请的内存区域。通常情况下该区域的内存访问速度优于Java堆。
    • Java栈
      Java栈是线程私有的,它的生命周期和线程相同。它在线程创建的时候被创建。Java栈中保存帧信息,每个方法创建的时候都会创建一个栈帧,用于存储局部变量、方法参数、操作数栈、方法出口灯信息,和方法的调用返回密切相关。
    • 本地方法栈
      和Java栈类似,但其中最大的不同是Java栈用于方法调用,而本地方法栈用于本地方法调用。
    • PC寄存器
      该区域也是每个线层私有的空间。Java虚拟会为每个Java线程创建PC寄存器。当当前执行的方法不是本地方法时,PC寄存器就会指向当前正在被执行的指令。若当前执行的方式是本地方法,则PC寄存器的值为undefined。
    • 执行引擎
      执行引擎是虚拟机最核心的组件之一,它负责执行虚拟机的字节码。

    二 Java堆

    Java堆是和Java应用程序关系最为密切的内存空间。Java堆内存通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示的释放。Java堆分为新生代和老年代。其中新生代存放新生对象或年龄不大的对象,而老年代则存放老年对象。新生代和老年代结构如下图所示:


    Java堆结构.png

    在大多数情况下,对象首先在Eden区分配,在一次新生代回收后,若对象还存活着则进入S0或S1,在这之后,每经一次新生代回收,若对象还存活着则它的年龄会加1,达到一定年龄后该对象就被认为是老年对象,从而进入老年代。当然这里只是其中一种方式进入老年代,后续文章会有详细叙述。

    三 Java栈

    Java栈是一块线程私有内存空间。Java栈用于传递每次函数调用的数据。它是一块后进先出的数据结构,在其中保存的主要内容是栈帧。每一次函数调用都会有对应的栈帧压如Java栈,同时函数结束时栈帧被弹出。


    Java栈帧.jpg

    上图可用下面的代码表示:

    public void function1() {
        public void function2();
    }
    
    public void function2() {
        public void function3();
    }
     public void function3() {
        ....
    }
    ...
    

    在一个栈帧中至少包含局部变量表、操作数栈和帧数据区几个部分。
    但是Java栈空间也不能无限使用下去,它受-Xss参数限制,该参数也决定了函数调用的最大深度。
    示例:

    public class TestStackDeep {
        private static int count = 0;
        public static void recursion() {
            count++;
            recursion();
        }
        public static void main(String[] args) {
            try {
                recursion();
            } catch (Throwable e) {
                System.out.println("deep of calling = " + count);
                e.printStackTrace();
            }
        }
    }
    

    上面的代码计算最大栈深度,设置虚拟机参数-Xss256K,其结果为:

    deep of calling = 2374
    

    当设置-Xss512K时,结果为:

    deep of calling = 9245
    

    栈溢出则会抛出java.lang.StackOverflowError异常。

    1)局部变量表
    局部变量表用于保存函数的参数以及局部变量,它同样也随函数的调用而生灭,函数变量表可通过jclasslib工具查看,在Idea中,jclasslib可作为插件方式安装。
    示例代码:

    public class TestStack {
    
        public void test1() {
            int m, n, i, j, k;
            System.out.println("hello world");
        }
    
        public void test2(int param1, int param2) {
            long m, n, i, j, k;
            System.out.println("hello world");
        }
    
    
        public static void main(String[] args) {
            TestStack testStack = new TestStack();
    
        }
    }
    

    通过查看jclasslib可看到以下内容:


    testStack

    在jclasslib中,可看到当前类中的静态池,接口字段以及方法等信息。查看方法的Code部分可看到局部变量表统计信息:


    局部变量表信息

    从上图看出,test2()最大局部变量表占用大小为13字,因为test2()参数为两个int,加上this字段以及5个long变量正好是13字(int占用一个字,this占用一个字,long占用两个字)。查看局部变量表信息可通过LocalVariableTable:


    局部变量表

    上图中分别对应了局部变量的作用域范围,所在槽位(index),变量名(name)以及数据类型(Descriptor)
    栈帧中局部变量表的槽位是可以复用的,如果一个局部变量过了其作用域,那么在其后申明的新局部变量就有可能复用过期局部变量的槽位,从而达到节省资源的目的。
    2)操作数栈
    操作数栈主要用于保存计算过程中间结果。也作为计算过程中变量的临时存储空间。
    3)帧数据区

    4)栈上分配
    栈上分配是Java虚拟机提供的一项优化技术,基本思想是对于线程私有对象(即不会被其它线程访问到的对象实例),可以将它们分配在栈上,而不必从堆中分配,这样的好处是该对象在函数调用完毕后可以自行销毁而不必接入垃圾回收器,从而提高系统的整体性能。栈上分配的一个基础技术是逃逸分析,逃逸分析的目的是判断对象作用域是否逃逸出函数体。

    在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。
    当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或是返回到调用者子程序。如果使用尾递归优化(通常在函数编程语言中是需要的),对象也可以看作逃逸到被调用的子程序中。如果一种语言支持第一类型的延续性在Scheme和Standard ML of New Jersey中同样如此),部分调用栈也可能发生逃逸。
    编译器可以使用逃逸分析的结果作为优化的基础:[1]

    • 将堆分配转化为栈分配。如果某个对象在子程序中被分配,并且指向该对象的指针永远不会逃逸,该对象就可以在分配在栈上,而不是在堆上。在有垃圾收集的语言中,这种优化可以降低垃圾收集器运行的频率。
    • 同步消除。如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
    • 分离对象或标量替换。如果某个对象的访问方式不要求该对象是一个连续的内存结构,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

    例如下面的代码便是一个逃逸对象:

    private static Bean bean;
    public static void alloc() {
        bean = new Bean();
        bean.setParam(23);
    ....
    }
    

    其中bean字段可能被其它线程访问到,故属于逃逸对象。
    下面的代码显示了非逃逸对象:

    public static void alloc() {
         bean = new Bean();
        bean.setParam(23);
    ....
    }
    

    启用逃逸分析需要设置-server执行程序,JVM参数如下:

    -server -Xmx10m -Xms10m  -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
    
    • -Xmx256m -Mms256m 分别制定了堆最大空间和堆最小空间为10M;
    • -XX:+DoEscapeAnalysis 启用逃逸分析;
    • -XX:+PrintGC 打印GC信息;
    • -XX:-UseTLAB 关闭TLAB;
    • -XX:+EliminateAllocations 开启标量替换,允许将对象打散分配到栈上。
      以上参数都是默认启用的。
      示例:
    public class OnStackTest {
    
        public static class User {
            public int id = 0;
            public String name = "";
        }
    
        public static void alloc() {
            User user = new User();
            user.id = 10;
            user.name = "vincent";
        }
    
        public static void main(String[] args) throws InterruptedException {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 1000000000; i++) {
                alloc();
            }
            System.out.println(System.currentTimeMillis() - start);
        }
    }
    

    上面的代码进行了1000000000次调用,但是产生的GC日志很少:

    [GC (Allocation Failure)  2047K->536K(9728K), 0.0008531 secs]
    7
    

    但如果关闭了逃逸分析,则会产生大量的GC日志。例如将-XX:+DoEscapeAnalysis 替换成-XX:-DoEscapeAnalysis

    四 方法区

    方法区也是所有线程共享的内存区域,用于保存系统类信息,例如字段,方法常量池等。该区域大小决定了系统可以保存多少类。但若定义了太多类同样也会导致方法区溢出。
    在JDK6和JDK7中,方法区可链接为永久区,通过参数-XX:PermSize和-XX:MaxPermSize指定。但在JDK8中,永久区已经被移除,替代为元数据区,可使用-XX:MaxMetaspaceSize参数指定,若不指定该参数,默认情况下虚拟机会耗尽所有可用系统内存,在VisualVM中可观察永久区:


    永久区

    元数据区溢出虚拟机会抛出java.lang.OutOfMemoryError: Metaspace异常。

    参考

    相关文章

      网友评论

        本文标题:Java虚拟机基本结构

        本文链接:https://www.haomeiwen.com/subject/jsfttctx.html