所谓JVM就是JAVA虚拟机(Java Virtual Machine)。
Java的特点就是“一次编译,到处运行”。这就是JVM做到的,JVM就是一台虚拟的计算机,把具体的机器指令屏蔽起来,用自己独有的一套东西。开发者编写的程序经过编译器生成Java虚拟机上运行的目标代码(字节码),就可以无视平台,带来的弊端也显而易见,Java虚拟机在执行字节码时,也需要把字节码解释成具体平台上的机器指令执行。
举个例子:我们吃饭,西餐用刀叉,吃中餐用筷子,吃汉堡直接用手,我们需要针对吃不同的东西选择不同的方式,这就相当于我写Windows程序,或者写Mac程序,亦或者写Android程序,写不同的程序我要用不同的语言和编辑器。JVM是什么呢?就相当于我吃饭工具的一个盒子,里面有各种各样的工具,我不需要考虑我吃的是什么,吃就完了,反正有盒子替我考虑。
java在运行时,jvm(即java虚拟机)就会拿到操作系统给他分配的内存区域。
这内存区域分为五个,他们分别是:
栈区(也叫虚拟机栈)、堆区、本地方法栈、程序计数器、方法区(也叫元空间)
这五个区域都有自己的用途。它们存储的数据不相同,数据的结构也不同,创建时间和销毁时间仍然不同。
因为它们的执行过程不一样,所以在不影响数据功能的前提下,同时也提高了代码的运行效率。
其中程序员能控制的区域就只有堆区,栈区、方法区。程序计数器和本地方法栈都是自动控制的,程序员控制不了。
在介绍JVM栈之前,我先了解一下栈帧概念。
栈帧:
一个栈帧随着一个方法的调用开始而创建,这个方法调用完成而销毁。栈帧内存放着方法中的局部变量,操作数栈等数据。
Java栈也称作虚拟机栈(Java Vitual Machine Stack),JVM栈只对栈帧进行存储,压栈和出栈操作。Java栈是Java方法执行的内存模型。下面我们来看一个Java栈图。
由上图可以看出,Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。
栈内存的大小可以有两种设置,固定值和根据线程需要动态增长。
在JVM栈这个数据区可能会发生抛出两种错误:
1、StackOverflowError出现在栈内存设置成固定值的时候,当程序执行需要的栈内存超过设定的固定值会抛出这个错误。
2、OutOfMemoryError出现在栈内存设置成动态增长的时候,当JVM尝试申请的内存大小超过了其可用内存时会抛出这个错误。
StackOverflowError和OutOfMemoryError都属于Throwable下的Error。
JVM栈 (Java Virtual Machine Stacks)总结:
1、每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)。对象都存放在堆区中。
2、每个战中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。
3、栈分为3个部分:基本类型变量,执行环境上下文,操作指令区(存放操作指令)。
4、在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。
5、当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
堆数据区是用来存放对象和数组(特殊的对象)。
堆内存由多个线程共享。堆内存随着JVM启动而创建。
众所周知,Java中有一个很好的特性就是自动垃圾回收。
垃圾回收就操作这个数据区来回收对象进而释放内存。
如果堆内存剩余的内存不足以满足于对象创建,JVM会抛出OutOfMemoryError错误。
堆内存 (Heap Memory)总结:
1、存储的全部是对象,每个对象包含一个与之对应的class信息–class的目的是得到操作指令。
2、jvm只有一个堆区(heap)被所有线程共享,堆区中不存放基本类型和对象引用,只存放对象本身。
3、堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。
4、缺点是,由于要在运行时动态分配内存,存取速度较慢。
方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
一个支持native方法调用的JVM实现,需要有这样一个数据区,就是本地方法栈,Java官方对于本地方法的定义为methods written in a language other than the Java programming language,就是使用非Java语言实现的方法,但是通常我们指的一般为C或者C++,因此这个栈也有着C栈这一称号。一个不支持本地方法执行的JVM没有必要实现这个数据区域。
本地方法栈基本和JVM栈一样,其大小也是可以设置为固定值或者动态增加,因此也会对应抛出StackOverflowError和OutOfMemoryError错误。
在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
在通用的计算机体系中,程序计数器用来记录当前正在执行的指令,在JVM中也是如此。
程序计数器是线程私有,所以当一个新的线程创建时,程序计数器也会创建。
由于Java是支持多线程,Java中的程序计数器用来记录当前线程中正在执行的指令。如果当前正在执行的方法是本地方法,那么此刻程序计数器的值为undefined。
注意这个区域是唯一一个不抛出OutOfMemoryError的运行时数据区。