JVM学习第三篇思考:一个Java类在Jvm内存中是怎么存在的
又名:Java虚拟机的内存模型(JMM)是什么样的.
通过前面两篇文章的学习,我们知道了一个Java类的生命周期及类加载器。我们可以得到如下两幅图:
类生命周期:
父类委托机制:
思考:
我们编写的类中的变量、方法、对象这些都需要内存存放的。那么在运行时候这些数据在Java虚拟机内存中是怎么存放的呢?
本文目标:
凯哥(凯哥Java:kaigejava)希望通过本文学习,大家对Java虚拟机运行时数据区域有更深的了解
我们写的代码在JVM中是怎么存在的?
1:我们现在看看总体Java运行时数据模型:
2:我们来看看下面这段代码,执行的时候,在JVM中数据存放:
上面代码很简单,那么对应的变量、对象等在内存中都是怎么分配的呢?
2.1:方法区
注:在JDK1.8之后,方法区被元空间替换了。
方法区:用来存放的是类的信息、常量、静态变量等。该区域也是各个线程共享的内存区域。
根据Java虚拟机规范中的规定,当方法去无法满足内存分配的时候,会抛出:OutOfMemoryError异常的。
根据上面的 定义,我们可以知道比如我们JvmDemo.class信息、static string str=“jvmDemo”是在方法区存放的。
对应咱们代码,方法区存放的如下图:
2.2:堆区
堆区是JVM所管理的内存中的最大的一块区域。该区域是所有线程共享的一块内存区域。该区域空间在虚拟机启动的时候就被创建了(-Xms的设置。后面凯哥(凯哥Java:kaigejava)也会详细讲解的)。
此区域的目的是存放对象实例的。几乎所有的对象实例都是在这里分配的。Java虚拟机规范中是这么描述的:所有的对象的实例以及数组都要在堆上分配。
堆区是垃圾收集器管理的主要区域(后面凯哥(凯哥Java:kaigejava)也会详细讲解的).
堆区空间,在物理上可以不是连续的内存空间,只要在逻辑上是连续的即可。如果堆没有内存完成实例分配,并且堆也无法在扩展的时候,将会抛出异常:OutOfMemoryError。这个大家很熟悉吧。
根据上面定义的,我们可以知道,上面代码中Son son = new Son(); 这行代码创建的实例对象是存放在堆区的。
2.3:程序计数器
程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器在工作的时候,时候通过改变计数器的值来选择接下来要执行的字节码指令的。
同时我们都知道,当多线程的时候,Java虚拟机是通过线程轮流切换分配处理器执行时间的方式来实现的。在任何一个确定的时刻一个处理器只会执行一条线程中的指令。因此,为了解决多个线程在切换后,能够迅速恢复到切换前执行的位置,每个线程都需要有个独立的程序计数器,各个线程直接的计数器互不影响,独立存储的。一般称这类内存区域为:"线程私有"的内存。当线程正在执行的一个方法是Native的,这种情况下,计数器的值就是undefined了。这个区域也是Java虚拟机内存区域中唯一一个没有OOM的区域。
根据上面描述,我们可以知道,我们自己编写的*.java文件要想被执行,需要被编译成*.class的字节码文件。字节码文件对应各种字节码指令。比如我们上面JvmDemo的字节码文件:
从上面截图,我们可以看到,行号是0,3,4,7,8这样的。程序计数器就是记录这些行号的
我们也可以使用idea的插件,来查看我们JvmDemo的相关信息:
2.4:虚拟机栈
Java虚拟机栈,也是线程私有的。其生命周期与线程相同,当一个线程运行结束后,对应的虚拟机栈也结束。
虚拟机栈是Java方法执行的内存模型:即每个方法被执行的时候,都会被同时创建一个栈帧(Stack Frame),这个栈帧是用来存放方法局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直到其执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
比如:我们上面代码执行的时候,执行main方法的时候,主线程就会把main方法压入到虚拟机栈中,当执行到add方法的时候,add方法就被压入到栈中了。当执行完add方法后,add方法就被从虚拟机栈中弹出,这个时候add对应的栈帧也销毁。
虚拟机栈帧如下图:
局部变量存放:各种基本数据类型、对象引用和返回类型
八大基本数据类型:boolean、byte、char、short、init、float、long、double;
对象引用:reference类型。这里是存放的是对象在对内存中的地址值。不等同于对象自身的。根据不同的虚拟机的实现,这个指向可能是指向了对象起始地址的引用指针,也有可能是指向了对象对象的句柄或其他对象与其他对象的位置;
返回类型:returnAddress类型。指向一条字节码指令地址。
扩展:long类型和double类型的数据会占用2个局部变量空间。其他6个数据类型占用1个。
局部变量表所消耗的内存空间在编译期间就完成了分配,当进入一个方法的时候,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的。在方法的运行期间,不会改变该区域空间大小的。
在咱们上面代码中,虚拟机栈存放的就是咱们main方法和add方法相关的
2.5:本地方法栈
本地方法栈的作用和虚拟机栈的作用相似。不同之处在于:虚拟机栈是为了虚拟机执行Java方法服务的。而本地方法栈则是为了虚拟机使用到Native方法服务的。此区域也是方法私有的。比如我们调用线程的run方法或者CAS的时候,调用的都是native方法。
总结:
通过本文学习,我们在自己脑海中应该有如下图的概念:
其中方法区、堆区是所有线程共享的;虚拟机栈、程序计数器、本地方法栈这三个是线程私有的,其生命周期同线程一致。
方法区:存放类型、常量、静态变量等
堆区:用来存放对象实例、数组
虚拟机栈:局部变量表、动态链接、操作栈等
本地方法栈:用来存放当线程调用native方法的时候使用的
程序计数器:用来记录当前线程执行的字节码行号的。
好了,本文凯哥就和大家唠唠在运行时候Java虚拟机的数据区域。在下篇文章中,咱们在详细唠唠堆区。