这篇文章一句废话我也不打算说,建议准备好姿势阅读,收藏关注有利于随时学习!
今天看到一个特别有意思的图,想起以前刚学Java的时候听别人聊JVM的时候,大概就是这个样子,哈哈
开头就说了废话,啊呸,进入主题,冲冲冲
JVM是Java Virtual Machine(Java虚拟机)的缩写,任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是一次编译,多次运行
对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作都写配对的delete/free代码(释放内存操作),不容易出现内存泄漏和内存溢出问题,但是并不是代表不会出现,一旦出现这方面问题我们需要知道如何去解决问题
本文将从内存管理、垃圾回收、类文件结构三大方面来让你认识JVM
内存管理
内存管理从运行时数据区域和Java对象两部分介绍下
1、运行时数据区域
JAVA虚拟机在执行Java程序的过程中会把它所管理的内存区域划分为若干个不同的区域来完成各自的作用,运行时数据区主要分为堆、虚拟机栈、本地方法栈、方法区以及程序计数器
这些,如下图所示:
堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。
线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。
可以位于物理上不连续的空间,但是逻辑上要连续
Java堆是垃圾回收的主要区域,从内存回收的角度Java堆可以细分为:新生代和老年代,再细一点可以分为Eden、From Survivor空间和To Survivor空间等。
无论如何划分存储的都是对象的实例,细分的目的是为了更好的管理内存,更好的进行回收内存。如果堆内没有内存完成实例分配,并且堆无法进行扩展的时候(主流虚拟机通过-Xmx和-Xms控制),将会抛出OutOfMemoryError异常
虚拟机栈
线程私有,生命周期和线程一样。虚拟机栈描述的是JAVA方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法的执行就对应这一个栈帧在虚拟机栈中的入栈到出栈的过程
其中,局部变量表存放了编译器可知的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)。
对象引用类型不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象有关的位置(和堆空间为new对象的分配方法有关,而分配方法又和垃圾回收器有关,下面会详细介绍)
这个区域存在两种异常,若线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError;
若虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存,则抛出OutOfMemoryError异常
本地方法栈
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
也会有 StackOverflowError 和 OutOfMemoryError 异常
方法区栈
方法区也是属于各个线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
Java虚拟机规范堆方法区的限制很宽松,除了和堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以不实现垃圾收集。
垃圾收集行为在这个区域是比较少出现的,这个区域的内存回收目标主要是针对常量池的回收和类型的卸载。这部分无法满足内存分配需求时,将抛出OutOfMemoryError 异常
运行时常量池
运行时常量池属于方法区一部分,用于存放编译期生成的class文件的常量池中的各种字面量和符号引用
Java虚拟机对Class文件中每一部分都有严格的规定,Class文件格式固定,下一篇讲。
但对于运行时常量池来说,jvm虚拟机未做任何细节的要求,除了Class文件中描述的符号引用外,还会把翻译出来的直接引用存储在运行时常量池中
运行时常量池相对于Class文件内部的常量池的一个重要特性就是具备动态性,JAVA语言并不要求常量必须在编译器才能产生,也就是并非放入Class文件常量池的内容才能进入方法区的运行时常量池,运行期间也可以将新的常量放入池中。
编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError
程序计数器
程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成,此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
-
虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址\
-
当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined
-
程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个
直接内存
非虚拟机运行时数据区的部分,在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。
可以避免在 Java 堆和 Native 堆中来回的数据耗时操作
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常
2、Java对象
对象的创建
当虚拟机遇到new指令的时候,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。
如果没有,执行相应的类加载。(类的加载过程会在下面讲解)
类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。
接下来就是在堆中划分出一块同样大小的内存出来,这里的划分方法分为两种,一种是“指针碰撞”,另一种是"空闲列表"
- 指针碰撞:若Java堆得内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个分界点的指示器,只需要把指针向一边挪动一块和对象内存大小相等的内存即可
- 空闲列表:Java堆内存不是绝对规整,已使用的内存和空闲内存互相交错,虚拟机维护着一个列表,记录着哪些内存可用,在分配内存的时候在列表中找出一块足够大的内存划分给此对象,并更新列表上的记录
选择哪种方式是由虚拟机内存是否规整决定的,而数据内存是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定的。在使用Serial、ParNew等带Compact过程的收集器的时候,系统采用的是指针碰撞分配内存。
而使用CMS这种基于Mark-Sweep算法的收集器,通常采用空闲列表
这里还存在一个问题,对象创建在虚拟机中是非常频繁的行为,在并发情况下修改指针的行为并不是线程安全的,如何解决?
在并发情况下,可能出现正在给A分配内存,指针还未来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
有两种方案解决这个问题:
- 一种是对分配内存的动作进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
- 另一种是把内存分配的动作划分在不同的空间之中进行,即每个线程都会在Java堆中预先分配一小块内存(本地线程分配缓冲TLAB)。当TLAB使用完再使用同步处理分配内存,虚拟机是否采用TLAB,可通过-XX:+/-Use