文章目录
- 0. 运行流程
- 1. 内存区域划分
- 1.1 堆
- 1.2 Java 虚拟机栈
- 1.3 程序计数器
- 1.4 方法区
- 2. 类加载机制
- 类加载过程
- 2.1 加载
- 2.2 验证
- 2.3 准备
- 2.4 解析
- 2.5 初始化
- 双亲委派模型
- 3. 垃圾回收机制
- 3.1 垃圾判断算法
- 3.1.1 引用计数算法
- 3.1.2 可达性分析算法
- 3.2 垃圾回收算法
- 3.2.1 标记清除算法
- 3.2.2 复制算法
- 3.2.3 标记整理算法
- 3.2.4 分代算法
- 3.3 垃圾收集器
0. 运行流程
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 **执行引擎(Execution Engine)**将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。
1. 内存区域划分
如图:
1.1 堆
程序中创建的所有对象都保存在堆中。
堆中又可以划分为两个部分,新生代和老生代。
新生代为新创建的对象,当经过一定 GC(下面讲) 次数之后还存活的对象
会放入老生代。
而新生代还有 3 个区域:一个 Endn + 两个 Survivor(from/to | s0/s1)。
垃圾回收的时候会将 Endn 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清楚掉。
1.2 Java 虚拟机栈
Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的。
内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。
- 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表 所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变 量。
- 操作栈:每个方法会生成一个先进后出的操作栈。
- 动态链接:指向运行时常量池的方法引用。
- 方法返回地址:PC 寄存器的地址。
本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的。
1.3 程序计数器
程序计数器的作用:用来记录当前线程执行的行号的。
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器值为空。
1.4 方法区
方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。
在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)。
2. 类加载机制
JVM执行流程中对程序员来说最重要的就是类加载的流程,对于类来说,它的生命周期如图:
而类加载的流程就是前五步。
类加载过程
2.1 加载
加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,它和类加载 Class Loading 是不同的,一个是加载 Loading 另一个是类加载 Class Loading,所以不要把二者搞混了。
在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
通俗点就是: 找到.class文件,打开文件,读取文件内容,并且尝试解析格式。
2.2 验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节 流中
包含的信息符合《Java虚拟机 规范》的全部约束要求,保证这些信 息被当作代码运行后不会危害虚拟机自身的安全。
验证当前.class文件的格式,是否符合要求。
验证选项:
- 文件格式验证
- 字节码验证
- 符号引用验证…
2.3 准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,。
给类对象分配空间,内容全是0.
2.4 解析
解析阶段是 Java 虚拟机将常量池内的符号引用(相对位置)替换为直接引用(真实位置)的过程,也就是初始化常量的过程。
主要是初始化类对象中涉及的字符串常量,但其实.class文件中含有,直接读取到内存即可。
2.5 初始化
初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
初始化阶段就是执行类构造器方法的过程。
双亲委派模型
描述类加载的过程,如何找到.class文件。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
而JVM带有三个类加载器(也可以自己写新的类加载器),从上到下依次是BootStrap ClassLoader(负责加载标准库中的类)——>Extension ClassLoader(负责加载扩展库中的类)——>Application ClassLoader(负责加载第三方库中的类;例如: servlet,自己写的类……)。
3. 垃圾回收机制
上面讲了Java运行时内存的各个区域。对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。因此我们所讲的有关内存分配和回收关注的为Java堆与方法区这两个区域。
3.1 垃圾判断算法
Java堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行垃圾收集前,首先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下几种算法:
- 引入计数
- 可达性分析
3.1.1 引用计数算法
给对象增加一个引用计数器,当有一个对象引用它,计数器加1,引用消失,计数器减1,当引用计数器为0,就判断对象为垃圾。
python采用引用计数法管理内存。
但这种算法有着很大的问题,所以Java JVM中不使用这种方法。
缺点:
- 浪费内存,如果对象很小,内存浪费很严重。
- 在对象循环引用时,可能会有问题。
两个对象中都引用对方,两者引用计数器都为2,当使两个对象为空,-1,但是引用计数器为1,这样就会出现这两个对象已经为垃圾,但是没有收集。
3.1.2 可达性分析算法
而Java中使用的是可达性分析算法,判断对象是否为垃圾。这个算法的起点是“GC Roots”,即一组被认为是Java虚拟机中的根对象。Java虚拟机会遍历这些根对象,找到它们所引用的对象,将这些对象标记为“存活”。当一个对象到GC Roots没有任何引用链相连,即从GC Roots到这个对象不可达,则证明此对象是不可用,就表示可回收的对象。
类似于树,有一个根对象,后面的对象都被其他对象引用,当前面的对象消失,这个对象后引用的对象也会消失。
3.2 垃圾回收算法
通过上面的学习我们可以将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了,在正式学习垃圾收集器之前,我们先看下垃圾回收机器使用的几种算法(这些算法是垃圾收集器的指导思想)。
3.2.1 标记清除算法
"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(标记过程见3.1.2章节)。后续的收集算法都是基于这种思路并对其不足加以改进而已。
"标记-清除"算法的不足主要有两个 :
- 效率问题 : 标记和清除这两个过程的效率都不高
- 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中 需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
如图:
3.2.2 复制算法
"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。
算法的执行流程如下图 :
但是缺点也很明显:
- 浪费了一半内存。
- 效率低,容易出现STW。
STW是指Java虚拟机在执行垃圾回收(GC)时的一种状态,即Stop-The-World。在STW状态下,Java应用程序的所有线程都将被暂停执行,只有GC线程除外。
3.2.3 标记整理算法
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
流程图如下:
3.2.4 分代算法
前面我们知道JVM堆中对象分为新生代和老年代。这就是为GC做的,Java中使用的垃圾回收算法就是分代算法。
新生代:一般创建的对象都会进入新生代;
老年代:大对象和经历了 N 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代移动到老年代。
在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
这样我们只需要经常扫描新生代,老年代扫描间隔相对新生代大10倍以上。
请问了解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倍以上。
3.3 垃圾收集器
上面讲的都是垃圾收集的思想,而我们垃圾收集器则是具体的实现。
作用:
垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。
下面收集器是不同时代推出的不同的垃圾收集器,它们有的已经淘汰,有的仍在时候,它们具有不同的优缺点。
了解了解即可。