当我们不熟悉JVM的时候,或者初次看到JVM的文档,会觉得JVM是枯燥无味的。而当你真正的了解它的内部运行机制后,你会对它着迷。
我们经常说的JVM(java虚拟机),在不同的环境会有不同的定义,它可以表示一个JVM抽象规范,也可以是一个JVM具体实现,还可以是运行中的JVM实例。
编译:
我们开发的java代码使用JDK中的编译器编译成class文件,每个类或者接口对应一个class文件,该文件的内容是二进制的字节码流。使用二进制的好处是可以通过多种方式获得class文件,比如网络等;
加载:
编译好的class文件在使用时可首先被JVM加载,加载是由JVM的类加载器执行的,类加载器分为启动类加载器(bootstrup)和用户自定义类加载器。其中启动类加载器负责加载JDK API中的class文件,比如我们常用的java.lang包中的类。而用户自定义的类可以由用户自定义的类加载器加载。
加载阶段流程:
装载:查找并装载class文件。
连接:又可以细分为验证、准备、解析(可选)三个步骤。
验证:验证导入的类型正确;
准备:为其中的类变量分配内存空间、并将其初始化为默认的值;
解析:将类中的符号引用转换为直接饮用;
初始化:初始化类变量为正确的初始化值。
运行时数据区:
JVM会在内存中开辟出一块区域用来存放数据,被称为运行时数据区。包括:堆、方法区、栈、PC寄存器、本地方法栈。
堆:用来存放类生成的实例对象;
方法区:存储JVM的类加载器加载的类信息;
栈:JVM为线程运行时分配的数据区;
PC寄存器:线程运行时用来存放要执行的指令地址;
本地方法栈:执行本地方法时的数据区;
方法区:
首先来看一下方法区的内部结构,方法区中保存的是加载过的类型信息,每个类或者接口都对应一个类型信息。
类型信息:
类型的全限定名;
类型的直接超类的全限定名;
类型是类还是接口;
类型的访问修饰符;
实现的所有接口的全限定名;
类型的常量池:该类型所有常量的一个有序集合,包括直接常量和对其他类型、字段和方法的符号引用。
字段信息:字段名、字段类型、字段的修饰符;
方法信息:方法名、方法返回的类型、方法的修饰符、方法的字节码、操作数栈和局部变量的大小、异常表;
除常量以外的类变量;
到类ClassLoader的引用;
到类Class的引用;
堆:
不同的JVM实现会有不一样的堆结构,比如有的JVM堆实现采用句柄池、对象池的方式来存储实例对象。每个对象引用对应堆中的一个句柄池指针,该指针包括一个指向方法区的类信息的指针和一个指向对象池的指针,对象池中存储实例变量。由于堆是被所有线程共享的。所以堆中的对象可能会同时被多个线程访问,需要考虑多线程问题。此时就需要涉及到对象锁定的问题,每个对象都有一个相应的锁对象,可以实现互斥。只有在第一次调用同步方法时,对象才会与锁对象关联。另外一个保存的是和GC相关的数据,不同的GC机制决定保存的数据不同。
栈:
是在线程启动时,JVM分配的内存区域;用来保存该线程运行阶段的数据,属于线程私有,所以栈内的数据操作都是线程安全的。线程在执行一个方法时,会创建一个帧,帧内包含方法的参数、局部变量、操作数栈、帧数据。首先线程会将传递给方法的参数按照顺序保存到局部变量中,局部变量是一个类似数组的数据结构。然后再将方法中的真实变量无需的加入到局部变量。当执行方法中的一条命令时,比如两个变量相加,首先将两个变量从局部变量中取出,压入操作数栈,然后从操作数栈中弹出变量进行相加,再将结果压入到操作数栈中,最后在讲操作数栈中的结果存储到局部变量中。所以在方法内部,只要不涉及到实例变量,就不需要考虑线程同步的问题。线程每执行一个方法,就需要在栈中压入一个帧,所以,在方法内部调用其他方法,其他方法的帧被压入栈顶。栈顶在栈的最下方,栈生成是自下而上的顺序。
垃圾收集:
释放不再使用的对象空间,碎片整理。
算法:
检测、释放
可触及性
根对象集合:总会包含局部变量中的对象引用和栈帧中的操作数栈(以及类变量中的对象引用)。另外一个来源是被加载的类的常量池中的对象引用,比如字符串。还有一个来源是传递到本地方法中的,并没有被本地方法释放的对象引用。在一个潜在的来源是java虚拟机运行时数据区中从垃圾收集器的堆中分配的部分。
检查从根对象集合中的根对象开始,是否有到对象的引用。
引用计数器和跟踪:计数记录下对那个对象的引用次数(不被使用了)。跟踪从根节点开始的引用图。引用的对象加上标记,最后回收未加标记的对象。
标记并清除
压缩收集器:对付堆碎片。引用指向句柄,句柄再指向堆中对象。改变堆中对象的位置,只需要改变句柄就可以。引用不动。
拷贝收集器:把所有活动的对象移动到一个新区域。好处是从根对象的遍历开始,随着发现被拷贝,不再有标记,清除之分。
停止并拷贝:堆被分为两个区域,每次只使用一个区域,当一个区域满后,停止程序执行,将区域中的活动对象拷贝到另一个区域,来回往复。缺点是生命周期长的对象会被来回拷贝。
按代收集器:通过把对象按照寿命来分组解决停止并拷贝的缺点问题。更多的收集那些短暂出现的年幼对象,而非寿命长的对象。在这里,堆被分为两个或多个子堆,每个子堆为一代对象服务。最年幼的那一代进行最频繁的垃圾收集。因为大多数对象都是短促出现的,只有很少部分对象在他们经历一次收集后还存活。如果一个年幼的对象经过好几次收集还存活,那么这个对象就成长为寿命更高的一代。它被转移到另一个子堆。
火车算法:为了解决垃圾收集时暂停程序执行的时间过长的问题。使用渐进式收集算法。