zoukankan      html  css  js  c++  java
  • Java程序执行过程及内存机制

    本讲将介绍Java代码是如何一步步运行起来的,其中涉及的编译器,类加载器,字节码校验器,解释器和JIT编译器在整个过程中是发挥着怎样的作用。此外还会介绍Java程序所占用的内存是被如何管理的:堆、栈和方法区都各自负责存储哪些内容。最后用一小块代码示例来帮助理解Java程序运行时内存的变化。

    Java程序执行过程

    • 步骤 1: 写源代码,源代码将以.java的文件格式保存在电脑硬盘中。
    • 步骤 2: 编译器(compiler)检查是否存在编译期错误(例如缺少分号,关键字拼写错误等)。若通过检测,编译器就会将源代码翻译成字节码(bytecode),以.class的文件格式保存在电脑硬盘中。
    • 步骤 3: 若要运行此Java程序,JVM中会有一个叫类加载器(class loader)的内置程序把字节码从硬盘载入到正位于内存中的JVM里去。
    • 步骤 4: JVM中还有一个叫字节码校验器(bytecode verifier)的内置程序检测是否存在运行期错误(例如栈溢出)。若通过检测,字节码校验器就会将字节码传递给解释器(interpreter)。
    • 步骤 5: 解释器会对字节码进行逐行翻译,将其翻译成当前所在系统可以理解的机器码(machine code),
    • 步骤 6:将机器码交给操作系统,操作系统会以main方法作为入口开始执行程序。至此,一个Java程序就这样运行起来了。

    细心的读者可能注意到了,在流程图中还涉及到一个叫JIT的东西在步骤中没有被解释。那么JIT编译器(Just-In-Time Compiler)是如果参与进程序的执行过程中呢?让我们来看以下两个例子。

    • 情况 1: 解释器对代码进行逐行解释,正如我们在步骤中所介绍的。
    • 情况 2: 这是JIT编译器参与进Java执行过程的情况,JIT编译器会扫描所有代码并对其进行优化。例如此时它发现最后一行代码是重复多余的,就会将其移除,只传递前4行代码给解释器。这样解释器就能运行地更快速高效,毕竟少了一行多余的代码需要翻译。

    当然,这只是JIT编译器的优化手段之一,不同公司设计的JIT编译器对Java程序的运行会有不同的优化方式。此外需要知道的是,JIT编译器并不是每次都会参与到执行过程中来。

    内存机制

    在步骤3中我们谈到字节码会被类加载器载入到内存,那么载入之后JVM是如何对其进行内存管理的呢?

    通常,在载入内存后,一个Java程序所占用的内存会被大致分为3块区域:堆(heap),栈(stack)和方法区(method area)。

    堆:存放new出来的东西。

    栈:存放局部变量。

    方法区:类型信息,字段信息,常量池(constant pool),静态变量,方法信息等。

    public final class Student extends Object implements Serializable { // 1.类信息
        // 2.对象字段信息
        private String name;
        private int score;
     
        // 3.常量池
        public final int id = 0;
        public final String gender = "male";
      
        // 4.静态变量
        public static int a = 0;
        
        // 5.方法信息
        public int getid() {
            return id;
        }
    }
    

    PC寄存器:存放将要执行的指令的地址。(因为机器的脑子不灵活,所以需要一块专门的区域帮他记住执行到哪一步,不然它会忘记)

    本地方法栈:与JVM栈所发挥的作用是非常相似的,其区别不过是JVM栈为Java方法服务,而本地方法栈则是为使用到的Native方法服务。有的虚拟机(例如Sun HotSpot虚拟机)甚至直接就把本地方法栈和虚拟机栈合二为一。

    每个线程拥有各自独立的(虚拟机)栈、PC寄存器和本地方法栈。而堆和方法区则是所有线程共享的。

    最后让我们通过一个小例子来理解Java程序执行时内存的变化。

    public class Person {
        int id;
        int age;
      
        Person(int id1, int age1) {
            id = id1;
            age = age1;
        }
      
        public static void main(String[] args) {
            Person Tom = new Person(1, 25);
        }
    }
    

    首先,式子的左侧Person tom代表:在stack中申请了一块内存,这块内存区域名字叫Tom,此时区域里存储的内容undefined(注意:不是null)。

    接着,式子的右侧new Person:通过new关键字,就会在heap中new出一个对象,连带着对象中的成员变量一块构建好,并赋予它们默认值,如0,0。

    接着会自动调用Person的构造方法,为什么叫构造方法呢,因为它是用来帮助我们构造一个对象的。在构造方法中,完成对成员变量的赋值。

    但别忘了,构造方法是有参数的,在Java里,方法的参数属于局部变量,因此在stack中有两块区域分别存放id1和age1。

    最后,将这个对象的引用值(类似于地址)传递给Tom,通过引用值我们就可以找到这个对象。

    (注意:位于stack中的id1和age1会随着构造方法调用的结束而消失,这里为了更好地表现全过程,因此保留在图中。)

    关于栈和堆的其他小知识

    • 栈和堆的大小都是固定为一个默认值的,它们作为jvm的参数设定好了,不同的jvm设定的参数不同,相应的栈和堆的大小也就不同。
    • 栈是运行时的单位:里面存储的信息都是跟当前线程相关的,包括局部变量、程序运行状态、方法返回值等;而堆是存储的单位:它只负责存储对象。
    • 当一个方法调用结束后,方法里的局部变量会随之消失,不会在stack中继续占用空间。
    • 栈与堆的分离使得不同线程可以访问同一个对象,这是一种有效的数据交互方式(共享内存);此外也节省了空间,因为堆中的共享常量和缓存可以被所有栈访问。

    参考

    1. https://simplesnippets.tech/execution-process-of-java-program-in-detail-working-of-just-it-time-compiler-jit-in-detail/
    2. https://blog.csdn.net/yfqnihao/article/details/8289363
    3. https://www.zhihu.com/question/29833675

     
    有问题欢迎大家在评论区留言,转载请注明出处。

  • 相关阅读:
    WPF入门教程系列六——布局介绍与Canvas(一)
    WPF入门教程系列五——Window 介绍
    WPF入门教程系列四——Dispatcher介绍
    WPF入门教程系列三——Application介绍(续)
    html5 标签
    html5
    sublime汉化教程
    html5 文本格式化
    主键和索引的区别
    响应式布局的开发基础知识
  • 原文地址:https://www.cnblogs.com/linj7/p/14122919.html
Copyright © 2011-2022 走看看