JVM被分为三个主要的子系统:
- 类加载器子系统
- 运行时数据区
- 执行引擎
类加载子系统
Java的动态类加载功能是由类加载子系统处理。当它在运行时(不是编译时)首先引用一个类时,它加载、连接并初始化该类文件。
1.1 加载
类有此组件加载。启动类加载器(BootStrap class Loader)、扩展类加载器(Extension class Loader)和引用程序类加载器(Application class Loader)这三种类加载器帮助完成类的加载。
- 启动类加载器:负责从启动类路径中加载类,无非就是rt.jar。这个加载器会被赋予最高优先级。
- 扩展类加载器:负责加载ext目录(jre/lib)内的类
- 应用程序类加载器:负责加载引用程序级别类路径,设计到路径的环境变量etc。
1.2 链接
- 检验:字节码校验器会校验生成的字节码是否正确,如果校验失败,我们会得到校验错误。
- 准备:分配内存并初始化默认值给所有静态变量。
- 解析:所有符号内存引用被方法区的原始引用替代。
1.3 初始化
这是类加载的最后阶段,这里所有的静态变量会被赋初始值,并且静态代码块将被执行。
2. 运行时数据区
运行时数据区被划分为5个主要组件:
2.1 方法区
所有类级别数据将被存储在这里,包括静态变量。每个jvm只有一个方法区,它是一个共享资源。
2.2 堆区
所有的对象和它们相应的实例变量以及数组将被存储在这里。每个jvm同样只有一个堆区。由于方法区和堆区的内存由多个线程共享,所以存储的数据不是线程安全。
2.3 栈区
每个线程会单独创建一个运行时栈。每个函数呼叫会在栈内存生成一个栈帧。所有的局部变量将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。栈帧被分为三个子实体:
- a 局部变量表 包含多少个与方法相关的局部变量并且相应的值将被存储在这里。
- b 操作数栈 如果需要执行任何中间操作,操作数栈作为运行时工作区去执行指令
- c 帧数据 方法的所有符号都保存在这里。在任何异常的情况下,catch块的信息将会被保存在帧数据里面。
2.4 PC寄存器
每个线程都有一个单独的PC寄存器来保存当前执行指令的地址,一旦该指令被执行,pc寄存器会被更新至下条指令的地址。
2.5 本地方法栈
本地方法栈保存本地方法信息。对于每一个线程,将创建一个单独的本地方法栈。
3.执行引擎
分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。
3.1 解释器
解释器能快速的解释字节码,但执行却很慢。解释器的缺点就是,当一个方法被多次调用,每次都需要重新解释。
编译器
JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果是重复的代码则使用JIT编译器将全部字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提高了系统的性能。
a. 中间代码生成器 – 生成中间代码
b. 代码优化器 – 负责优化上面生成的中间代码
c. 目标代码生成器 – 负责生成机器代码或本机代码
d. 探测器(Profiler) – 一个特殊的组件,负责寻找被多次调用的方法。
JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而产生、随线程而湮灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收问题,因为方法结束或者线程结束时,内存自然跟随着回收了。而java堆区和方法区则不一样,这部分内存的分配和回收是动态的,真是垃圾收集器所需关注的部分。
垃圾收集器在对堆区和方法区进行回收之前,首先要确定这些区域的对象哪些可以被回收、哪些暂时还不能回收,这就要用到判断对象是否存活的算法!
1. 引用计数算法
算法分析:引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个实力都有一个引用计数。当一个对象被创建时,就将该对象实力分配给一个变量,该变量计数设置为1.当任何其他变量被赋值为这个对象的引用时,计数加1(a=b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了声明周期或者被设置为一个新值时,对象实例的引用计数器减1.任何引用计数器为0的对象可以被当做垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1.
优缺点:
优点:引用技术收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。
2.可达性分析算法
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC Root开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,则无用的节点,无用的节点将会被判定为是可回收的对象。
可作为GC Root的对象包括下面几种:
a)虚拟机栈中引用的对象(栈帧中的本地变量表)
b)方法去中类静态属性引用的对象
c)方法去中常量的引用对象
d)本地方法栈中JNI引用的对象
Java的引用
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判断对象是否存活都与“引用”有关。在Java语言中,将引用分为强引用、软引用、弱引用、虚引用4种。
强引用:类似new Object(),只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
软引用:用来描述一些还有用但是并非必须的对象。碎玉软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用:也是用来描述非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对只能生存到下一次垃圾收集发生之前。当垃圾收集器工作之前,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
对象死亡前的最后一次挣扎:
即使在可达性分析算法中不可达的对象,也并非是非死不可,这时候它们处于“缓刑”阶段,要宣告一个对象死亡,至少需要经历两次标记过程。
- 第一次标记:如果对象在可达性分析之后发现没有雨GC Root相连接的引用链,那塔将会被第一次标记;
- 第二次标记:第一次标记后接着会进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立第二次关联关系的,将被进行第二次标记。标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与应用链建立了关联关系,那么将会逃离本次回收,继续存活。
方法区如何判断是否回收:
方法区存储的内容是否需要回收的判断可就不一样咯。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可以通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
常用的垃圾收集算法
1. 标记-清除算法
标记清楚算法采用从根集合(GC Root)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,只需要对不存货的对象进行处理,在存活对象较多的情况下极为高效,但是由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
2. 复制算法
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始把堆分成一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从根集合中扫描活动对象了,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存
3. 标记-整理算法
标记-整理算法采用标记-清除算法一样的方式进行对象标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
4.分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象的存活生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代和新生代,在堆区之外还有一个代就是永久代。老年代的特点就是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最合适的收集算法。
年轻代的回收算法
1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
2.新生代内存按照8:1:1的比例分为一个eden和两个survivor(survivor0,survivor1)区。一个Eden区,两个survivor区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活的对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区时空的,然后将survivor0和survivor1区交换,即保持survivor1区为空,如此反复。
3.当survivor1区不足以存放eden和survivor0的存活对象时,就将存活对象存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
4.新生代发生的GC也叫做Minor GC,Minor GC发生频率比较高(不一定等eden区满了才触发)。
老年代的回收算法
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
4.内存比新生代也大很多,当老年代内存满了时触发Major GC即Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代的回收算法
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著的影响,但是有些引用可能动态生成或者调用一些class,例如hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。
垃圾收集器
serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
Serial Old收集器(标记-整理算法)
老年嗲单线程收集器,Serial收集器的老年代版本。
ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU.shihe后台引用等对交互响应要求不高的场景。
GC是什么时候触发的
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
1.Scavenge
一般情况下,当新对象生成,并且在eden申请空间失败时,就会触发Scavenge GC,对Eden区进行GC,清除非存活对象,并且把尚存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的eden区进行,不会影响到老年代。因为大部分对象都是从Eden区开始 的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区尽快空闲出来。
2.Full GC
对整个堆进行整理,包括young、tenured和perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对Full GC的调节。有如下原因可能导致Full GC:
1.老年代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显示调用
4.上一次GC之后Heap的各域分配策略动态变化。