JVM的运行机制
运行过程
- Java源文件被编译器编译成字节码文件
- JVM将字节码文件编译成相应操作系统的机器码
- 机器码调用相应操作系统的本地方法库执行相应的方法
类加载器用于将编译好的.Class文件加载到JVM中
即时编译器:将Java字节码编译成具体的机器码
多线程
JVM的线程与操作系统中的线程是相互对应的,在JVM线程的本地存储、缓冲区分配、同步对象、栈、程序计数器等准备工作都完成时,JVM会调用操作系统的接口创建一个与之对应的原生线程;在JVM线程运行结束时,原生线程随之被回收。
操作系统负责调度所有的线程,并为其分配CPU时间片,在原生线程初始化完毕后,就会调用Java线程的run()执行该线程;在线程结束时,会释放原生线程和Java线程对应的资源
在Jvm后台运行的线程主要有
- 虚拟机线程(JVM Thread) 虚拟机线程在JVM到达安全点时出现
- 周期性任务线程:通过定时器调度线程来实现周期性操作的执行
- GC线程:支持JVM中不同的垃圾回收活动
- 编译器线程:在运行时将字节码动态编译成本地平台机器码,是JVM跨平台的具体体现
- 信号分发线程:接收发送到JVM的信号并调用JVM方法
JVM的内存区域
线程私有区域
生命周期与线程相同,在JVM内,每个线程都与操作系统的本地线程直接映射,这部分区域的存在与否和本地线程的启动和销毁对应
程序计数器:线程私有,无内存溢出问题
一块很小的内存空间,用于存储当前运行的线程所执行的字节码的型号指示器
每个运行中的线程都有一个独立的程序计数器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字节码指令的地址;如果该方法执行的是Native方法,则值为空
它是唯一没有内存溢出的区域
虚拟机栈:线程私有,描述Java方法的执行过程
描述Java方法的执行过程的内存模型,它在当前栈帧中存储了局部变量表,操作数栈,动态链接,方法出口等,也用来存储部分运行时数据及其数据结构,处理动态链接方法的返回值和异常分派
本地方法区:线程私有
和虚拟机栈作用类似,但是为Native方法服务
线程共享区
随虚拟机的启动而创建,随虚拟机的关闭而销毁
堆:线程共享
也被称为【运行时数据区】,是垃圾回收期进行垃圾回收的最主要的内存区域
堆从GC【GarbageCollection 垃圾回收】可细分为:新生代、老年代、永久代
方法区: 线程共享
也被称为永久代,存储常量、静态变量、类消息、即时编译器编译后的机器码、运行时常量池等数据
JVM使用java堆的永久代来实现方法区,这样JVM的垃圾收集器就可以像管理java堆一样管理这部分内存。永久代的内存回收主要针对常量池的回收和类的卸载,可回收的对象很少
常量被存储在运行时常量池中,是方法区的一部分
直接内存
也叫对外内存, 并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。
JDK的NIO模块提供的Channel与Buffer的I/O操作方式就是基于对外内存实现的,NIO模块通过调用Native函数库直接在操作系统上分配对外内存,然后使用DirectByteBuffer对象作为这块内存的引用对内存进行操作,Java进程可以通过对外内存技术避免在java堆和Native堆中来回复制数据带来的资源占用和性能消耗,因此堆外内存在高并发应用场景下被广发使用
JVM的运行时内存(JVM堆)
新生代
JVM新创建的对象(除了大对象以外)会被放在新生代,由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC(新生代的GC过程)进行垃圾回收
MinorGC过程,采用复制算法:
- 把在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区。
如果某对象的年龄达到老年代的标准(由XX:MaxTenuringThreshold设置,默认为15),则将其复制到老年代,同时把他们年龄+1,
如果对象属于大对象(2KB~128KB),也复制到老年代
- 清空Eden区和ServivorFrom区中的对象
- 将ServivorTo区和ServivorFrom区互换
Eden区
新创建的对象会首先放在 Eden区,如果新创建的对象属于大对象(一般为2KB~128KB),则分配到老年代
在Eden区内存不足时,触发MinorGC,对新生代进行垃圾回收
ServivorTo区
保留上一次MinorGC时的幸存者
ServivorFrom区
将上一次MinorGC时的幸存者作为这一次的被扫描者
老年代
存放有长生命周期的对象和大对象。老年代的GC叫MajorGC。
对象比较稳定,MajorGC不会频繁触发。
在进行MajorGC前,JVM会进行MinorGC,如果之后还是存在老年代空间不足或无法找到足够大的连续空间分配给新创建的大对象时,会触发MajorGC进行垃圾回收,释放JVM的内存空间
MajorGC采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间
因为要先扫描老年代的所有对象再回收,所以MajorGC耗时较长。MajorGC的标记清楚算法容易产生内存碎片。老年代没有内存分配时,会抛出Out Of Memory
永久代
主要存放Class和Meta(元数据),Class在类加载时被放入永久代。GC不会再程序运行期间对永久代清理内存,导致了永久代的内存会随着Class的增多而变多,过多时会抛出Out Of Memory
Java8以后,永久代被【元数据区(元空间)】取代,区别在:元数据区直接使用操作系统的本地内存,不受JVM内存的限制,之和操作系统的内存有关
Java8中,JVM将 元数据放入本地内存中,常量池和类的静态变量放入Java堆中
垃圾回收与算法
如何确定垃圾
- 引用计数法
给对象中添加一个引用计数器:
- 每当有一个地方引用它,计数器就加 1;
- 当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
- 可达性分析
通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,如果在GC Roots和一个对象之间没有可达路径,则称该对象是不可达的,不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后仍然是不可达的,就应该被回收
下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。
垃圾回收算法
标记清除算法
最基础的算法
清理后并没有整理可用的空间,所以会引起碎片化的问题,继而引起大对象无法获得连续可用空间的问题
复制算法
它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
虽然改进了标记-清除算法,但依然存在下面这些问题:
- 可用内存变小:可用内存缩小为原来的一半。
- 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。
标记整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。<br />在新生代中,每次收集都会有大量对象死去,选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。<br />老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
分区收集算法
将整个堆空间划分为连续的大小不同的小区域,对每个小区域都单独进行内存使用和垃圾回收,好处是能根据每个小区域内存的大小灵活使用和释放内存
可以根据系统可接受的停顿时间,每次都快速回收若干个小区域的内存,以缩短垃圾回收时,系统停顿的时间,最后以多次并行累加的方式逐步完成整个内存区域的垃圾回收
垃圾收集器
Serial
在它进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束
对于单CPU环境来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率
JVM运行在Client模式下的新生代的默认垃圾收集器
ParNew
是Serial的多线程实现,在它进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束
是JVM运行在Server模式下新生代的默认垃圾收集器
Parallel Scavenge
为提高新生代垃圾收集器收集效率而设计的垃圾收集器,通过自适应调节策略优化了系统吞吐量,提供了三个参数用于调节
- -XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间
- -XX:GCTimeRadio 控制吞吐量大小
- UseAdaptiveSizePolicy 控制自适应调节策略开启与否
CMS
Concurrent Mark Sweep
达到最短的垃圾回收停顿时间
- 初始标记
只标记和GC Roots直接关联的对象,速度很快,需要"Stop The World"
- 并发标记
和用户线程一起工作,执行GC Roots跟踪标记过程,不需要暂停工作线程
- 重新标记
在并发标记过程中用户线程继续运行,导致在垃圾回收过程中部分对象的状态发生变化,为了确保这部分对象的状态正确性,需要对其重新标记并暂停工作线程
- 并发消除
和用户线程一起工作,执行清除GC Roots不可达对象的任务,不需要暂停线程
Serial Old
Serial垃圾收集器的老年代实现
Parallel Old
优先考虑系统吞吐量、其次考虑停顿时间等因素
G1
Garbage First
为了避免全区域垃圾收集引起的系统停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域
G1通过内存区域独立划分使用和根据不同优先级回收各区域垃圾的机制,确保了G1在有限时间内获得最高的垃圾收集效率
相对于CMS,有以下优点
- 基于标记整理算法,不产生内存碎片
- 可以精确的控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾回收
类加载机制
类加载阶段
加载
JVM读取Class文件,并且根据Class文件描述创建java.lang.Class对象的过程
类加载过程主要包含将Class文件读取到运行时区域的方法区内,在堆中创建java.lang.Class对象,并封装类在方法区的数据结构的过程,在读取Class文件时既可以以文件的形式读取,也可以用过jar包、war包读取,还可以通过代理自动生成Class或其他方式读取
验证
确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被JVM加载
准备
在方法区中为类变量分配内存空间并设置类中变量的初始值(不同数据类型的默认值)
final类型与否的初始化过程不同public static long value = 1000;
静态变量在准备阶段的初始值是0,将value设置为1000的动作是在对象的初始化中完成的,因为JVM在编译阶段会将静态变量的初始化操作定义在构造器中public static final int value = 1000;
JVM在编译阶段后会为final类型的变量value生成其对应的ConstantValue属性,虚拟机在准备阶段会根据ConstantValue属性将value赋值为1000
解析
JVM会将常量池中的符号引用替换为直接引用
初始化
主要通过类构造器的方法为类进行初始化
方法是在编译阶段由编译器自动收集类中静态语句块和变量的赋值操作组成的,JVM规定,只有在父类的方法都执行成功后,子类中的方法才可以被执行
在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成方法
发生以下几种情况,JVM不会执行初始化流程
- 常量在编译时会将其常量值存入使用该常量的类的常量池中,该过程不需要调用常量所在的类
- 在子类引用父类的静态字段时,不会触发子类的初始化,只会出发父类的初始化
- 定义对象数组
- 使用类名获取Class对象时
- 使用Class.forName加载指定的类时,可以通过initialize参数设置是否需要对类进行初始化
- 使用ClassLoader默认的loadClass方法加载类时
类加载器
启动类加载器
负责加载Java_HOME/lib目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库
扩展类加载器
负责加载Java_HOME/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库
应用程序类加载器
负责加载用户路径(classpath)上的类库
还可以通过继承java.lang.ClassLoader实现自定义的类加载器
双亲委派机制
一个类在收到类加载请求后不会尝试自己加载这个类,而是把请求向上委派其父类完成,父类又会委派给自己的父类,所有的类加载请求都被向上委派到了启动类加载器中
若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的class文件在父类的类的加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,否则抛出ClassNotFound异常
- 将自定义加载器挂在到应用程序类加载器
- 应用程序类加载器将类加载请求委托给扩展类加载器
- 扩展类加载器将类加载请求委托给启动类加载器
- 启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由扩展类加载器加载
- 扩展类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由应用程序类加载器加载
- 应用程序类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由自定义类加载器加载
- 在自定义加载器下查找并加载Class文件,如果未找到目标Class文件,抛出ClassNotFound异常
核心是唯一性和安全性。
比如 加载rt.jar包中的java.lang.Object类时,无论那个类加载器加载这个类,最终都将类请求委托给了启动类加载器加载,就保证了类加载的唯一性,如果重名,则类无法被加载
OGSI
Open Service Gateway Initialtive是Java动态化模块化系统的一系列规范,旨在为实现Java程序的模块化编程提供基础条件
基于OSGI的程序可以实现模块级的热插拔功能,只针对需要更新的程序进行停用和重新安装