目录
- [1. JVM简介](#1. JVM简介)
-
- [1.1 JVM执行流程](#1.1 JVM执行流程)
- [2. JVM运行时数据区](#2. JVM运行时数据区)
-
- [2.1 本地方法栈(线程私有)](#2.1 本地方法栈(线程私有))
- [2.2 程序计数器(线程私有)](#2.2 程序计数器(线程私有))
- [2.3 堆(线程共享)](#2.3 堆(线程共享))
- [2.4 虚拟机栈(线程私有)](#2.4 虚拟机栈(线程私有))
- [2.5 方法区(线程共享)](#2.5 方法区(线程共享))
- [2.6 内存布局中的异常问题](#2.6 内存布局中的异常问题)
- [3. 垃圾回收](#3. 垃圾回收)
-
- [3.1 死亡对象的判断算法](#3.1 死亡对象的判断算法)
-
- [3.1.1 引用计数算法:](#3.1.1 引用计数算法:)
- [3.1.2 可达性分析算法](#3.1.2 可达性分析算法)
- [3.2 垃圾回收算法](#3.2 垃圾回收算法)
-
- [3.2.1 标记-清除算法](#3.2.1 标记-清除算法)
- [3.2.2 复制算法](#3.2.2 复制算法)
- [3.2.3 标记-整理算法](#3.2.3 标记-整理算法)
- [3.2.4 分代算法](#3.2.4 分代算法)
- [3.3 垃圾收集器](#3.3 垃圾收集器)
-
- [3.3.1 CMS收集器(老年代收集器,并发GC)](#3.3.1 CMS收集器(老年代收集器,并发GC))
- [3.3.2 G1收集器(唯一一款全区域的垃圾回收器)](#3.3.2 G1收集器(唯一一款全区域的垃圾回收器))
- [3.4 总结: 一个对象的一生](#3.4 总结: 一个对象的一生)
1. JVM简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机.
JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键,那么 JVM 是如何执行的呢
1.1 JVM执行流程
JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface
程序在执行之前先要把java代码转换成字节码(class文件) ,JVM 首先需要把字节码通过类加载器 把文件加载到内存中 运行时数据区 ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,
因此需要特定的命令解析器执行引擎将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能
2. JVM运行时数据区
JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称JMM)完全不同

线程私有: 由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为"线程私有"的内存
2.1 本地方法栈(线程私有)
本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的
2.2 程序计数器(线程私有)
用来记录当前线程执行的行号的, 可以看做是当前线程所执行的字节码的行号指示器
记录当前线程正在执行哪一条字节码指令。当线程被 CPU 切换出去时,PC 会"记住"执行位置,等线程再次获得 CPU 时间片时,从 PC 记录的位置继续执行
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是一个Native方法,这个计数器值为空
2.3 堆(线程共享)
堆: 程序中创建的所有对象都在保存在堆中
堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象会放入老生代
2.4 虚拟机栈(线程私有)
Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的
内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
JVM虚拟机栈四部分:
- 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
- 操作栈:每个方法会生成一个先进后出的操作栈。
- 动态链接:指向运行时常量池的方法引用。
- 方法返回地址:PC 寄存器的地址
2.5 方法区(线程共享)
用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。
在《Java虚拟机规范中》把此区域称之为"方法区",而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace)
JDK 1.8 元空间的变化:
- 对于 HotSpot 来说,JDK 8 元空间的内存属于本地内存,这样元空间的大小就不在受 JVM 最大内存的参数影响了,而是与本地内存的大小有关
- JDK 8 中将字符串常量池移动到了堆中
运行时常量池: 运行时常量池是方法区的一部分,存放字面量与符号引用
字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
2.6 内存布局中的异常问题
Java堆溢出: Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常.
Java堆内存的OOM异常是实际应用中最常见的内存溢出情况。当出现Java堆内存溢出时,异常堆栈信息 "java.lang.OutOfMemoryError" 会进一步提示 "Java heap space" 。当出现"Java heap space"则很明确的告知我们,OOM发生在堆上
此时要对Dump出来的文件进行分析。分析问题的产生到底是出现了内存泄漏还是内存溢出
内存泄漏 : 泄漏对象无法被GC
内存溢出 : 内存对象确实还应该存活。此时要根据JVM堆参数与物理内存相比较检查是否还应该把JVM堆内存调大;或者检查对象的生命周期是否过长
3. 垃圾回收
对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其
生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。因此我们本文所讲的有关内存分配和回收关注的为Java堆与方法区这两个区域.
在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收。
3.1 死亡对象的判断算法
Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下几种算法
3.1.1 引用计数算法:
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"
但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题
3.1.2 可达性分析算法
通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的
3.2 垃圾回收算法
通过上面的学习我们可以将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了,在正式学习垃圾收集器之前,我们先看下垃圾回收机器使用的几种算法算法
3.2.1 标记-清除算法
算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的
对象,在标记完成后统一回收所有被标记的对象
"标记-清除"算法的不足主要有两个 :
- 效率问题 : 标记和清除这两个过程的效率都不高
- 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
3.2.2 复制算法
"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可
现在的商用虚拟机(包括HotSpot都是采用这种收集算法来回收新生代)
新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保
3.2.3 标记-整理算法
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存.
3.2.4 分代算法
分代算法和上面讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收
当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块 ,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法
新生代 :一般创建的对象都会进入新生代;
老年代:大对象和经历了 N 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代移动到老年代
3.3 垃圾收集器
如果说上面我们讲的收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现. 垃圾收集器有很多, 这里只简单介绍两个
3.3.1 CMS收集器(老年代收集器,并发GC)
目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求.
CMS收集器是基于"标记---清除"算法实现的, 它的主要优点在名字上已经体现出来了:并发收集、低停顿.
CMS收集器也有缺点: 对CPU资源非常敏感, 无法处理浮动垃圾, 会产生大量空间碎片
3.3.2 G1收集器(唯一一款全区域的垃圾回收器)
G1垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的region块,然后并行的对其进行垃圾回收,
年轻代垃圾收集: 在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域
老年代垃收收集: 基本跟CMS垃圾收集器一样,但略有不同, 初始标记, 并发标记, 最终标记, 筛选回收
3.4 总结: 一个对象的一生
我是一个普通的 Java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了 Survivor区的 "From" 区(S0 区),自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的 "From" 区,有时候在 Survivor 的 "To" 区(S1 区),居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在老年代里,我生活了很多年(每GC加一岁)然后被回收了。