JVM学习笔记之基础篇
想来看过JVM之类的视频,也看过JVM之类的书籍。如何才能消化学到的知识并转换为自己的能力呢,想必做笔记,思维导图不失为一个好办法。
1. 讲在前面之JVM历史以及版本
JVM(Java virtual machine) Java 虚拟机运行在操作系统之上,不与硬件设备直接交互。讲得简单一点来讲,如果说我这个程序员要去租房子,那么C语言好比是房东直租,直接对接房东,没有中间商赚差价。而Java虚拟机更类似于链家中间商,你去中介那里找房子,中介对接房东。
再来说说Java的跨平台性,正因为有了jvm将java的字节码文件翻译机器码才成就了java的跨平台性。所以Java语言是跨平台的,而Jvm不是,因为不同操作系统不同版本安装的jvm版本不同。
最后谈一谈本文所讲的JVM,广义上是指遵守Java虚拟机规范的虚拟机,有很多,具体的就不阐述。我们通常所使用的,面试所问的,皆是oracle官方推荐的 hotspot虚拟机。故后文所指的虚拟机,若无特殊说明,皆为hotspot。
2. JVM运行机制
Java程序运行的具体过程如下:
- Java源文件被编译器翻译成字节码文件。
- JVM将字节码文件编译成相应操作系统的机器码。
- 机器码调用相应操作系统的本地方法库执行相应的方法。
Java虚拟机包括类加载器子系统
,运行时数据区
,执行引擎
,本地接口库
。
3. 类加载子系统
类加载系统将编译好的.Class文件加载到JVM中。
JVM的类加载分为5个阶段:加载,验证,准备,解析,初始化。
3.1. 加载
加载是指JVM读取Class文件,并且根据Class文件描述创建java.lang.Class对象的过程。
那么虚拟机是如何读取文件二进制字节流的?接下来就得讲到类加载器。
3.1.1. 类加载器
类加载器共有四种:启动类加载器
,扩展类加载器
,应用程序类加载器
,自定义类加载器
。前面三种由JVM提供,自定义类加载器是通过继承java.lang.ClassLoader
来实现的。
启动类加载器:负责加载Java_HOME/lib目录中的类库,或者通过-Xbootclasspath
参数指定路径中被虚拟机认可的类库。
扩展类加载器:负责加载Java_HOME/lib/ext 目录中的类库,或通过java.ext.dirs
系统变量加载指定路径中的类库。
应用程序类加载器:负责加载用户路径(classpath)上的类库。
3.1.2. 双亲委派模型
JVM通过双亲委派机制进行类加载。用通俗的话来说就是,当你拿到一个梨,你去问你的妈妈吃不吃梨,你妈妈拿到梨问你奶奶吃不吃梨,如果你奶奶吃梨,那么这个梨就是她吃了,你们就没得吃。如果你奶奶说我不饿让晚辈吃,返回给你妈妈,你妈妈如果说我也不吃,再给你,你就拿起来吃了。
故双亲委派机制是:一个类在收到类加载请求时候不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到请求后又会委派给自己的父类,以此类推,到最顶就是启动类加载器。如果启动类无法加载就向下传递,直到被加载,否则抛出ClassNotFound异常。
那么思考一下为什么需要双亲委派模型?
答:主要还是保障类的唯一性和安全性。比如加载rt.jar
包中的java.lang.Object
类时,无论是哪个类加载器加载这个类,最终都将类加载请求委托给启动类加载器加载,这样就保证了类加载的唯一性。
3.2. 验证
验证的目的很自然就是为了Class文件中的字节流符合虚拟机规范不会危害到虚拟机自身的安全
验证四种:文件格式验证,元数据验证,字节码验证,符号引用验证。
3.2.1. 文件格式验证
- 开头:CA FE BA BE
- 主次版本号
- 常量池中的常量是否有不被支持的常量类型
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
3.2.2. 元数据验证
- 对字节码描述的信息进行语义分析,确保符合Java规范
- 类是否有父类,除了Object之外,所有的类都应该有父类
- 类的父类是否继承了不该被继承的类(比如被final修饰)
- 如果不是抽象类,是否实现了父类或者接口中要求实现的所有方法
- 类的字段,方法是否与父类产生矛盾
3.2.3. 字节码验证
- 对类的方法体进行,进行校验,确保运行时不会危害虚拟机
- 确保程序语义合法,符合逻辑
3.2.4. 符号引用验证
- 通过字符串描述的全限定名是否能找到对应的类
- 符号引用中的类,字段,方法的可访问性是否可被当前类访问
3.3. 准备
- 为类变量分配内存空间并设置类中变量的初始值
- 如果是没有final修饰的,那么准备阶段是赋0值
- 如果是final修饰的,那么准备阶段就是赋值对应的值
3.4. 解析
JVM将符号引用替换为直接引用
3.5. 初始化
- 初始化阶段是执行类构造器方法的()过程
- ()方法是编译器在编译阶段自动收集类中静态语句块和变量的赋值操作组成的
- JVM规定,只有父类的方法执行后,子类的方法才可以被执行
- 如果类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为其生成方法
在发生以下情况时候不会执行类的初始化流程:
- 常量在编译时会将其常量值存入使用该常量的类的常量池中,该过程不需要调用常量所在的类,因此不会触发该常量类的初始化
- 在子类引用父类的静态字段时,不会触发子类的初始化,只会触发父类的初始化
- 定义对象数组不会触发该类的初始化
- 使用类名获取Class对象时不会触发类的初始化
- 使用Class.forName加载指定类,可以通过
initialize
参数设置是否需要对类进行初始化 - 在使用ClassLoader默认的loadClass方法加载类时不会触发该类的初始化
4. 运行时数据区
4.1. 程序计数器
程序计数器是一块很小的内存空间,用来存放当前运行线程锁运行字节码的行号指示器。每个运行中的线程都有一个独立的程序计数器,该方法执行时,程序计数器记录的是实时虚拟机字节码指令的地址,如果是native方法,则程序计数器为null。
程序计数器的特点:小内存,线程私有,没有内存溢出(OOM)也不会有GC。
4.2.虚拟机栈
与程序计数器一样,虚拟机栈也是线程私有的,它的生命周期与线程相同。方法执行的时候,虚拟机栈都会创建一个栈帧。栈帧中存储了局部变量表,操作数栈,动态链接,方法出口等信息。方法的执行和返回对应栈帧的出栈和入栈。
4.3. 本地方法栈
和虚拟机栈差不多,区别是本地方法栈执行native本地方法,而虚拟机栈执行Java方法。
4.4.方法区
JDK7前也被成为永久代。用于存储常量,静态变量,类信息,即时编译器编译后的机器码,运行时常量池等数据。
4.5.堆
JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是线程共享的内存区域,也是垃圾回收主要区域。由于JVM采用分代垃圾回收,所以堆还可以分为:新生代,老年代和元空间。
对象分配一般过程如下:
- new的对象直接放在eden区,放得下直接放。
- 当创建新对象,Eden空间填满,会触发一次Minor GC/YGC,将Eden不再被其他对象引用的对象销毁。将eden中未销毁的对象移动到survivor0区,年龄计数器+1
- 如果eden有空间,加载的新对象放到eden区,(大对象直接到老年代)
- 再次eden满,触发GC,将eden和survivor0中幸存的对象放到survivor1中,年龄+1
- 再垃圾回收,又把eden和survivor1放到survivor0中,以此类推
- 超大对象放入老年代,若放不下或者满了,OOM
由此可知,新生代采用的是 复制算法,主要是为了减少内存碎片。
对象分配特殊过程如下:
4. 垃圾回收器
4.1.如何确定垃圾
Java采用引用计数法和可达性分析来确定对象是否应该被回收,其中,引用计数法容易产生循环引用的问题,可达性分析通过根搜索算法来实现。
4.1.1.引用计数法
在Java中要操作一个对象,那么我们首先要获取对象的引用。为对象添加一个引用,引用计数+1;在为对象删除一个引用时,引用计数减1;如果一个对象的引用计数为0,则表示此刻该对象没有被引用,可以被回收。
但是这样会有一个问题:循环引用。
循环引用是指两个对象相互引用,导致它们的引用一直存在,而不能被回收。
比如Object1和Object2相互引用,Object1的引用计数为1,而Object2同样为1,两个都导致不能被回收。
4.1.2.可达性分析
首先定义一些GC Roots对象,然后以这些GC Roots对象作为起点向下搜索,如果GC Roots和一个对象之间没有可达路径,则称该对象不可达。不可达对象至少要经过两次标记才能判定是否可以被回收,如果两次标记后该对象仍然不可达,则将被垃圾回收。
4.2.垃圾回收算法
4.2.1.标记清除算法
最基础的垃圾回收算法,标记需要回收的对象,清除对象并释放内存空间
由于清除对象所占用的内存空间并没有重新整理可用的内存,所以有内存碎片化的问题。
内存碎片化:内存中总是很多小空间,没有可用的连续的大块空间,如果遇见分配大对象就放不下去。
4.2.2.复制算法
复制算法是为了解决内存碎片化问题设计的。复制算法将内存分为大小相等的两个区域,区域1和区域2,新生成的对象都在区域1中,区域1满后会对区域1进行标记,并将标记后仍然存活的对象复制到区域2中,然后一次性清空整个区域1.
复制算法内存清理效率高且容易实现,但是内存空间可用的只有一半,浪费了内存空间。如果在系统中有大量的长时间存活的对象,那么就要不停的来回在区域1和2间复制,影响效率。因此复制算法适合用在朝生夕死的对象。比如:新生代。
4.2.3.标记整理算法
标记整理算法结合了标记清除算法和复制算法的优点。在标记阶段和标记清除算法的标记相同,不同的是,标记清除算法清除内存后不会整理碎片,而整理算法会整理内存。
4.2.4.分代收集算法
这是JVM采用的算法,这是根据对象的不同类型将内存划分为不同的区域,根据不同区域特点选择不同的算法
JVM将堆划分为新生代和老年代。新生代主要存放新生成的对象,主要特点是对象多但是生命周期短,每次进行垃圾回收时候都有大量的垃圾被回收;老年代主要存放大对象和生命周期长的对象,因此可回收的对象较少。目前大部分JVM新生代都是复制算法。老年代主要是标记整理算法。
4.3.Java中几种不同的引用类型
在Java中一切皆是对象,对象的操作是通过该对象的引用实现的,Java中的引用类型有4种:强引用,软引用,弱引用和虚引用。
[