JVM-内存模型

我在写程序的时候想过的以下这些问题,不知道大家是不是都是这样:

1. 类是怎么加载的,存储在哪里?类的对象存储在哪里,类和类对象怎么关联、对应的?

2. 方法存在哪里?子类继承父类之后覆盖父类的方法之后JVM什么机制执行子类or父类方法?

3. static变量和普通变量是放着一起吗?

4. 为什么共享变量要做线程同步控制,而函数内变量不用?

5. JVM什么时候会做内存回收,怎么收的?

6. JVM怎么实现平台无关性的?做到一处编写,到处运行的?

下面就是我找了很多资料,写了点程序搞明白这些问题,可能一篇文件写不完,会写很多,最终一定是把所有问题都搞清楚。

1. Java类的编译过程

   机器码:是机器语言的指令集体系结构的表示方式。好比"加"在汇编中用add表示,类似的在这个中则是用1100表示(1100只是举例用,实际不是),操作系统看到1100就会执行add指令。

   Java字节码:相信应该很多人都听过字节码,JAVA不同于c/c++直接编译成机器码,而是编译成字节码,字节码是平台无关的,它是介于Java和机器语言的中间语言,JAVA字节码是Java代码部署的最小单元。这也就解释了上面的第6个问题,Java如何实现平台无关性的,编写好的Java字节码如果在window平台,window平台的JVM(准确说是JRE)会将字节码最后翻译成符合window平台的机器码,在linux平台JVM(准确说是JRE)会将字节码最后翻译成符合linux平台的机器码。

   字节码长什么样呢?我写了段小程序,如下所示:

class Test{
    public static void main(String[] args){
        Apple apple = new Apple("wo");
        apple.eat("jige");
    } 
}

class Apple{
    private String name;
    public Apple(String name){
        this.name = name;
    }
    public void eat(String who){
        System.out.println(who +" eat " + name + "' apple");
    }
}

java命令编译后的字节码如下:

cafe babe 0000 0034 000f 0a00 0300 0c07
000d 0700 0e01 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0004
6d61 696e 0100 1628 5b4c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b29 5601 000a
536f 7572 6365 4669 6c65 0100 0954 6573
742e 6a61 7661 0c00 0400 0501 0004 5465
7374 0100 106a 6176 612f 6c61 6e67 2f4f
626a 6563 7400 2000 0200 0300 0000 0000
0200 0000 0400 0500 0100 0600 0000 1d00
0100 0100 0000 052a b700 01b1 0000 0001
0007 0000 0006 0001 0000 0001 0009 0008
0009 0001 0006 0000 002d 0002 0004 0000
0009 043c 053d 1b1c 603e b100 0000 0100
0700 0000 1200 0400 0000 0300 0200 0400
0400 0500 0800 0600 0100 0a00 0000 0200
0b

诈一看还以为是机器码,但是不是,我们通过javap -c查看汇编语言,如下图所示:

class Test {
  Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class Apple
       3: dup
       4: ldc           #3                  // String wo
       6: invokespecial #4                  // Method Apple."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: aload_1
      11: ldc           #5                  // String jige
      13: invokevirtual #6                  // Method Apple.eat:(Ljava/lang/String;)V
      16: return
}

以上就是平台无关性的字节码,Java 字节码指令集包括操作码和操作数,上面的指令就是JVM编译指令。

2. 类加载及运行过程

  1. Test类加载:在编译好在命令行敲java Test,系统会启动一个jvm进程,jvm从classpath类Test所在路径中找到Test.class字节码文件,将Java的类信息加载到运行时数据区的方法区中

  2. 执行main:jvm找到Test的main方法,开始执行;

  3. main方法执行 new Apple时,JVM发现没有Apple类信息,所以JVM马上加载Apple类,在Apple类信息放到方法区,初始化Apple类对象(Class extend Object)。

  4. 加载完Apple类之后,JVM开始在堆上为Apple对象分配内存,然后调用构造函数初始化Apple实例,这个Apple实例持有指向方法区的Apple类的类型信息的引用(方法表,java动态绑定的实现)。

 5. 当调用apple.eat("jige") 的时候,JVM会根据apple引用找到Apple对象,然后在Apple对象中引用找到方法区Apple类的方法表,获取eat() 函数的字节码的地址,在线程私有的线程栈中push栈帧,运行。

这个流程回答了上面第1个问题。

3. Java内存模型

   1. Java虚拟机在执行Java程序时会把内存分成若干个不同的数据区域,如下图所示,内存分成线程私有(红色)的和线程共有的(绿色)。

  2. 程序计数器

      程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。JVM的程序计数器和机器语言的PC程序计数器是不一样的,PC程序计数器是CPU中的寄存器,保存的是程序下一条指令的地址。

   3.  Java虚拟机栈

  与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,我觉得应该换个名,应该叫做线程栈,所以实际上是有多少个线程,就有多少个虚拟机栈,虚拟机栈生命周期和线程相同,线程产生时,虚拟机栈随之产生,线程销毁时,虚拟机栈也会销毁。每个方法被执行时,同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、指向当前方法所属类的运行时常量池(方法区)的引用,方法返回地址push到栈中,执行完毕之后pop出栈。

     3.1 局部变量表:用来存储函数中的局部变量,包括函数中定义的非静态变量和函数形参,基本变量直接存值,引用类型存储指向对象的引用,局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

     3.2 操作数栈:一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

   3.3 指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

   3.4 方法返回地: 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

 4.   本地方法栈

  本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。也就是说本地方法也会创建栈帧,押栈出栈,线程私有。

 5. 堆

  在C语言中,堆这部分空间是唯一一个程序员可以管理的内存区域。程序员可以通过malloc函数和free函数在堆上申请和释放空间。那么在Java中是怎么样的呢?

  Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。

 6.  方法区

  方法区是JVM中非常非常重要的区域,取名为方法区我觉得不是很恰当,应该存放的内容是以Class为单元的,存放了每个类的信息(类名、方法信息、字段信息)、运行时常量池(静态变量、常量、字面量和符号引用),编译后的代码。

     在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。

欢迎关注Java流水账公众号
原文地址:https://www.cnblogs.com/guofu-angela/p/9384039.html