目录
Java虚拟机内存管理
1.JVM整体架构
2.JVM运行时内存
JVM加载机制
1.类装载子系统
1.1 类加载子系统介绍
1.2 类加载器ClassLoader角色
1.3 类加载的执行过程
2.类加载器
2.1 类加载器的作用
2.2 类加载器分类
3.双亲委派模型
3.1 什么是双亲委派
3.2 如何实现双亲委派模型
4. 自定义加类加载器
4.1 为什么要自定义类加载器
4.2 自定义函数调用过程
垃圾回收机制及算法
1.垃圾回收概述
2.垃圾回收-对象是否已死
2.1 判断对象是否存活 - 引用计数算法
2.2 判断对象是否存活-可达性分析算法
3.垃圾收集算法
3.1 分代收集理论
3.2 标记-清除算法
3.3 标记-复制算法
3.4 标记-整理算法
4.垃圾收集器
4.1垃圾收集器概述
Java虚拟机内存管理
1.JVM整体架构
根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。从线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:元空间(MetaSpace)、Java堆
JVM分为五大模块: 类装载器子系统 、 运行时数据区 、 执行引擎 、 本地方法接口 和 垃圾收集模块 。
2.JVM运行时内存
Java 虚拟机有自动内存管理机制,如果出现面的问题,排查错误就必须要了解虚拟机是怎样使用内存的。
Java7和Java8内存结构的不同主要体现在方法区的实现
方法区是java虚拟机规范中定义的一种概念上的区域,不同的厂商可以对虚拟机进行不同的实现。我们通常使用的Java SE都是由Sun JDK和OpenJDK所提供,这也是应用最广泛的版本。而该版本使用的VM就是HotSpot VM。通常情况下,我们所讲的java虚拟机指的就是HotSpot的版本。
JDK8虚拟机内存详解:
JDK7和JDK8变化
对于Java8,HotSpots取消了永久代,那么是不是就没有方法区了呢?
当然不是,方法区只是一个规范,只不过它的实现变了。
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。
方法区Java8之后的变化
- 移除了永久代(PermGen),替换为元空间(Metaspace)
- 永久代中的class metadata(类元信息)转移到了native memory(本地内存,而不是虚拟机)
- 永久代中的interned Strings(字符串常量池) 和 class static variables(类静态变量)转移到了Java heap
- 永久代参数(PermSize MaxPermSize)-> 元空间参数(MetaspaceSize MaxMetaspaceSize)
Java8为什么要将永久代替换成Metaspace?
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- Oracle 可能会将HotSpot 与 JRockit 合二为一,JRockit没有所谓的永久代。
JVM加载机制
1.类装载子系统
1.1 类加载子系统介绍
- 类加载子系统负责从文件系统或是网络中加载.class文件,class文件在文件开头有特定的文件标识。
- 把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;
- 如果调用构造器实例化对象,则该对象存放在堆区;
1.1 类加载子系统介绍
- 类加载子系统负责从文件系统或是网络中加载.class文件,class文件在文件开头有特定的文件标识。
- 把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;
- 如果调用构造器实例化对象,则该对象存放在堆区;
1.2 类加载器ClassLoader角色
- class file 存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
- class file 加载到JVM中,被称为DNA元数据模板。
- 在 .class文件 --> JVM --> 最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
1.3 类加载的执行过程
类使用的7个阶段
类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸载(Unloading)这7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这七个阶段的发生顺序如下图:
2.类加载器
2.1 类加载器的作用
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
2.2 类加载器分类
- jvm支持两种类型的加载器,分别是引导类加载器和 自定义加载器
- 引导类加载器是由c/c++实现的,自定义加载器是由java实现的。
- jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器。
- 按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)
3.双亲委派模型
3.1 什么是双亲委派
3.1 什么是双亲委派
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException ),子加载器才会尝试自己去加载。
3.2 如何实现双亲委派模型
双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实 ClassLoader 类默认的 loadClass 方法已经帮我们写好了,我们无需去写。
4. 自定义加类加载器
4.1 为什么要自定义类加载器
- 隔离加载类
模块隔离,把类加载到不同的应用选中。比如tomcat这类web应用服务器,内部自定义了好几中类加载器,用于隔离web应用服务器上的不同应用程序。 - 修改类加载方式
除了Bootstrap加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。 - 扩展加载源
比如还可以从数据库、网络、或其他终端上加载 - 防止源码泄漏
java代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。
4.2 自定义函数调用过程
- 隔离加载类
模块隔离,把类加载到不同的应用选中。比如tomcat这类web应用服务器,内部自定义了好几中类加载器,用于隔离web应用服务器上的不同应用程序。 - 修改类加载方式
除了Bootstrap加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。 - 扩展加载源
比如还可以从数据库、网络、或其他终端上加载 - 防止源码泄漏
java代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。
4.2 自定义函数调用过程
4.3 自定义类加载器实现实现方式:
所有用户自定义类加载器都应该继承ClassLoader类
在自定义ClassLoader的子类是,我们通常有两种做法:
- 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
- 重写findClass方法 (推荐)
垃圾回收机制及算法
1.垃圾回收概述
java垃圾回收的优缺点:
优点:
- a.不需要考虑内存管理,
- b.可以有效的防止内存泄漏,有效的利用可使用的内存,
- c.由于有垃圾回收机制,Java中的对象不再有"作用域"的概念,只有对象的引用才有"作用域"
缺点:
- java开发人员不了解自动内存管理, 内存管理就像一个黑匣子,过度依赖就会降低我们解决内存溢出/内存泄漏等问题的能力。
2.垃圾回收-对象是否已死
2.1 判断对象是否存活 - 引用计数算法
引用计数算法可以这样实现:给每个创建的对象添加一个引用计数器,每当此对象被某个地方引用时,计数值+1,引用失效时-1,所以当计数值为0时表示对象已经不能被使用。引用计数算法大多数情况下是个比较不错的算法,简单直接,也有一些著名的应用案例但是对于Java虚拟机来说,并不是一个好的选择,因为它很难解决对象直接相互循环引用的问题。
优点:
实现简单,执行效率高,很好的和程序交织。
缺点:
无法检测出循环引用。
2.2 判断对象是否存活-可达性分析算法
可达性分析算法
在主流的商用程序语言如Java、C#等的主流实现中,都是通过可达性分析(Reachability Analysis)来判断对象是否存活的。此算法的基本思路就是通过一系列的“GC Roots”的对象作为起始点,从起始点开始向下搜索到对象的路径。搜索所经过的路径称为引用链(Reference Chain),当一个对象到任何GC Roots都没有引用链时,则表明对象“不可达”,即该对象是不可用的。
3.垃圾收集算法
3.1 分代收集理论
思想也很简单,就是根据对象的生命周期将内存划分,然后进行分区管理。 当前商业虚拟机的垃圾收集器, 大多数都遵循了“分代收集”(Generational Collection)的理论进 行设计, 分代收集名为理论, 实质是一套符合大多数程序运行实际情况的经验法则
3.2 标记-清除算法
最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep) 算法, 在1960年由Lisp之父 John McCarthy所提出。 如它的名字一样, 算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回 收的对象, 在标记完成后,统一回收掉所有被标记的对象, 也可以反过来, 标记存活的对象, 统一回 收所有未被标记的对象。
标记过程就是对象是否属于垃圾的判定过程, 这在前一节讲述垃圾对象标记 判定算法时其实已经介绍过了。 之所以说它是最基础的收集算法, 是因为后续的收集算法大多都是以标记-清除算法为基础, 对其 缺点进行改进而得到的。
标记-清除算法有两个不足之处:
第一个是执行效率不稳定, 如果Java堆中包含大量对 象, 而且其中大部分是需要被回收的, 这时必须进行大量标记和清除的动作, 导致标记和清除两个过 程的执行效率都随对象数量增长而降低;
第二个是内存空间的碎片化问题, 标记、 清除之后会产生大 量不连续的内存碎片, 空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.3 标记-复制算法
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题, 1969年Fenichel提出了一种称为“半区复制”(Semispace Copying) 的垃圾收集算法, 它将可用 内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着 的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。 如果内存中多数对象都是存活的, 这种算法将会产生大量的内存间复制的开销, 但对于多数对象都是可回收的情况, 算法需要复制的就是占少数的存活对象, 而且每次都是针对整个半区进行内存回收, 分配内存时也就不用考虑有空间碎片的复杂情况, 只要移动堆顶指针, 按顺序分配即可.
但是这种算法也有缺点:
- 需要提前预留一半的内存区域用来存放存活的对象(经过垃圾收集后还存活的对象),这样导致可用的对象区域减小一半,总体的GC更加频繁了
- 如果出现存活对象数量比较多的时候,需要复制较多的对象,成本上升,效率降低
- 如果99%的对象都是存活的(老年代),那么老年代是无法使用这种算法的。
3.4 标记-整理算法
标记-复制算法在对象存活率较高时就要进行较多的复制操作, 效率将会降低。 更关键的是, 如果不想浪费50%的空间, 就需要有额外的空间进行分配担保, 以应对被使用的内存中所有对象都100%存活的极端情况, 所以在老年代一般不能直接选用这种算法。
针对老年代对象的存亡特征, 1974年Edward Lueders提出了另外一种有针对性的“标记-整 理”(Mark-Compact) 算法, 其中的标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存 。
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法, 而后者是移动式的。 是否移动回收后的存活对象是一项优缺点并存的风险决策:
是否移动对象都存在弊端, 移动则内存回收时会更复杂, 不移动则内存分配时会更复杂。 从垃圾收集的停顿时间来看, 不移动对象停顿时间会更短, 甚至可以不需要停顿, 但是从整个程序的吞吐量来看, 移动对象会更划算。
4.垃圾收集器
4.1垃圾收集器概述
1.垃圾回收器与垃圾回收算法
垃圾回收算法分类两类,第一类算法判断对象生死算法,如引用计数法、可达性分析算法 ;第二类收集死亡对象方法有四种,如标记-清除算法、标记-复制算法、标记-整理算法。一般的实现采用分代回收算法,根据不同代的特点应用不同的算法。垃圾回收算法是内存回收的方法论。垃圾收集器是算法的落地实现。和回收算法一样,目前还没有出现完美的收集器,而是要根据具体的应用场景选择最合适的收集器,进行分代收集。
2.垃圾收集器分类
串行垃圾回收(Serial)
串行垃圾回收是为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,不适合交互性强的服务器环境
并行垃圾回收(Parallel)
多个垃圾收集器线程并行工作,同样会暂停用户线程,适用于科学计算、大数据后台处理等多交互场景。
并发垃圾回收(CMS)
用户线程和垃圾回收线程同时执行,不一定是并行的,可能是交替执行,可能一边垃圾回收,一边运行应用线程,不需要停顿用户线程,互联网应用程序中经常使用,适用对响应时间有要求的场景。
G1垃圾回收
G1垃圾回收器将堆内存分割成不同的区域然后并发地对其进行垃圾回收。
3.七种垃圾收集器及其组合关系
根据分代思想,我们有7种主流的垃圾回收器
新生代垃圾收集器:Serial 、 ParNew 、Parallel Scavenge
老年代垃圾收集器:Serial Old 、 Parallel Old 、CMS
整理收集器:G1
垃圾收集器的组合关系
JDK8中默认使用组合是: Parallel Scavenge GC 、ParallelOld GC
JDK9默认是用G1为垃圾收集器
JDK14 弃用了: Parallel Scavenge GC 、Parallel OldGC JDK14 移除了 CMS GC
4.GC性能指标
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
暂停时间:执行垃圾回收时,程序的工作线程被暂停的时间
内存占用:java堆所占内存的大小
收集频率:垃圾收集的频次



