jvm的整体结构大致如下:
这里说的JVM生命周期,指JVM执行Java程序时的周期:
HotSpot是Java体系下使用最多的虚拟机,它结合了最新的内存模型,垃圾收集器和自适应优化器,为使用许多先进技术的Java应用程序提供了最佳性能。
类的加载机制是指把编译后的.class类文件的二进制数据读取到内存中,并为之创建一个java.lang.Class对象,用来封装类在元数据空间的数据结构。
类在JVM中的生命周期为:加载,连接,初始化,使用,卸载。不过这里只重点描述加载,连接,初始化这三个过程
基于一张图看类加载子系统的细节流程:
过程描述:加载阶段需要完成以下三个过程:
类加载器:
验证:目的在于确保Class文件的字节流中包含的信息符合当前虚拟机的要求,保证加载类的正确性,不会危害虚拟机自身的安全,主要包括四种检验动作:
准备:为类的静态变量分配内存,并初始化为默认值,这时候进行内存分配的仅包括类变量(static)修饰,不包括(final-static)修饰的,这里也不会为实例变量分配初始化,实例变量会随着对象一块分配到Java堆中。
解析:将常量池中的符号引用转换为直接引用的过程,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。解析主要针对类或接口、字段、类方法、接口方法、方法类型等,解析的动作实际是会随着JVM在执行完初始化之后再执行的。
执行类构造器clinit()方法的过程,该方法不需要自定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来,Jvm要保证clinit()方法在多线程访问下的安全性。
类加载器收到了类加载的请求时,不会自己先去尝试加载这个类,而是把请求委托给父加载器去执行;
如果父加载器还存在父类加载器,则依次向上委托,因此类加载请求最终都应该被传递到顶层的启动类加载器中;
如果父类加载器可以完成类加载请求,就直接成功返回,只有当父加载器在无法完成该加载,子加载器才会尝试自己去加载该类;
假设自定义一个类名为String且所在包为java.lang,在使用引导类加载器加载时会先加载JDK中的String类,因为这个类本来是属于jdk的,后面再次出现String类就会报错,以此保证源代码不被恶意篡改,这就是沙箱安全机制
在虚拟机启动运行时,会创建多个线程,数据区中有的模块是线程共享的,有的是线程私有的:
线程共享:元数据区、堆Heap;
线程私有:虚拟机栈、本地方法栈、程序计数器;
单个CPU在特定时刻只能执行一个线程,所以多线程通过几块空间的使用,然后不断的争抢CPU的执行时间段。
基本描述:方法元空间(方法区)在JVM启动的时候被创建,是被各个线程共享的内存空间,用于存放类和方法的元数据以及常量池,比如Class和Method。在实际的开发中,经常因为加载的类太多,进而导致内存溢出问题,这样可以对元空间的大小进行扩展。
与堆的关系:
元空间存放加载的类信息,当类被实例化时,堆中存储实例化的对象信息,并且通过对象类型数据的指针找到类。
基本描述:JVM启动时创建堆区,是内存管理的核心区,通常情况下也是最大的内存空间,是被所有线程共享的,几乎所有的对象实例都要在堆中分配内存,所以这里也是垃圾回收的重点空间。
堆栈关系
栈是JVM运行时的单位,堆是存储单位,当栈中方法结束,相关对象失去所有引用后,不会马上被移除堆空间,要等到垃圾收集器运行的时候。
虚拟机栈(Java栈)在每个线程创建时都会生成一个虚拟机栈,栈的内部是一个个栈帧单元,对应Java方法的调用,其生命周期和线程周期保持一致。用来存储方法的局部遍历,部分执行结果,方法的调用和返回。
栈帧是方法执行的数据集,维持执行过程中的各种数据信息,执行的方法依次入栈,栈顶存放当前要执行的方法,执行结束后出栈,对于栈没有垃圾回收问题。
基本描述:JVM中程序计数寄存器用来存储下一条将要执行指令的地址,执行引擎获取到指令后进行执行,是线程私有的。它可以看作是当前线程所执行的字节码的行号指示器。
前后关系:线程在获取CPU的时间段内执行代码,但是线程随时可能没有执行完就被挂起,等到线程A再次获取CPU执行时,CPU 得知道执行到线程A的哪一个指令,程序计数器会存储该动作。
本地方法栈与虚拟机栈所起到的作用是类似的,虚拟机栈为虚拟机执行Java方法,本地方法栈管理虚拟机使用到的 本地方法,在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一。
应用程序经过编译,转换为字节码文件,字节码加载到内存空间并不能直接在操作系统上执行,执行引擎作为Java虚拟机核心的组成部分,作用就是将字节码指令解释/编译为对应系统平台上的本地机器指令。
解释器:虚拟机启动时会根据预定义对字节码采用逐行解释的方式执行,将每条字节码文件中的内容解释为对应系统平台的本地机器指令执行;
JIT编译器:虚拟机将源代码编译成本地机器平台相关的机器语言,并且寻找热点高频执行的代码将其放入元空间中,即元空间中存放的JIT缓存代码;
垃圾回收:对于没有任何引用的对象标记为垃圾,会被回收释放内存空间。
每个对象保存一个整型引用计数器,用来记录对象被引用的次数,当该对象被一个对象引用时,计数器加1,当失去一个引用时,计数器减1;引用计数算法就是通过判断对象的引用数量来决定对象是否可以被当做垃圾对象回收掉。
虽然引用计数法效率高,但是当两个对象互相引用时会导致这两个对象一直不会被回收,这是一个致命的缺陷。所以JVM并没有采用该标记算法。
可达性分析算法是基于对象到根对象的引用链是否可达来判断对象是否可以被回收;
运行程序把所有的引用关系链看作一张图,通过GC-Roots根对象对象集合作为起始点,从每个根节点向下不断搜索被根对象集合所连接的对象是否可达,搜索路径称为引用链(Reference-Chain),如果对象到GC-Roots没有任何引用链存在,则说明此对象是不可用的,虚拟机栈中引用的对象如下:
相对于引用计数法算法,可达性分析算法则避免了循环引用导致的问题,同样具备执行高效的特点,也是JVM采用的标记算法。
标记-清除算法分为标记和清除两个阶段:
该算法效率不高,进行垃圾回收需要暂停应用程序,同时会产生大量内存碎片,后续程序运行过程中分配内存占用较大的对象时,会有连续内存不够情况,容易触发再一次垃圾收集动作。
标记整理算法的标记过程类似标记清除算法
该垃圾回收算法效率不高,对象移动过程需要暂停应用程序,适用于对象存活率高的场景(老年代)。
复制算法将内存按容量划分为大小相等的两块,每次只使用其中的一块,当使用的这块的内存用完,就将还存活着的对象复制到另外一块空闲内存上,然后使用过的内存空间一次清理。
该算法实现简单,运行效率高,但是内存空间严重浪费,适用于对象存活率低的场景,比如新生代。
当前市场上几乎所有的虚拟机都采用该回收算法,分代收集算法根据年轻代和老年代的各自特点采用不同的算法机制,不同内存区域中对象生命周期也不同,因此对堆内存不同区域采用不同的回收策略可以提高垃圾回收执行效率。通常情况新生代对象存活率低,回收频繁,就采用复制算法;老年代存对象生命周期长,活率高,就用标记清除算法或者标记整理算法。
Java堆内存一般可以分为新生代、老年代和永久代三个模块,如下图所示:
新生代:通常情况下,新创建的对象实例首先都是放在新生代空间中,所以追求快速的回收掉垃圾对象,一般情况下,新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区,对象实例大部分在Eden区中生成;
垃圾回收时先把eden区存活对象复制到S0区,然后清空eden区,当S0区也满时,再将eden区和S0区存活对象复制到S1区,然后清空eden和S0区,之后交换S0区和S1区的角色,当S1区无法存放eden区和S0区的存活对象时,就将存活对象直接存移到老年代区,当老年代区也满了,触发一次FullGC,即新生代、老年代都进行回收。
老年代:老年代区存放一些生命周期较长的对象,对象实例在新生代中经历了多次垃圾回收仍然存活的对象,会被移动到老年代区中。