java虚拟机内存模型

 2019-12-10 11:21  阅读(567)
文章分类:Java Core

java虚拟机内存模型是java程序运行的基础。

java虚拟机将其内存数据分为程序计数器、虚拟机栈、本地方法栈、java堆和方法区。

如果根据受访权限的不同我们可以定义上述几个区域分为线程共享和线程私有两大类。线程共享指的是可以允许被所有线程共享访问的一类内存区这类区域包括堆内存区、方法区、运行时常量池三个内存区。

简单一句话描述用途,程序计数器用于存放下一条运行的指令,虚拟机栈和本地方法栈用于存放函数调用堆栈信息,java堆用于存放java程序运行时所需的对象等数据,方法区用于存放程序的类元数据信息。

20191210001118\_1.png

1. 程序计数器

当线程数量超过CPU数量时,线程之间会自动根据时间片轮询方式抢夺CPU资源。对于单核CPU而言,每一个时刻只能有一个线程处于运行状态,其他线程必须被切换出去,直到轮询轮到自己才能使用CPU资源,为此,每一个线程都必须用一个独立的程序计数器,他被用来记录下一条需要执行的计算机指令。对于多核CPU来说,可以允许多个线程同时进行,各个线程之间的计数器互不影响,独立工作,所以程序计数器是线程独有的一块内存空间。为了让线程切换后能恢复到正确的执行位置,每条线程要有一个独立的程序计数器。

2.虚拟机栈

该区域位于通用RAM里,通过所谓的“栈指针”可以让我们访问处理器,存取速度仅次于寄存器。

保存局部变量的值,包括:1.用来保存基本数据类型的值;2.保存类的实例,即堆区对象的引用(指针)。也可以用来保存加载方法时的帧(栈帧)。

栈帧: 保存一个方法的相关信息。每个方法调用时都伴随着入栈,每个方法返回时都伴随着出栈。

20191210001118\_2.png

虚拟机栈代码示例:

1 import java.util.concurrent.CountDownLatch;
     2 public class TestJVMStack {
     3 private int count  = 0;
     4 public void recursion() {
     5     count ++ ; //栈深度加 1
     6 
     7     recursion();//递归消耗栈内存分配
     8 }
     9     public void testStack() {
    10         
    11         try {
    12             recursion();
    13         } catch (java.lang.StackOverflowError e) {
    14             System.out.println("打印出栈溢出的深度:"  + count);            
    15             e.printStackTrace();
    16         }    
    17     }
    18     public static void main(String[] args) {
    19         TestJVMStack   ts  = new TestJVMStack();
    20         ts.testStack();
    21     }
    22 }

运行结果:

1 打印出栈溢出的深度:18664
    2 java.lang.StackOverflowError
    3     at TestJVMStack.recursion(TestJVMStack.java:8)
    4     at TestJVMStack.recursion(TestJVMStack.java:8)
    5     at TestJVMStack.recursion(TestJVMStack.java:8)
    6     at TestJVMStack.recursion(TestJVMStack.java:8)

3.本地方法栈

本地方法栈和java虚拟机栈的功能很相似,java虚拟机栈用于管理java函数的调用,而本地方法栈用于管理本地方法的调用。本地方法并不是用java实现的而是使用c实现的。当某个线程调用一个本地方法时,他就进入了一个全新的世界并且不再受java虚拟机限制的世界,本地方法可以通过本地方法接口来访问,虚拟机运行时的数据区,但不止于此,他还可以做任何想做的事情。比如,它甚至可以直接使用本地处理器中的寄存器,或者直接从本地内存的堆中分配任意数量的内存。总之,它和虚拟机拥有同样的权限。

本地方法本质上是依赖于实现的,虚拟机的设计者可以自由的决定使用怎样的机制来让java程序调用本地方法,任何本地方法接口都会使用某种本地方法栈。

4.java堆

堆在JAVM规范里是一种通用性的内存池(也存在于RAM中),用来存放动态产生的数据,比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。

