写在前面:今天七夕,祝大家七夕快乐哈。
在JAVA开发中,字节码是在JRE上运行的,而JVM则是JRE中的核心组成部分,它可以分析和执行JAVA字节码文件。虽然开发中并不需要了解JVM运行机制便可以开发出应用程序,但是掌握JVM的内部机制,则可以解决复杂的性能问题,也是JAVA程序员必备知识之一。
1.什么是JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
JRE/JDK/JVM是什么关系?(JDK > JRE > JVM)
JRE(JavaRuntimeEnvironment,Java运行环境),也就是Java平台。所有的Java 程序都要在JRE下才能运行。普通用户只需要运行已开发好的java程序,安装JRE即可。
JDK(Java Development Kit)是程序开发者用来来编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。为了保持JDK的独立性和完整性,在JDK的安装过程中,JRE也是 安装的一部分。所以,在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。
JVM(JavaVirtualMachine,Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
JVM结构如图所示:
2.java代码编译和执行的整个过程
Java代码编译是由Java源码编译器来完成,流程图如下所示:
Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:
Java代码编译和执行的整个过程包含了以下三个重要的机制:
(1) Java源码编译机制
(2) 类加载机制
(3) 类执行机制
编译机制:
Java 源码编译由以下三个过程组成:
(1) 分析和输入到符号表
(2) 注解处理
(3) 语义分析和生成class文件
生成的class文件组成:
(1) 结构信息:包括class文件格式版本号及各部分的数量与大小的信息。
(2) 元数据:对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池。
(3) 方法信息:对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息。
类加载机制:
JVM的类加载是通过ClassLoader及其子类来完成的,采用双亲委派模型,详细请看java基础(十)之类加载机制。
类执行机制:
JVM是基于栈的体系结构来执行class字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。
JVM生命周期:
(1) 启动:启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
(2) 运行:main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以表明自己创建的线程是守护线程。
(3) 消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。
3.运行时数据区
(1) 寄存器
JVM内部虚拟寄存器,存取速度非常快,程序不可控制。每个线程都会有一个PC(Program Counter)寄存器,并跟随线程的启动而创建。PC寄存器中存储下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。
(2) JVM栈
每个线程都有一个JVM栈,并跟随线程的启动而创建。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表(基本类型变量)、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
(3) 堆
是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,包括:所有的对象实例、全局变量以及数组都要在堆上分配。同时,堆也是垃圾回收的目标场所。当涉及到JVM性能优化时,通常也会提及到数据堆空间的大小设置。JVM提供者可以决定划分堆空间或者不执行垃圾回收。
(4) 方法区
方法区(Method Area)也是各个线程共享的内存区域,在JVM启动时创建,存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息。JVM规范并对方法区的垃圾回收未做强制限定,因此对于JVM实现者来说,方法区的垃圾回收是可选操作。
(5) 本地方法栈
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。
(6) 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分,存放的是类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。
4.JVM垃圾回收
GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
(1) 对新生代的对象的收集称为minor GC;
(2) 对旧生代的对象的收集称为Full GC;
(3) 程序中主动调用System.gc()强制执行的GC为Full GC。
首先来看下堆的结构图:
从上图可知,JVM采用分代收集。即将内存分为几个区域,将不同生命周期的对象放在不同区域里;
(1) 在GC收集的时候,频繁收集生命周期短的区域(Young area);
(2) 比较少的收集生命周期比较长的区域(Old area);
(3) 基本不收集的永久区(Perm area)。
新生代的GC
新生代被分为Eden和Survivor区,而Survivor则由From Space和To Space组成。
新生代通常存活时间较短,因此采用基于Copying算法来进行回收。
所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和From Space或To Space之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到survivor,最后到旧生代,用java visualVM来查看,能明显观察到新生代满了后,会把对象转移到旧生代,然后清空继续装载,当旧生代也满了后,就会报outofmemory的异常。
旧生代的GC
旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收。
所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行GC(Serial MSC)、并行GC(parallel MSC)和并发GC(CMS)。
持久代的GC
Permanent GC是指内存的永久保存区域,主要存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域.。它和存放Instance的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。
5.GC算法
(1) 引用计数法:通过引用计数来回收垃圾。
引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
引用计数法的缺点:引用和去引用伴随加法和减法,影响性能。很难处理循环引用
(2) 标记-清除算法:是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。常用于大量对象存活,如老年代。
(3) 标记-压缩算法:适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。常用于大量对象存活,如老年代。
(4) 复制算法:是一种相对高效的回收方法,不适用于存活对象较多的场合,如老年代。将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。常用于少量对象存活的时期,如新生代。
复制算法的最大问题是:空间浪费,整合标记清理思想。
三种算法的比较
效率:复制算法 > 标记/整理算法 > 标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记/整理算法>标记/清除算法。
内存利用率:标记/整理算法=标记/清除算法>复制算法。
6.引用类型
不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
(1) 强引用
当我们使用new 这个关键字创建对象时被创建的对象就是强引用,如Object object = new Object() 这个Object()就是一个强引用了,如果一个对象具有强引用。垃圾回收器就不会去回收有强引用的对象。如当jvm内存不足时,具备强引用的对象,虚拟机宁可会报内存空间不足的异常来终止程序,也不会靠垃圾回收器去回收该对象来解决内存。
(2) 软引用
如果一个对象具备软引用,如果内存空间足够,那么垃圾回收器就不会回收它,如果内存空间不足了,就会回收该对象。当然没有被回收之前,该对象依然可以被程序调用。一般用于实现内存敏感的高速缓存。
(3) 弱引用
如果一个对象只具有弱引用,只要垃圾回收器在自己的内存空间中线程检测到了,就会立即被回收,对应内存也会被释放掉。相比软引用弱引用的生命周期要比软引用短很多。
(4) 虚引用
如果一个对象只具有虚引用,那么它就和没有任何引用一样,随时会被jvm当作垃圾进行回收。虚引用既不会影响对象的生命周期,也无法通过虚引用来获取对象实例,仅用于在发生GC时接收一个系统通知。
jdk中的引用实现类
代表软引用的类:java.lang.ref.SoftReference
代表弱引用的类:java.lang.ref.WeakReference
代表虚引用的类:java.lang.ref.PhantomReference
他们同时继承了:java.lang.ref.Reference
引用队列:java.lang.ref.ReferenceQueue,这个引用队列是可以三种引用类型联合使用的,以便跟踪java虚拟机回收所引用对象的活动。
7.垃圾收集器
Serial收集器
是一个单线程的收集器,不是只能使用一个CPU。在进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束。
新生代采用复制算法,Stop-The-World
老年代采用标记-整理算法,Stop-The-World
简单高效,Client模式下默认的新生代收集器
ParNew收集器
ParNew收集器是Serial收集器的多线程版本
新生代采用复制算法,Stop-The-World
老年代采用标记-整理算法,Stop-The-World
它是运行在Server模式下首选新生代收集器 。
除了Serial收集器之外,只有它能和CMS收集器配合工作 。
ParNew Scanvenge收集器
类似ParNew,但更加关注吞吐量。目标是:达到一个可控制吞吐量的收集器。
停顿时间和吞吐量不可能同时调优。我们一方面希望停顿时间少,另外一方面希望吞吐量高,其实这是矛盾的。因为:在GC的时候,垃圾回收的工作总量是不变的,如果将停顿时间减少,那频率就会提高;既然频率提高了,说明就会频繁的进行GC,那吞吐量就会减少,性能就会降低。
G1收集器
是当今收集器发展的最前言成果之一,对垃圾回收进行了划分优先级的操作,这种有优先级的区域回收方式保证了它的高效率 。
最大的优点是结合了空间整合,不会产生大量的碎片,也降低了进行gc的频率,让使用者明确指定指定停顿时间 。
CMS收集器:(Concurrent Mark Sweep:并发标记清除老年代收集器)
一种以获得最短回收停顿时间为目标的收集器,适用于互联网站或者B/S系统的服务器上 。
初始标记(Stop-The-World):根可以直接关联到的对象 。
并发标记(和用户线程一起):主要标记过程,标记全部对象 。
重新标记(Stop-The-World):由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正 。
并发清除(和用户线程一起):基于标记结果,直接清理对象 。
并发收集,低停顿。
8.Student s = new Student();在内存中做了哪些事情?
(1) 编译完成后,加载Student.class文件进内存;
(2) 在栈内存为s开辟空间;
(3) 在堆内存为学生对象开辟空间;
(4) 对学生对象的成员变量进行默认初始化;
(5) 对学生对象的成员变量进行显示初始化;
(6) 通过构造方法对学生对象的成员变量赋值;
(7) 学生对象初始化完毕,把对象地址赋值给s变量。