什么是JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
JVM结构
1 Class Loader(类加载器)就是将Class文件加载到内存,再说的详细一点就是,把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是类加载器的作用。
2 Run Data Area(运行时数据区) 就是我们常说的JVM管理的内存了,也是我们这里主要讨论的部分。运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行。这部分也是我们这里将要讨论的重点。
3 Execution engine(执行引擎) 是Java虚拟机最核心的组成部分之一。执行引擎用于执行指令,不同的java虚拟机内部实现中,执行引擎在执行Java代码的时候可能有解释执行(解释器执行)和编译执行(通过即时编译器产生本地代码执行,例如JIT),也有可能两者兼备。
类加载
类加载是指JVM把编译过的二进制class文件加载到内存中,并对数据进行校验,转换解析和初始化。
类加载过程
装载
1.通过全限定类名找到对应的class文件,生成二进制字节流。
2.解析这个二进制流,生成方法区类信息。
3.根据类信息在堆中生成对应的class对象,作为方法区类信息的访问入口。
链接
验证:该阶段主要是为了保证加载进来的字节流符合JVM的规范,不会对JVM有安全性问题。其中有对元数据的验证,例如检查类是否继承了被final修饰的类;还有对符号引用的验证,例如校验符号引用是否可以通过全限定名找到,或者是检查符号引用的权限(private、public)是否符合语法规定等。
准备:为静态变量分配内存,并将其初始化默认值,如果此静态变量是final修饰的,直接赋值
解析:把类中的符号引用转换为直接引用,此处针对的是静态方法及属性和私有方法与属性,因为这类方法与私有方法不能被重写,静态属性在运行期也没有多态这一说,即在编译器可知,运行期不可变,所以适合在该阶段解析
初始化
对类的静态变量赋值,执行静态代码块
类加载器
所有的类加载器都继承自ClassLoader这个抽象类,其中loadClass(String)就是加载对应类的方法。
loadClass(String)方法的机制遵循向上检查,向下委派,也就是双亲委派机制。如果要打破这种机制,需要重写loadClass()方法。
运行时数据区
运行时数据区主要由程序计数器,本地方法栈,虚拟机栈,堆,方法区组成,其中,程序计数器,本地方法栈,虚拟机栈由线程各自独享,堆,方法区线程共享。
方法区
方法区是所有线程共享的内存,在java8以前是放在JVM内存中的(永久代),由于永久代的内存大小不好估计,容易发生OOM,所以在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),方法区主要分为这两部分:
1.类元信息(Klass)
类元信息在类编译期间放入方法区,里面放置了类的基本信息,包括类的版本、字段、方法、接口以及常量池表(Constant Pool Table),常量池表存储了类在编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中
2.运行时常量池(Runtime Constant Pool)
运行时常量池主要存放在类加载后被解析的字面量与符号引用。在JDK8之前,字符串常量池也是运行时常量池的一部分,JDK8后放到堆里,方便回收。
Heap(堆)
Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。Java对象实例以及数组都在堆上分配。
堆区域划分
为什么这么设计?
假设一下,如果不分新老代,内存就一整块,垃圾收集器每次都要把那些长期存在的对象,和生命周期很短的对象放在一起回收,一般长生命周期的对象可能跟应用生命周期一致,你基本回收不掉的,整个应用运行期间都存在,这样就很耗费性能。区分新老代之后,老年代放长期存活的对象,新生代就放生命周期短的对象,老年代对象很稳定,新生代回收不影响老年代,回收效率能大大提高。
设计S区的意义?为什么设计两个S区
如果没有Survivor,Eden区每进行一次young GC,存活的对象就会被送到老年代。老年代很快被填满,触发full GC。S区可以为我们做筛选,筛选出那些存活时间久的对象,一般是经历了16次young GC的对象,减少了送往老年代对象的数量,从而减少full GC的频率,并且保证老年代对象的稳定性。
设计两个S区可以有效减少内存碎片化,假设现在只有一个survivor区:刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。但是设置多个S区,每一块的空间就会比较小,很容易导致Survivor区满,所以设置两个配合标记复制算法是比较合理的。
对象内存分配过程
Java Virtual Machine Stacks(虚拟机栈)
虚拟机栈是一个线程执行的区域,保存着线程中执行的方法的调用状态。是线程私有的,随着线程生灭。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。如下图:
局部变量表:方法中定义的局部变量以及对象的引用,局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。
操作数栈:保存的是计算过程中操作的变量以及结果,以压栈和出栈的方式存储操作。
动态链接:每个栈帧也就是每个方法,都有一个指向运行时常量池中该方法所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。
本地方法栈(Native Method Stack)
与虚拟机栈是一样的,只不过执行的是native方法。
程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。java中最小的执行单位是线程,因为虚拟机的是多线程的,每个线程是抢夺cpu时间片,程序计数器就是存储线程目前执行到哪里了。每个线程都有属于自己的程序计数器,而且互不影响,独立存储。
垃圾回收算法
可达性分析算法
可达性分析算法是用来判断堆中对象是否存活的算法,他是以一系列GC ROOT对象为出发点,引出他们下一个引用的节点,直到遍历到最后一个引用的节点,形成一条引用链,在这条引用链上的对象是存活对象。不在任意一条引用链上的对象会被GC回收。GC ROOT是哪些?
粗体
1.虚拟机栈栈帧中的局部变量表中引用的对象,也就是我们每个方法中引用的对象。同样本地方法栈同理。
2.堆中类静态属性引用的对象,也就是堆内存中每个类加载时生成的class对象中静态属性引用的对象。
3.方法区中常量引用的对象。
标记清除算法
标记
找出内存中需要回收的对象,并且把它们标记出来。
清除
清除掉被标记需要回收的对象,释放出对应的内存空间。
缺点
1.标记和清除都需要遍历两次对象,效率不高。
2.会产生大量不连续的内存碎片,浪费内存空间。
适用场合
相对来说,更适合在生命周期长,存活对象多的old区。
复制算法
将内存划分为两块大小相等的区域,每次只是用其中一个,如下图所示:
当其中一块使用完了,就将还存活的对象复制到另一块上面,然后把已经使用过的那一块一次清除掉。
优点
1.只用遍历一次内存空间,将存活的对象复制到另外一块,效率较高。
2.复制过去后能保存内存空间的连续性,不会出现碎片的问题。
缺点
内存空间利用率低。
使用场合
适用生命周期短,存活几率小的对象,young区复制效率比较高。
标记整理算法
标记
找出内存中需要回收的对象,并且把它们标记出来。
整理
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优点
1.消除了标记清除算法当中,容易出现内存碎片的缺点。
2.消除了复制算法当中,内存减半的高额代价。
缺点
相对于复制算法来说,效率较低。
适用场合
适合在生命周期长,存活对象多的old区。
垃圾回收器
1.Serial
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。
特点
1.回收垃圾时是单线程的,在单核CPU的场景下简单高效。
2.采用复制算法,适用于young区。
3.会STW。
2.Serial Old
Serial Old收集器是Serial老年代版本,也是一个单线程收集器,不同的是采用标记整理,运行过程和Serial收集器一样。
3.ParNew
特点
1.回收垃圾时是多线程的,适合多核CPU的场景,效率高。
2.采用复制算法,适用于young区。
3.会STW。
4.Parallel Scavenge
Parallel Scavenge是一款新生代垃圾收集器,是JDK1.8默认的垃圾收集器。其特点与Parnew相同。区别在于Parallel Scavenge更关注吞吐量。
吞吐量
吞吐量 = 运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。由此可见吞吐量越高,可以最大程度上利用CPU资源来执行我们的程序运算。
5.Parallel Old
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法进行垃圾回收,也是更加关注系统的吞吐量。
6.CMS
CMS(Concurrent Mark Sweep)收集器是一种更关注最短回收停顿时间的垃圾收集器。使用标记清除算法,用于老年代。
1.初始标记
标记GC Roots直接关联对象,速度很快,直接STW,避免线程切换消耗。
2.并发标记
多线程标记GC Roots链上其他引用的对象,这个阶段比较耗时,所以与业务线程并行。
3.重新标记
修改并发标记阶段因业务线程变动的内容,速度很快,直接STW。
4.并发清理
清除不可达对象回收空间,与业务线程并行,同时有新垃圾产生,留着下次清理称为浮动垃圾。
优点:
把垃圾回收阶段比较耗时的标记GC Roots链上的对象和清理垃圾对象,取消STW,实现和业务线程并行,最大程度上降低了STW时间。
缺点
产生内存碎片,在并行阶段,吞吐量降低。
7.G1
使用G1(Garbage-First)收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
工作流程
1.初始标记:标记GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程
2.并发标记:从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
3.最终标记:修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
4.筛选回收:对各个Region的回收价值和耗费的时间进行排序,根据用户所期望的GC停顿时间制定回收计划
特点
1.分代收集(仍然保留了分代的概念)
2.Region的角色可以根据周围内存空间的角色作灵活的变换,一定程度上保证了内存的连续性。
3.G1可以在用户指定时间范围内进行垃圾回收(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。