堆不同于栈的好处是,编译器不需要知道,要从堆里分配多少存储区域,也不需要知道存储的数据在堆里需要存活多长时间,因此堆相对于栈来说,有很大的灵活性。

java堆区在JVM启动时被创建,它只要求逻辑上是连续的,在物理空间上可以是不连续的。所有的线程共享java堆,在这里可以划分线程私有得缓冲区。

5.方法区

方法区主要保存的信息是类的元数据。方法区与堆空间类似,他也是被JVM中所有线程共享的区域。

方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。

方法区是系统分配的一个内存逻辑区域,是用来存储类型信息的(类型信息可理解为类的描述信息)。方法区主要有以下几个特点:
**一.**方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待
**二.**方法区的大小不必是固定的,JVM可根据应用需要动态调整。同时,方法区也不一定是连续的,方法区可以在一个堆(甚至是JVM自己的堆)中自由分配。
**三.**方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集

方法区里存放的是哪些内容?
方法区里存的都是类型信息,也就是类的信息,而类的信息又包括以下内容:
类的全限定名(类的全路径名)
类的直接超类的全限定名(如果这个类是Object,则它没有超类)
这个类是类型(类)还是接口
类的访问修饰符,如public、abstract、final等
所有的直接接口全限定名的有序列表(假如它实现了多个接口)

  1. 常量池
    字段、方法信息、类变量信息(静态变量) 装载该类的装载器的引用(classLoader)、类型引用(class)

    7.JVM运行原理 例子

以上都是纯理论,我们举个例子来说明 JVM 的运行原理,我们来写一个简单的类,代码如下:
20191210001118\_3.png
20191210001118\_4.png

1 public class JVMShowcase {  
     2 //静态类常量,  
     3 public final static String ClASS_CONST = "I'm a Const";  
     4 //私有实例变量  
     5 private int instanceVar=15;  
     6 public static void main(String[] args) {  
     7 //调用静态方法  
     8 runStaticMethod();  
     9 //调用非静态方法  
    10 JVMShowcase showcase=new JVMShowcase();  
    11 showcase.runNonStaticMethod(100);  
    12 }  
    13 //常规静态方法  
    14 public static String runStaticMethod(){  
    15 return ClASS_CONST;  
    16 }  
    17 //非静态方法  
    18 public int runNonStaticMethod(int parameter){  
    19 int methodVar=this.instanceVar * parameter;  
    20 return methodVar;  
    21 }  
    22 }

20191210001118\_5.png
20191210001118\_6.png 这个类没有任何意义,不用猜测这个类是做什么用,只是写一个比较典型的类,然后我们来看 看 JVM 是如何运行的,也就是输入 java JVMShow 后,我们来看 JVM 是如何处理的:
第 1 步

