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队列中。
以上两种情况均被视作没有必要执行。
第二次标记:GC会对队列中的对象进行二次小规模标记,只要对象与引用链上的任何一个对象建立关联即可被移除出”即将回收“的集合。
回收方法区
方法区的垃圾收集效率很低。主要收集废弃常量以及无用的类。
废弃常量是指没有对象指向且没有被引用的常量池成员,包括类(接口)、方法、字段等
无用类:必须满足下面三个条件才算是”无用的类“,且不一定会被回收。
垃圾收集算法
标记-清除
标记阶段,程序会检查每个对象是否存活,若存货,则程序会在对象头部打上标记;清除阶段进行对象回收取消标记位。
标记-整理
让所有存活的对象向一端移动,直接清理掉端边界以外的内存。
复制
将内存空间平分为两部分,每次只使用一半,一块用完后就将存活的对象复制到另一半,然后将这一半直接清理。
;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进行回收。标记-清除算法会造成老年代空间浪费。
G1则将堆划分为多个大小相等的独立区域,可以对每个小空间进行单独垃圾回收。每个region都有Remembered Set用以记录该region对象的引用对象所在的Region。可大概划分为以下几个步骤:
整体按照标记-整理实现收集器,局部基于复制算法实现。停顿也是可预测的。
Java的四种引用,参看Java基础(一)49
Minor GC:新生代GC,指发生在新生代的垃圾收集动作,由于Java对象大多朝生夕灭,所以Minor GC非常频繁,回收速度也比较快。触发条件较为简单,Eden空间满时,就会触发一次MinorGC。
Major GC:发生在老年代的GC,至少会伴随一次的Minor GC,速度会比Minor GC慢10倍以上。触发条件较为复杂
对象优先在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应用程序提供了高度的灵活性。
上图为类从被加载到虚拟机内存开始,到卸载出内存为止整个的生命周期。验证、准/备、解析统称为连接部分。
两个类相等,需要类本身相等,并使用同一个类加载器进行加载,因为每个类加载器都拥有一个独立的类名称空间。相等包括类的Class对象的equals、isAssignableFrom、isInstance方法的返回结果。
双亲委派模型
启动类加载器,采用c++语言实现,是虚拟机自身的一部分
所有其他类的加载器:使用Java实现,独立于虚拟机,继承自抽象类java.lang.ClassLoader
还可以细分为三种:
应用程序是由三种类加载互相配合从而实现类加载,除此之外可以加入自己定义的类加载器。除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器,一般通过组合关系实现。
工作过程:一个类加载器首先将类加载请求委派给父类去完成,只有当父类加载器无法完成是,子类加载器才会尝试自己加载。
好处在于Java类随着类加载器拥有了一种带有优先级的层次关系。