JVM对于是每个Java程序员掌握一定Java基础后,都需要学习的。因为很多代码问题,只能了解了JVM底层原理后才能解决。大多数Java后端开发者都知道堆(Heap)和栈(Stack)的概念,却没有真正理解其原理。推荐《深入理解Java虚拟机(第二版)》---周志明著学习JVM。
进程和线程
学习JVM前要了解进程和线程的概念。
以下是一个类比,来自阮一峰---进程与线程的一个简单解释。
- 计算机的CPU是像一座工厂,时刻在运行。
- 因工厂电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
- 进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
- 一个车间里,可以有很多工人。他们协同完成一个任务。
- 线程就好比车间里的工人。一个进程可以包括多个线程。
进程和线程里面还涉及到“锁”的知识,请在参考网址中学习。
下图是:Java虚拟机运行时数据区
程序计数器(Program Counter Resiger)
首先程序计数器是一块较小的内存空间,“决定”当前线程字节码的执行顺序,因为它存储字节码的行号。而在多线程中,每个线程都具有一个程序计数器,各条线程之间独立,计数器互不影响,独立存储。程序计数器是“线程私有”的内存。
如果线程执行Java方法,计数器记录的是虚拟机字节码指令的地址。而如果执行Native方法,值为空(Undefined),理解这一段文字需要理解Native方法是什么!Native方法是非Java代码(比如C)编写的方法。程序计数器是Java虚拟机规范中唯一没有OutOfMemoryError的内存区域。
Java虚拟机栈(Java Virtual Machine Stacks)
虚拟机栈是Java方法(也就是字节码)运行时的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)[帧是一种数据结构]用于存储局部变量表等。它是“线程私有”的,生命周期和线程相同。
我们常说的栈(Stacks)其中一种含义就是Java虚拟机栈,确切的说是虚拟机栈中局部变量表部分。局部变量表存放原始(基本)数据类型,其中Long和double占两个局部变量空间(Slot)-32位。也存放对象引用和ruturnAddress类型(指向了一条字节码指令的地址?)。
局部变量表所需的内存空间在编译期间完成分配,当进入(运行)一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,这是因为局部变量表是有结构的,每个区块按照一定次序存放,所以可以明确知道每个区块的大小。局部变量就是在方法运行期间不会改变局部变量表的大小。
Java虚拟机规范中规定此区域有两种异常:
- 每次方法调用都会有一个栈帧压入虚拟机栈,JVM分配给虚拟机栈的内存是有限的。如果方法调用过多,导致虚拟机栈满了就会溢出。如果线程请求的栈深度(栈帧的数量)大于虚拟机所允许的深度(虚拟机栈的内存),将抛出StackOverflowError异常
- 虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈(Native Method Stack)
本地方法栈与Java虚拟机栈类似,是虚拟机使用到的Native方法服务。Sun HotSpot虚拟机直接本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆(Java Heap)
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。是被所有线程共享的一块内存区域。存放对象实例和数组(数组也是对象),现在不是那么“绝对”了。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。
根据Java虚拟机规范,Java堆只要逻辑上是连续的即可,就像磁盘空间。可以实现成固定大小的,也可以是可扩展的(大部分虚拟机都可动态扩展)。如果堆没有内存完成实例分配,且堆也无法再扩展时,会抛出OutOfMemoryError异常。
方法区(Method Area)
方法区(Method Area)不是存储方法,而是用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范将方法区描述为堆的一个逻辑部分,是逻辑区,但为区分堆,别名叫Non-Heap(非堆)。
方法区(Method Area)一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。此区域的内存回收目标主要是针对常量池的回收和(对类型的卸载?)。也可能抛出OutOfMemoryError异常。
运行时常量池(Runtime Constant Pool)
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References),这部分内容将在类加载后进入方法区的运行时常量池中存放。
字面量比较接近于Java语言层面的常量概念,如文本字符串、 声明为final的常量值等。 而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
对于运行时常量池,Java虚拟机规范没有做任何细节的要求。运行时常量池不同于Class文件常量池,运行期间也可能将新的常量放入池中。内存不足时会抛出OutOfMemoryError异常。
直接内存(Direct Memory)
直接内存通俗来说就是I/O方式使用Native堆直接分配堆外内存。
总结
一般来说,每个进程分配一个"Heap",每个线程分配一个"Stack"。因为一个进程中有很多线程,所以Java堆等是线程共享的;而"Stack"的生命周期跟线程相同,即"Stack"是线程独占的,所以程序计数器、Java虚拟机栈等是线程私有的。
image"Stack"的另外两种含义有:
- 一种数据结构,即一组数据的存放方式,特点是FILO---First In,Last Out(先进后出)
- 一种代码运行方式,即"调用栈"(call Stack),表示函数或子例程像堆积木一样存放,以实现层层调用。
"Stack"和Heap的主要区别是:"Stack"是有结构的,"Heap"是没有结构的。因此,"Stack"的寻址速度要快于"Heap"。
线程共享的好处:任何时候一个线程改变一些数据,其他线程可以看到它。
?号部分通读全文理解后解释。
网友评论