**向操作系统申请空闲内存。**JVM 对操作系统说“给我 64M(随便模拟数据,并不是真实数据) 空闲内存”,于是,JVM 向操作系统申请空闲内存作系统就查找自己的内存分配表,找了段 64M 的内存写上“Java 占用”标签,然后把内存段的起始地址和终止地址给 JVM,JVM 准备加载类文件。
第 2 步,
**分配内存内存。**JVM 分配内存。JVM 获得到 64M 内存,就开始得瑟了,首先给 heap 分个内存,然后给栈内存也分配好。
第 3 步,
**文件检查和分析class 文件。**若发现有错误即返回错误。
第 4 步,
**加载类。**加载类。由于没有指定加载器,JVM 默认使用 bootstrap 加载器,就把 rt.jar 下的所有类都加载到了堆类存的Method Area,JVMShow 也被加载到内存中。我们来看看Method Area区域,如下图:(
这时候包含了 main 方法和 runStaticMethod方法的符号引用,因为它们都是静态方法,在类加载的时候就会加载
20191210001118\_7.png Heap 是空,Stack 是空,因为还没有对象的新建和线程被执行。
第 5 步、
**执行方法。**执行 main 方法。执行启动一个线程,开始执行 main 方法,在 main 执行完毕前,方法区如下图所示: (
public final static String ClASS_CONST = “I’m a Const”;
20191210001118\_8.png 在 Method Area 加入了 CLASS_CONST 常量,它是在第一次被访问时产生的(runStaticMethod方法内部)。 堆内存中有两个对象 object 和 showcase 对象,如下图所示:(执行了JVMShowcase showcase=new JVMShowcase(); )
20191210001118\_9.png 为什么会有 Object 对象呢?是因为它是 JVMShowcase 的父类,
JVM 是先初始化父类,然后再初始化子类,甭管有多少个父类都初始化。 在栈内存中有三个栈帧,如下图所示:
20191210001118\_10.png 于此同时,还创建了一个程序计数器指向下一条要执行的语句。
**第 6 步,**释放内存。释放内存。运行结束,JVM 向操作系统发送消息,说“内存用完了,我还给你”,运行结束。

——————————————————————————————–

现在来看JVM内存是如何分配的

预备知识:

**1.**一个Java文件,只要有main入口方法,我们就认为这是一个Java程序,可以单独编译运行。

**2.**无论是普通类型的变量还是引用类型的变量(俗称实例),都可以作为局部变量,他们都可以出现在栈中。只不过普通类型的变量在栈中直接保存它所对应的值,而引用类型的变量保存的是一个指向堆区的指针,通过这个指针,就可以找到这个实例在堆区对应的对象。因此,普通类型变量只在栈区占用一块内存,而引用类型变量要在栈区和堆区各占一块内存。

示例:(以下所有实例中,是根据需要对于栈内存中的帧栈简化成了只有局部变量表,实际上由上面对帧栈的介绍知道不仅仅只有这些信息,同理堆内存也一样)

20191210001118\_11.png

**1.**JVM自动寻找main方法,执行第一句代码,创建一个Test类的实例,在栈中分配一块内存,存放一个指向堆区对象的指针110925。

**2.**创建一个int型的变量date,由于是基本类型,直接在栈中存放date对应的值9。

**3.**创建两个BirthDate类的实例d1、d2,在栈中分别存放了对应的指针指向各自的对象。他们在实例化时调用了有参数的构造方法,因此对象中有自定义初始值。

20191210001118\_12.png

调用test对象的change1方法,并且以date为参数。JVM读到这段代码时,检测到i是局部变量,因此会把i放在栈中,并且把date的值赋给i。

20191210001118\_13.png

把1234赋给i。很简单的一步。

20191210001118\_14.png

change1方法执行完毕,立即释放局部变量i所占用的栈空间。

20191210001118\_15.png

调用test对象的change2方法,以实例d1为参数。JVM检测到change2方法中的b参数为局部变量,立即加入到栈中,由于是引用类型的变量,所以b中保存的是d1中的指针,此时b和d1指向同一个堆中的对象。在b和d1之间传递是指针。

20191210001118\_16.png

change2方法中又实例化了一个BirthDate对象,并且赋给b。在内部执行过程是:在堆区new了一个对象,并且把该对象的指针保存在栈中的b对应空间,此时实例b不再指向实例d1所指向的对象,但是实例d1所指向的对象并无变化,这样无法对d1造成任何影响。

20191210001118\_17.png

change2方法执行完毕,立即释放局部引用变量b所占的栈空间,注意只是释放了栈空间,堆空间要等待自动回收。

20191210001118\_18.png

调用test实例的change3方法,以实例d2为参数。同理,JVM会在栈中为局部引用变量b分配空间,并且把d2中的指针存放在b中,此时d2和b指向同一个对象。再调用实例b的setDay方法,其实就是调用d2指向的对象的setDay方法。

20191210001118\_19.png

调用实例b的setDay方法会影响d2,因为二者指向的是同一个对象。

20191210001118\_20.png

change3方法执行完毕,立即释放局部引用变量b。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> java虚拟机内存模型

相关推荐