JVM相关知识。
文章目录
一、组成部分
答:JVM由Class Loader(类加载器)、Runtime Data Area(运行时数据区域)、Execution Engine(执行引擎)、Native Interface(本地库接口)。
Class Loader负责加载字节码文件;Runtime Data Area分为Stack(虚拟机栈)、Heap(堆)、Method Area(方法区)、PC Register(程序计数器)、Native Method Stack(本地方法栈),负责加载数据;Execution Engine将Class Loader加载的命令解释给操作系统;Native Interface负责调用本地接口。
Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
PC Register(程序计数器)负责记录正在执行的虚拟机字节码指令的地址(本地方法则为空)
每个Java方法在执行时会创建一个栈帧存放在Stack(虚拟机栈)内用以存放局部变量表,操作数栈,常量池引用等信息。方法从执行到完成对应着一个栈帧的入栈和出栈。-Xss
可以用来指定虚拟机栈的大小。
Native Method Stack(本地方法栈)与Stack类似,只是本地方法栈中存放本地方法的栈帧。
Heap(堆)是虚拟机所管理的内存中最大的一块,被所有线程共享,在JVM启动之初就被创建,只存放对象实例。无须连续内存,可以动态增加,增加失败会抛出OutOfMemoryError
的异常,-Xms
设定初始值,-Xmx
设定最大值。
Method Area(方法区)用已存放被加载的类信息,常量,静态变量,即时编译器等数据,和Heap类似,被所有线程共享,无须连续内存,可以动态增加,增加失败会抛出OutOfMemoryError
的异常。为了与Heap区分开,也叫Non-Heap。
Runtime Constant Pool(运行时常量池)是方法区的一部分,class文件中的常量池会在类加载后放到这个区域。
Direct Memory(直接内存)并不是虚拟机运行时数据区的一部分,是Java堆之外的,直接向系统申请的内存空间,但也可能导致
OutOfMemoryError
。JDK引入NIO后,操作系统内就直接划出了一块直接缓存区可以直接被Java访问,即“零拷贝”,能显著提高性能。
二、垃圾收集
主要针对方法区和堆进行垃圾回收,程序计数器、虚拟机栈和本地方法栈属于线程私有,线程结束后就会消失,因此无需进行垃圾回收。
-
判断对象是否可回收。
-
引用计数算法
当对象增加一个引用时,计数器加一,失效时,计数器减一。当计数器变为0时,对象可被回收。当两个对象出现循环引用时,该方法失效,故JVM不采取该方法。
-
可达性分析
该方法以一系列的GC Roots为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。
GC Roots一般指:虚拟机栈中局部变量表中引用的对象,本地方法栈中JNI(native方法)引用的对象,方法区中静态属性和常量引用的对象。
-
-
但不可达的对象也不代表着它一定要死亡。死亡对象要经过两次标记。
-
第一次标记:经过可达性分析,进行筛选后,标记。条件是:此对象是否有必要执行finalize()方法。
若被判定为有必要执行finalize方法,这个对象会被放置在一个F-Queue队列中。
- 对象没有覆盖finalize方法
- finalize方法已经被虚拟机调用过
以上两种情况均被视作没有必要执行。
-
第二次标记:GC会对队列中的对象进行二次小规模标记,只要对象与引用链上的任何一个对象建立关联即可被移除出”即将回收“的集合。
-
-
回收方法区
方法区的垃圾收集效率很低。主要收集废弃常量以及无用的类。
废弃常量是指没有对象指向且没有被引用的常量池成员,包括类(接口)、方法、字段等
无用类:必须满足下面三个条件才算是”无用的类“,且不一定会被回收。
- 该类的所有的实例都已经被回收(堆中不存在该类的任何实例)
- 加载该类的ClassLoader被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
-
垃圾收集算法
-
标记-清除
标记阶段,程序会检查每个对象是否存活,若存货,则程序会在对象头部打上标记;清除阶段进行对象回收取消标记位。
- 不足:效率低下,会产生大量不连续的内存碎片
-
标记-整理
让所有存活的对象向一端移动,直接清理掉端边界以外的内存。
- 不足:要移动大量对象,chulixiaolvdi
- 不会产生内存碎片
-
复制
将内存空间平分为两部分,每次只使用一半,一块用完后就将存活的对象复制到另一半,然后将这一半直接清理。
- 不足:空间利用率低。
;l现在商业虚拟机一般采用这种方法收集新生代。会将空间化为较大的Eden和两块较小的Survivor,每次使用Eden和一块Survivor。HotSpot默认的Eden:Survivor大小比例为8:1。当Survivor空间不够时,需要依赖老年代进行分配担保,存入老年代。
-
分代收集
将堆分为新生代和老生代。新生代使用复制算法,老生代使用标记-清除/标记-整理
-
-
垃圾收集器
-
单线程,多线程:垃圾收集器只使用一个线程/使用多个线程
-
串行并行:串行指垃圾收集器和用户程序交替执行,需要停顿用户程序;并行指垃圾收集器和用户程序同时执行。除CMS和G1,其他垃圾收集器都以串行方式执行。
垃圾收集器关注点是尽可能缩短垃圾收集时,用户线程的停顿时间,而Parallel Scavenge目标是达到一个可控制的吞吐量。
编号 名称 1 Serial收集器(单线程) Client环境下默认新生代收集器 2 ParNew收集器(Serial的多线程版本) Server环境下默认新生代收集器,可与CMS配合 3 Parallel Scavenge收集器(多线程) ”吞吐量优先“ 4 Serial Old收集器 Serial老年代版本 5 Parallel Old收集器 Parallel Scavenge老年代版本 6 CMS(Concurrent Mark Sweep) 7 G1收集器 可以直接回收新生代和老年代 -
CMS可被划分为四个流程:
- 初始标记:标记GC Roots可达的对象,需要暂停用户程序
- 并发标记:GC Roots Tracing,无需暂停,可并发完成(耗时最长)
- 重新标记:修正上一步过程中用户程序修改后的对象标记,需要暂停用户程序
- 并发清理:无需暂停用户程序
缺点在于:低暂停时间牺牲了吞吐量,无法处理浮动垃圾,只能等到下次GC进行回收。标记-清除算法会造成老年代空间浪费。
-
G1则将堆划分为多个大小相等的独立区域,可以对每个小空间进行单独垃圾回收。每个region都有Remembered Set用以记录该region对象的引用对象所在的Region。可大概划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:修正并发标记过程中因为用户程序而发生改变的对象标记,需要暂停线程,但可以并发执行
- 筛选回收:按照回收价值和成本进行排序,按照用户所期望的GC停顿时间来制定回收计划。
整体按照标记-整理实现收集器,局部基于复制算法实现。停顿也是可预测的。
-
-
Java的四种引用,参看Java基础(一)49
三、内存分配和回收
Minor GC:新生代GC,指发生在新生代的垃圾收集动作,由于Java对象大多朝生夕灭,所以Minor GC非常频繁,回收速度也比较快。触发条件较为简单,Eden空间满时,就会触发一次MinorGC。
Major GC:发生在老年代的GC,至少会伴随一次的Minor GC,速度会比Minor GC慢10倍以上。触发条件较为复杂
- 调用System.gc()。但虚拟机不一定真正去执行。
- 老年代空间不足。例如下面的2、3。故应当避免创建过大的数组和对象。
- 空间分配担保失败。例如下面的5。
- JDK1.7及以前的永久代空间不足。
- Concurrent Mode Failure,CMS GC过程中有对象要放入老年代,而老年代空间不足,会报这个错误,触发Full GC。
-
对象优先在Eden分配。
多数情况下,对象在新生代Eden区中分配,当空间不足够时,虚拟机将发起一次Minor GC。
-
大对象直接进入老年代
大对象:需要大量连续内存空间的Java对象例如数组、字符串,避免Eden、Survivor之间的大量内存复制。
-
长期存活的对象进入老年代
为每个对象定义了一个对象年龄计数器。如果对象在Eden出生,并经过第一次Minor GC后仍然存活,且能被Survivor容纳,将被移至Survivor中,且年龄被设为1。在Survivor区中每熬过一次Minor GC,年龄就增加一岁。直至增加到一定程度(默认15岁)就会被晋升到老年代。阈值可以通过
-XX:MaxTenuringThreshold
来设置。 -
动态对象年龄判断
虚拟机并不是永远地要求对象的年龄必须到达阈值才能晋升老年代,如果在Survior空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需达到阈值。
-
空间分配担保
在Minor GC前,会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,则Minor GC是安全的。如果不成立,虚拟机会查看HandlePromotionFailure的值是否允许担保失败。如果允许,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,则冒险进行Minor GC,若小于,进行Full GC。
四、类加载机制
JVM将描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以直接被虚拟机使用的Java类型,这就是虚拟机的类加载机制。类型的加载、连接和初始化都是在程序运行期间完成的,虽然略微增加了性能开销,但为java应用程序提供了高度的灵活性。
上图为类从被加载到虚拟机内存开始,到卸载出内存为止整个的生命周期。验证、准/备、解析统称为连接部分。
类加载时机
- 加载、验证、准备、初始化和卸载阶段的顺序是确定的。解析不一定非按照这个顺序开始,某些情况下它可以在初始化阶段之后再开始,以实现动态绑定。
- Java虚拟机规范中严格规定了有且仅有5种情况必须立即对类进行”初始化“。
- 遇到new、getstatic、putstatic、invokestatic时,若类没有进行初始化,则必须先触发它的初始化。
- 使用java.lang.reflect包的方法对类进行发射调用时,若类没有进行初始化,则必须先触发它的初始化。
- 当初始化一个类时,若它的父类没有进行初始化,则必须先触发父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。
- 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要触发其初始化。
- 主动引用:上述五种情况成为主动引用。
- 被动引用:除上述五种情况外, 所有引用类的方式都不会触发初始化,成为被动引用。
类加载过程
- 加载阶段(与连接阶段的部分内容是交叉进行的,但开始时间保持着固定的先后顺序),虚拟机要完成下面三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 验证阶段为了确保Class文件的字节流符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 包括文件格式验证、源数据验证、字节码验证、符号引用验证。
- 准备阶段是正式为类变量分配内存并设置类变量初始值的过程,变量所使用的内存都将在方法去进行分配。仅仅只有类变量,不包括实例变量;初始值通常情况下是数据类型的零值。
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 初始化阶段是类加载的最后一步,开始真正执行类中定义的Java程序代码。
类加载器
-
两个类相等,需要类本身相等,并使用同一个类加载器进行加载,因为每个类加载器都拥有一个独立的类名称空间。相等包括类的Class对象的equals、isAssignableFrom、isInstance方法的返回结果。
-
双亲委派模型
-
启动类加载器,采用c++语言实现,是虚拟机自身的一部分
-
所有其他类的加载器:使用Java实现,独立于虚拟机,继承自抽象类java.lang.ClassLoader
还可以细分为三种:
- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序是由三种类加载互相配合从而实现类加载,除此之外可以加入自己定义的类加载器。除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器,一般通过组合关系实现。
工作过程:一个类加载器首先将类加载请求委派给父类去完成,只有当父类加载器无法完成是,子类加载器才会尝试自己加载。
好处在于Java类随着类加载器拥有了一种带有优先级的层次关系。
-