文章目录
一、前言
1.1 什么是JVM
- JVM:JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的
个人理解:Java非常重要的特性就是平台无关性,即一份代码不用做任何修改即可在众多不同的平台上运行。之所以Java能够实现该特性,就是因为有Java虚拟机。Java的源代码(即*.java文件)首先生成一种与平台无关的字节码(*.class文件),然后字节码交给Java虚拟机进行解释执行。不同平台会有不同的虚拟机,但是都向编译器提供相同的编程接口。因此,在编写Java代码时只需要面向虚拟机即可,这样就做到了平台无关性
-
编译型语言::高级语言按程序的执行方式可以分为编译型和解释型两种。编译型语言指的是使用专门的编译器,针对特定平台(即操作系统)将某种高级语言的源代码一次性“翻译”成可被该平台硬件执行的机器码(包括机器指令和操作数),并包装成该平台所能识别的可执行性程序的格式,这个转换过程被称为“编译”(compile)。编译生产的可执行性程序可以脱离开发环境在特定的平台上独立运行(只需要运行环境不需要开发环境)。并且在编译结束之后可以通过链接其他编译好的目标代码生成最终的可执行性程序实现低层次的代码复用
-
解释型语言:解释型语言是指使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行的语言。解释型语言通过不会进行整体性的编译和链接处理,它相当于把编译型语言中的编译和解释的过程混合在一起同时完成。与编译型语言相比,解释型语言的运行效率更低,而且不会脱离解释器独立运行,但是跨平台比较容易,只需要提供特定平台的解释器即可,也即以牺牲程序执行效率为代价方便地实现了源程序级的移植
-
JAVA既不是纯粹的解释型语言也不是纯粹的编译型语言
1.2 为什么要学习JVM
- 根据上文的解释,Java其实是面向Java虚拟机的编程,熟练掌握很多技术特性的底层是如何由虚拟机实现的,可以更好的了解这些技术特性的本质,从而更好的运用这些特性
1.3 为什么要学习JVM的内存管理
- 与C++自己管理内存不同,Java的内存由JVM管理。因此如果要想要更好的进行内存管理,或者出现了内存泄露和溢出方面的问题,都需要了解JVM是如何分配、使用、回收内存的
1.4 参考文献与环境依赖
- 《深入理解Java虚拟机》第二版 周志明著
- 《疯狂Java讲义》第三版 李刚著
- OPEN JDK 7
- Sun HotSpot虚拟机
二、JVM的内存区域
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进去,墙内的人想出来
- 本章节的主要内容:
– JVM在运行时将内存分为哪几块?
–每块内存的私有性和生存周期是怎么样的?
– 每块内存有什么特性,会抛出哪些内存错误?
2.1 程序计数器
- 含义:是当前线程所执行的字节码的行号指示器。从概念上说(实际可能会有更高效的实现方式),字节码解释器通过改变程序计数器所指向的字节码来选取下一条字节码指令
- 私有性:线程独享。每个线程都有自己的程序计数器,各程序计数器互不影响、独立存储
- 生存周期:与该线程相同
- 特点:由于只是起到一个计数器的作用,程序计数器只占有一块非常小的内存区域。之所以需要该计数器,是因为在多线程运行时,每个处理器(更精确地说法是每个内核)只会执行一条线程中的执行,其他线程可能会出现等待阻塞的状态,为了在进行线程切换时恢复现场,因此需要记录每个线程中执行的执行情况
- 内存错误:JVM规范中没有规定任何内存错误。即认为程序计数器只占有一块非常小的内存区域,不应该产生任何内存泄露、溢出等内存错误
2.2 虚拟机栈
- 含义:描述的是Java方法执行的内存模型,即每个方法在执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成,对应着一个栈帧在虚拟机栈中从入栈到出栈
- 私有性:线程独享
- 生存周期:与该线程相同
- 特点:局部变量表存放的是各种编译期可确定的数据,包括基本数据类型、对象引用(即一个指针或者句柄)和returnAdress类型(指向了一条字节码指令的地址),由于编译期可确定,因此该局部变量表的大小在帧中所需要分配的空间也是完全确定的,在方法运行期间不会改变
个人理解:人们常常模糊的把Java运行时的内存分为栈和堆,这里的“栈”即是指虚拟机栈(中的局部变量表)。当一个对象调用一个方式时,在内存中的过程就是在虚拟机栈中创建一个栈帧,当该方法执行完成时该栈帧会弹出出栈
- 内存错误:当线程请求栈深大于虚拟机允许的栈深度时,会抛出StackOverflowError异常;当允许动态扩展内存的JVM在扩展时无法申请到足够的内存时,会抛出OutOfMemoryError异常
2.3 本地方法栈
- 含义:与虚拟机方法栈类似,只不过虚拟机栈为执行Java方法服务,本地方法栈为虚拟机使用到的native服务;
- 私有性:线程独享
- 生存周期:与该线程相同
- 特点:任何本地方法接口都会使用某种本地方法栈。当虚拟机调用java方法时,虚拟机会创建一个栈帧并且压入虚拟机栈;当虚拟机调用本地(native)方法时,虚拟机不会创建新的栈帧,虚拟机栈会保持不变,虚拟机只是简单的动态连接并直接调用相关的本地方法;如果本地方法接口是c连接模型的话,它的本地方法栈就是c栈。当c程序调用一个c函数时,传递给该函数的参数以相应的顺序压入栈,它的返回值以确定的方式返回给调用方。这就是虚拟机实现中本地方法栈的行为;
- 内存错误:与虚拟机栈相同。当线程请求栈深大于虚拟机允许的栈深度时,会抛出StackOverflowError异常;当允许动态扩展内存的JVM在扩展时无法申请到足够的内存时,会抛出OutOfMemoryError异常
2.4 Java堆
- 含义:是存放对象实例,(几乎)所有的对象都要在这里分配内存
- 私有性:所有线程共享
- 生存周期:JVM启动时创建,关闭时销毁
- 特点:是垃圾收集器管理的主要区域
- 内存错误:如果堆中没有内存完成实例分配并且堆也无法再扩展时,会抛出OutOfMemoryError异常
2.5 方法区
- 含义:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 私有性:所有线程共享
- 生存周期:JVM启动时创建,关闭时销毁
- 特点:运行时常量池是方法区的一部分,主要用于存储Class文件中的常量池,即编译期生成的各种字面量和符号引用。运行时常量池具有动态性,即运行期间也可能将新的常量放入常量池
个人理解:类的静态变量和实例变量虽然写法类似,但是在内存中分别被存储于不同的区域,静态变量存储在方法区,实例变量存储在堆上
- 内存错误:如果方法区无法满足内存分配的需求时,会抛出OutOfMemoryError异常
2.6 直接内存
- 含义:JDK1.4 中加入了NIO类,允许使用Native函数库直接分配堆外内存,然后通过堆上的队形作为对这块内存的引用进行操作,这块内存就是直接内存。这种技术的优点是直接操作堆外内存,避免了一些场景中在Java堆和Native堆上来回传数据的操作
- 特点:直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。直接内存不受Java堆的大小限制,只与运行该JVM的物理内存有关
- 内存错误:当直接内存占用过大时,会导致虚拟机进行内存的动态扩展时出现内存溢出异常
三、JVM的对象
此处仅讨论普通对象,不包括数组对象、Class对象
3.1 对象创建的具体过程
- 当虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否被加载、解析和初始化过。如果没有,需要先执行相应的类加载过程
- 类加载检查通过之后,虚拟机会为新生对象分配内存。由于对象所需要的内存大小是已知的,因此该步骤即是在Java堆上划分出一块确定大小的内存块。分配方式主要有指针碰撞和空闲列表两种,主要取决于虚拟机采用的垃圾收集器是否带有压缩整理功能。除此之外要考虑的问题是线程安全性,解决方案有CAS配上失败重试和划分出本地线程分配缓冲(TLAB)两种
- 在内存分配完成之后,进行初始化为零值
- 最后执行方法里程序员以代码形式给出的初始化
3.2 对象在内存中的格式
- 一个对象在内存中可以被分为三块区域,分别是对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。以下分别简介每一块区域
- 对象头:对象头由两部分构成。第一部分是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、线程相关状态等信息,第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例
- 实例数据:存储对象的各种数据,即程序代码中所定义的各个类型的字段内容
- 对齐填充:由于自动内存管理系统要求对象的大小必须是8字节的整数倍,当实例数据部分没有对齐时就是用对齐填充来补全;
3.3 如何访问对象
- Java程序通过栈上的引用类型数据来操作堆上的具体对象,但是由于只有一个引用类型数据,并没有定义具体的如何去访问,因此访问方式由虚拟机实现决定,目前主流的方式主要有两种
- 第一种是使用句柄。虚拟机如果采用了这种实现方式,会将堆分为两块,一块用作句柄池,一块作为实例池来存放具体的对象实例数据(同时方法区里还存有对象类型数据)。此时Java栈的本地变量表里存的引用类型数据是句柄地址,通过句柄地址再找到对象实例数据和类型数据。优点是由于垃圾收集时移动对象进行压缩整理的行为是非常普遍的,此时只需要改变句柄中的指针,而帧上的引用类型数据不需要任何更改,缺点是进行两次地址访问才能找到想要的数据,会相对较慢
- 第二种是直接指针访问,即在堆上的队形实例数据里存放指向方法区对象类型数据的地址,栈上指针直接指向堆上的对象实例数据地址。优点是速度快
本文详细讲解了Java虚拟机(JVM)的基本概念、内存区域划分及其管理机制,包括程序计数器、虚拟机栈、本地方法栈、Java堆、方法区和直接内存的特点及内存错误。此外,还介绍了对象的创建过程、内存格式以及访问方式。
168

被折叠的 条评论
为什么被折叠?



