基本概念
JVM是可运行Java代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM是运行在操作系统之上的,它与硬件没有直接的交互。
JVM 类加载机制
加载
- 通过类的全路径名,获取类的二进制数据流。
- 获得了类的信息,解析类的数据流,转化为⽅法区内部的数据结构。
- 创建java.lang.Class类的实例
验证
这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证的步骤:
- 格式的验证:验证类文件的魔术版本号常量等是否符合当前虚拟机支持的范围。
- 语义的验证:验证类的语义信息,是否符合java语言规范的要求。
- 字节码的验证:验证程序语义是合法的、合乎规范的。主要通过stackmapframe结构。
- 符号引的验证:虚拟机在将符号引用转化为直接引用,验证符号引用全限定名代表的类是否能够找到,对应的域和方法是否能找到,访问权限是否合法。
准备
即在方法区中分配这些变量所使用的内存空间
解析
虚拟机将常量池中的符号引用替换为直接引用的过程(什么是符号应用,什么是直接引用)
符号应用
以一组符号来描述应用的目标直接的关系。
直接引用
通过Class文件加载到内存之后,通过对符号引用的转换,就有了对应的直接引用。
初始化
初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子方法执行之前,父类的方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法
JVM 虚拟机内存模型
程序计数器(线程私有)
- 是当前线程所执行的字节码的行号指示器,指向虚拟机字节码指令的位置。
- 被分配了一块较小的内存空间。
- 针对于非Native方法:是当前线程执行的字节码的行号指示器。
- 针对于Native方法:则为undefined。
- 每个线程都有自己独立的程序计数器,所以,该内存是线程私有的。
- 这块区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域
虚拟机栈(线程私有)
- 虚拟机栈为执行Java方法服务
- 栈是线程私有的内存空间
- 每次函数调用的数据都是通过栈传递的。
- 在栈中保存的主要内容为栈帧。它的数据结构就是先进后出。每当函数被调用,该函数就会被入栈,每当函数执行完毕,就会执行出栈操作。而当前栈顶,即为正在执行的函数。
- 每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、帧数据区、动态链接、方法出口等信息。
栈帧
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
本地方法(线程私有)
本地方法区和Java Stack作用类似, 区别是虚拟机栈为执行Java方法服务, 而本地方法栈则为Native方法服务
方法区(线程共享)
- 逻辑上的东西,是JVM的规范。
- 是JVM 所有线程共享的、用于存储类信息,例如:类的字段、方法数据、常量池等。
- 方法区的大小决定了系统可以保存多少个类。
- JDK8之前——永久代 JDK8及之后——元空间
堆(线程共享)
- 运行时数据区,几乎所有的对象都保存在java堆中。
- 堆是垃圾收集器进行GC的最重要的内存区域。
- Java堆可以分为:新生代(Eden区、S0区、S1区)和 老年代。
- 在绝大多数情况下,对象首先分配在eden区,在一次新生代GC回收后,如果对象还存活,则会进入S0或S1,之后,每经历过一次新生代回收,对象如果存活,它的年龄就会加一。当对象的年龄达到一定条件后,就会被认为是老年代对象,从而进入老年代。
JVM 垃圾回收算法
引用计数法
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1.只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
但引用计数器有两个严重问题:
1.无法处理循环引用的情况。(解决方法可达性分析)
2.引用计数器要求在每次因引用产生和消除的时候,需要伴随一个加减法操作,对系统性能会有一定的影响。
可达性分析
为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
标记清除法
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
缺点:最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
复制算法
为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,
复制算法没有内存碎片,并且如果垃圾对象很多,那么这种算法效率很高。但是它的缺点是系统内存只能使用1/2。
标记整理清除法
它首先标记存活的对象,然后将所有存活的对象压缩到内存的一端,然后在清理所有存活对象之外的空间。
分代算法
分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
新生代与复制算法
目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。
老年代标记整理清除算法
而老年代因为每次只回收少量对象,因而采用Mark-Compact算法。
分区算法
将堆空间划分成连续的不同小区间,每个区间独立使用、回收。由于当堆空间大时,一次GC的时间会非常耗时,那么可以控制每次回收多少个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
JVM垃圾收集器
Serial垃圾收集器(单线程、复制算法)
Serial(英文连续)是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1之前新生代唯一的垃圾收集器。Serial是一个单线程的收集器,它不但只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 Serial垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。
ParNew垃圾收集器(Serial+多线程)
ParNew垃圾收集器其实是Serial收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。
Parallel Scavenge收集器(多线程复制算法、高效)
Parallel Scavenge收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU用于运行用户代码的时间/CPU总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。
CMS收集器
(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。 最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
垃圾回收步骤
初始标记
只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
预清理
记录YoungGC的发生的时间
并发标记
进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除
清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
CMS收集器
优先回收垃圾比例最高的区域。G1收集器将堆划分为多个区域,每次收集部分区域来减少GC产生的停顿时间。
G1收集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。