文章目录
- JVM
- 一、JVM运行流程
- 1. JVM执行流程
- 二、JVM运行时数据区
- 1. 程序计数器(线程私有)
- 2. 虚拟机栈 (线程私有)
- 3. 本地方法栈(线程私有)
- 4. 堆(线程共享)
- 5. 元空间(线程共享)
- 运行时常量池
- 三、JVM类加载
- 1. 类加载过程
- 1)加载
- 2)验证
- 3)准备
- 4)解析
- 5)初始化
- 双亲委派模型
- 双亲委派模型优点
- 四、垃圾回收
- 1. 死亡对象的判断算法
- 1)引用计数算法
- 2)可达性分析
- 2. 垃圾回收算法
- 1)标记清除法
- 2)复制算法
- 3)标记整理算法
- 4)分带算法
JVM
一、JVM运行流程
JVM是 Java 运行的基础,也是实现一次编译到处执行的关键,那么JVM是如何执行的呢?
1. JVM执行流程
程序在执行前先要把 Java 代码转换成字节码(.class文件),JVM 需要把字节码通过 类加载器(ClassLoader) 加载到内存中的 运行时数据区(Runtime Date Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此,需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由 CPU 去执行,而这个过程需要调动其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是四个主要组成部分的功能与职责。
小结,JVM 主要通过以下四个部分来执行 Java 程序的:
- 类加载器 (ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
二、JVM运行时数据区
JVM运行时数据区也叫做内存布局,他由以下五个部分组成:
1. 程序计数器(线程私有)
程序计数器保存了下一条要执行的指令的地址。(不是 CPU 的寄存器,而是内存空间)
“下一条指令”指的是 Java 的字节码。(不是 CPU 的二进制机器语言)
注记: 什么是线程私有?
由于 JVM 的多线程是线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此为了切换线程后能够恢复到正确的执行位置,每条线程都需要独立的程序计数器,各线程之间计数互不影响,独立存储。我们就把这类区域成为 ”线程私有“ 的内存。
2. 虚拟机栈 (线程私有)
Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法执行时的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
JVM 虚拟机栈中包含了以下四部分:
- 局部变量表:存放了编译器可知的各种基本数据类型、对象引用。(存放方法参数和局部变量)
- 操作栈:每个方法会生成一个先进后出的操作栈。
- 动态链接:指向运行时常量池的方法引用。
- 放法返回地址:PC 寄存器的地址。
3. 本地方法栈(线程私有)
本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的 。
4. 堆(线程共享)
作用:程序中所有创建的对象都保存在堆中。
我们常见的 JVM 参数设置 -Xms 10m最小启动内存是针对堆的,-Xmx 10m最大运行内存也是针对堆的。
ms 是 memory start 的简称,mx 是 memory max 的简称。
堆里面分为两个区域,新生代和老生代。新生代存放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代。
新生代又细分为三个区域:一个Eden + 两个Survivor(S0/S1)。
垃圾回收时会将 Eden 中存活的对象放到没有使用的 Survivor 中,同时把当前的 Eden 和正在使用中的 Servivor 中的对象清除掉。
垃圾回收的细节部分会在下文着重讲述。
5. 元空间(线程共享)
作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池
是方法区的一部分,存放字面量与符号引用。
字面量:字符串(Java8之后 移动到堆中)、final常量、基本数据类型的值。
符号引用:类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
注记:
因为在7之前的版本,方法区又被称为“永久代”,他有固定的大小,容易发生内存溢出,8之后元空间取代方法区,元空间使用的是本地内存,而不是JVM堆内存,这样就可以避免永久代相关的内存溢出的问题。
小结:
三、JVM类加载
1. 类加载过程
对一个类来说,他的生命周期是这样的:
我们分别来看每个步骤的具体内容:
1)加载
“加载” (Loading)阶段是整个“类加载”(Class Loading)过程中的第一个阶段,他和类加载 Class Loading是不同的,一个是加载 Loading 一个是类加载 Class Loading。
在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取此类的二进制字节流。
- 将这个字节流的静态存储结构转化为元空间的运行时数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为元空间这个类的各种区域的访问入口。
加载简单来说就是,JVM要读取.class中的内容,并执行里面的命令。
2)验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
3)准备
准备阶段是正式为类中定义的变量(即静态变量,被staic修饰的变量)分配内存并设置类变量初始值的阶段。
例如:
public static int value = 123;
他初始化 value
的 int 值为0,而非 123。
4)解析
解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。
5)初始化
初始化阶段,Java虚拟机真正开始执行类中编写的 Java 程序代码,将主导权交给应用程序。初始化阶段就是执行类构造器方法的过程。
双亲委派模型
提到类加载机制就不得不提的一个概念就是“双亲委派模型”。
什么是双亲委派模型?
如果一个类收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(他的搜索范围中没有找到所需的类)时,子类加载器才会尝试自己去完成加载。
JVM默认有三个类加载器:
- BootstrapClassLoader (引用类加载器) 负责加载标准库的类
- ExtensionClassLoader (扩展类加载器) 负责加载扩展类
- ApplicationClassLoader (应用程序类加载器) 负责加载第三方库的类/自己写的代码中的类
双亲委派模型优点
- 避免重复加载类:比如A类和B类都有一个父类C类,那么当A类启动时就会将C类加载起来,那么B类在加载时就不需要在重复加载C类了。
- 安全性:可以保证 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器自己加载的话就会出现一些问题,例如我们编写一个称为
java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的,因此安全性就不能得到保证了。
四、垃圾回收
1. 死亡对象的判断算法
1)引用计数算法
定义:给对象增加一个引用计数器,每当有地方引用他时,计数器就+1;当引用失效时,计数器就-1;任何时刻引用计数器为0的对象都是不能再被使用的了,就是对象已“死”。
主流的 JVM 中没有选用引用计数算法来管理内存,最主要的原因是无法解决对象的循环引用问题。
假设下面是我们写的代码:
Test a = new Test();
Test b = new Test();a.t = b;
b.t = a;a = null;
b = null;
最后造成了两个new出来的对象互相计数,而真正的引用已经置空,引用不到这两个对象了,但是无法把他们标记称“垃圾”,因为他们的引用计数器都还不为0。
(和死锁有点类似)
2)可达性分析
Java采用此方法来判断对象是否存活。
该算法的核心思想为:通过一系列称为 “GC Roots“ 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为”引用链“,当一个对象到 GC Root
没有任何任何的引用链相连时(就是不可达),证明此对象是不可用的。
例如:
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。
在 Java 中,可以作为 GC Root 的对象包含下面几种:
- 虚拟机栈中引用的对象;
- 元空间中类静态属性引用的对象;
- 元空间中常量引用的对象;
- 本地方法栈中 JNI(Native方法)引用的对象。
2. 垃圾回收算法
1)标记清除法
算法分为 “标记” 和 “清除” 两部分,先是找到所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
问题有两个:
- 效率问题:标记和清除两个过程的效率都不高
- 空间问题:标记清除后会产生大量的内存碎片,空间碎片太多就会导致后续程序需要分配较大对象时,无法找到足够连续的内存来存储,不得已提前出发下一次的垃圾清除。
2)复制算法
复制算法是为了解决 “标记清理” 的效率问题。他将可用内存分为相等大小的两等块,每次只使用其中的一块,当这块内存需要垃圾回收时,就将此区域内还存活的对象复制到另一块区域上面,然后将使用过区域的内存一次清理掉。这样做的好处是对整个半区进行内存回收,内存分配时就不需要考虑内存碎片的情况,只需移动堆顶指针,按顺序分配即可。
3)标记整理算法
是对 “标记清除” 算法的升级,标记过程一致,但清理不是对对象直接回收,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
4)分带算法
分带算法是通过区域划分,对不同的区域采用不同的垃圾回收策略,从而更好的实现垃圾回收。
当前的 JVM 垃圾回收 都是采用的“分代收集”算法。一般是把 Java 堆分为新生代和老生代,在新生代中,又分为一个Eden + 两个Survivor(S0/S1);新创建的对象会放到 Eden 区中,一轮 GC 过后会将 Eden 区和当前使用的 Survivor 区中还存活的对象放到未使用的 Survivor 区中,经过多轮 GC 后还存活的对象,会把他放到老生代,老生代的垃圾回收算法是上面讲到的 “标记整理算法”。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记整理"算法。
问题:
Minor GC 和 Full GC 这两种 GC 有什么不一样吗 ?
Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。