1. jvm与java体系结构
1.1 Java代码执行流程

2. 类加载子系统
2.1 内存结构
-
Class文件
-
类加载子系统
-
运行时数据区
-
- 方法区
-
- 堆
-
- 程序计数器
-
- 虚拟机栈
-
- 本地方法栈
-
执行引擎
-
本地方法接口
-
本地方法库
2.2 类的加载过程

加载阶段
- .class加载
链接阶段
- 验证
- 目的为了确保class文件的字节流包含信息符合当前虚拟机要求,保证正确性。
- 四种验证: 文件格式验证、元数据验证、字节码验证、符号引用验证
- 准备
- 为类变量分配内存并设置改类变量的初始值,即零值
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 解析
- 将
常量池内的符号引用转换为直接引用
- 将
初始化阶段
- 初始化阶段就是执行类构造器方法()的过程。
2.3 类加载器
JVM支持两种类的加载器。分别为
引导类加载器(Bootstrap classloader)
和自定义类加载器 (User-defined classloader)
引导类加载器
-
启动类加载器(引导类加载器, Bootstrap classloader)
-
它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
-
并不继承自ava.lang.ClassLoader,没有父加载器。
-
加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
-
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
-
-
扩展类加载器(Extension classloader)
-
Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
-
父类加载器为启动类加载器
-
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
-
-
应用程序类加载器(Application classloader)
-
java语言编写,由sun.misc.LaunchersAppClassLoader实现
-
父类加载器为扩展类加载器
-
它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
-
该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
-
通过ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器
-
用户自定义类加载器
在java的日常开发中,类的加载几乎都是由上述三种类加载器相互配合执行的,在必要时,我们好可以自定义类加载器,来定制的加载方式。为什么要自定义类加载器
隔离加载类、修改类的加载方式、防止源码泄露
用户自定义类加载器实现步骤:
-
开发人员可以通过继承抽象类ava.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
-
在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass() 方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass() 方法,而是建议把自定义的类加载逻辑写在findClass()方法中
-
在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
2.4 双亲委派机制
java虚拟机对class文件采用的是
按需加载
的方式,也就是说当需要使用该类时才会将它的class文件加载到内存。而且加载某个class文件时,java虚拟机采用的是双亲委派机制,即使吧请求交给父类处理,他是一种任务委派模式。
工作原理
1. 如果一个类加载器收到了类的加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载才会尝试自己加载。这就是双亲委派机制。
优势
- 避免类的重复加载
- 保护程序安全,防止核心api被随意篡改
3. 程序计数器
3.1 程序计数器(PC寄存器)
作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
概念
寄存器是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
。 所以不存在GC、OOMzai
在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一直。
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法以及jvm指令地址。
4. 虚拟机栈
4.1 虚拟机栈概述
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
java虚拟机栈是什么?
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次次java方法的调用。
生命周期
生命周期和线程一致
作用
主管java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
特点
栈式一种快速有效的分配存储方式,访问速度仅次于程序计数器
-
出栈
-
入栈
4.2 栈的存储单位

每个线程都有自己的栈,栈中的数据都是以
栈帧
的形式存在
4.2.1 栈帧的内部结构
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
- 一些附加信息
每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面有很多栈帧,栈帧的大小主要由局部变量表和操作数栈决定的。
4.3 局部变量表
-
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
-
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
-
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
-
方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
-
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
4.3.1 Slot的理解
- 局部变量表,最基本的存储单元是 slot(变量槽)
- 参数值的存放都是index0开始的
- 局部变量表中存放编译期各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
- 如果当前栈帧是由构造法方法或者实例方法创建的,name改对象的引用this将会存在index0的位置。

4.4 操作数栈
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
操作数栈,主要用于保存计算过程中的中间结果,同事作为计算过程中变量临时的存储空间
运行时数据区 | 是否存在Error | 是否存在GC |
---|---|---|
程序计数器 | 否 | 否 |
虚拟机栈 | 是(SOE) | 否 |
本地方法栈 | 是 | 否 |
方法区 | 是(OOM) | 是 |
堆 | 是 | 是 |
5. 本地方法接口和本地方法栈
5.1. 什么是本地方法?
简单地讲,一个Native Method是一个Java调用非Java代码的接囗。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C中,你可以用extern “c” 告知c编译器去调用一个c的函数。
5.2. 本地方法栈
Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
6. 堆
6.1 概述
堆针对一个jvm进程来说是唯一的,也就是说一个进程只有一个jvm,但是进程包含多个线程,他们是共享同一个对空间的
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
6.1.1 堆内存细分
jdk7:
jdk7及以前:新生代+老年代+永久代
-
Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
-
Tenure generation space 养老区 Old/Tenure
-
Permanent Space 永久区 Perm
jdk8:
Java 8及之后:新生区+养老区+元空间
-
Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
-
Tenure generation space 养老区 Old/Tenure
-
Meta Space 元空间 Meta
6.2 堆空间大小设置
-
“-Xms"用于表示堆区的起始内存,等价于
-XX:InitialHeapSize
-
“-Xmx"则用于表示堆区的最大内存,等价于
-XX:MaxHeapSize
6.3 年轻代与老年代
存储在JVM中的Java对象可以被划分为两类:年轻代(YoungGen)和老年代(oldGen)
一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。
另外一类对象的生命周期非常长,在某些极端的情况下还能与jvm的生命周期保持一直。
`其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)
6.4 对象分配过程
-
new的对象先放到伊甸园区。此区有大小限制
-
当伊甸园的空间满后,程序又要创建对象,JVM的垃圾回收器将会对伊甸园区进行垃圾回收(MinorGc) ,将伊甸园区中不再被其他对象引用的对象销毁。再家在新的对象到伊甸园区。
-
然后将伊甸园中的剩余对象移动到 幸存者0 区。
-
如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有被回收,就会放到幸存者1区
-
如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区
-
啥时候能进养老区呢? 可以设置次数,默认是15次。-Xx:MaxTenuringThreshold= N
-
在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major GC,进行养老区垃圾回收
-
若养老区执行Major GC之后,发现依然无法进行对象的保存,就会产生内存溢出异常OOM。
java.lang.OutofMemoryError: Java heap space
6.5. Minor GC,MajorGC、Full GC
JVM在进行GC的时候,并非每次都对上面三个空间进行GC,大部分回收的都是新生代。
针对HostspotVM的实现,它里面的GC按照回收区域又分为两大类型: 部分收集Partial GC、整堆收集 Full Gc
部分收集 Partial GC
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾回收。
整堆收集 Full GC
: 收集整个java堆和方法区的垃圾收集。
6.6 堆空间参数设置
// 详细的参数内容会在JVM下篇:性能监控与调优篇中进行详细介绍,这里先熟悉下
-XX:+PrintFlagsInitial //查看所有的参数的默认初始值
-XX:+PrintFlagsFinal //查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms //初始堆空间内存(默认为物理内存的1/64)
-Xmx //最大堆空间内存(默认为物理内存的1/4)
-Xmn //设置新生代的大小。(初始值及最大值)
-XX:NewRatio //配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio //设置新生代中Eden和S0/S1空间的比例
-XX:MaxTenuringThreshold //设置新生代垃圾的最大年龄
-XX:+PrintGCDetails //输出详细的GC处理日志
//打印gc简要信息:①-Xx:+PrintGC ② - verbose:gc
-XX:HandlePromotionFalilure://是否设置空间分配担保
7 方法区
7.1 概述
- 方法区与java堆一样,是各个
线程共享
的内存区域 - 方法区在jvm启动的时候被创建,并且它的实际的物理内存空间中和java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样可以
配置固定大小
或者可拓展 - 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误
7.2 方法区的演进
在jdk7及以前,习惯把方法区成为永久代。在jdk8开始,使用元空间替代了永久代。
7.3 方法区的内部结构
类型信息
对每个加载的类型(类class,接口,枚举,注解),JVM必须在方法区中存储以下类型信息。
- 这个类型的保证有效名称 全名:报名.类型
- 这个类型直接父类的完整有效名
- 这个类型的修饰符 public、abstract等
- 这个类型直接接口的一个有序列表
域(Field)信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(public,protected,static,final,transient的某个子集)
方法(Method)信息
JVM必须保存所有方法的一下信息,同域信息一样包括生命顺序:
- 方法名称
- 方法的返回值
- 方法参数的数量和类型
- 方法的修饰符
- 方法的字节码
- 异常表
non-final的类变量
-
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
-
类变量被类的所有实例共享,即使没有类实例时,你也可以访问它
补充说明:全局常量(static final)
被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
7.4 运行时常量池VS常量池
-
方法区
,内部包含了运行时常量池 -
字节码文件
,内部包含了常量池 -
要弄清楚方法区,需要理解清楚ClassFile,因为加载类的信息都在方法区。
-
要弄清楚方法区的运行时常量池,需要理解清楚ClassFile中的常量池。
运行时常量池
-
运行时常量池(Runtime Constant Pool)是方法区的一部分。
-
常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
-
运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
-
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
-
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
-
运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
-
运行时常量池类似于传统编程语言中的符号表(symboltable),但是它所包含的数据却比符号表要更加丰富一些。
-
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutOfMemoryError异常。
7.5 方法区的演进细节
-
首先明确:只有Hotspot才有永久代。BEA JRockit、IBMJ9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一
-
Hotspot中方法区的变化:
JDK1.6及之前 | 有永久代(permanet),静态变量存储在永久代上 |
---|---|
JDK1.7 | 有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中 |
JDK1.8 | 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。 |
8. 对象实例化以及直接内存
面试题
美团:
对象在JVM中是怎么存储的?
对象头信息里面有哪些东西?
蚂蚁金服:
Java对象头有什么?
8.1 对象实例化
8.2 对象内存布局
8.2.1 对象头
对象头包含了两部分,分别是 运行时元数据(mark word) 和类型指针。 如果是数组,还需要记录数组的长度。
运行时数据
- 哈希值
- GC分代年龄
- 锁状态标识
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
类型指针
指向类元数据InstanceClass,确定改对象所属的类型。
8.2.2 实例数据
它是真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类集成下来的和本身拥
有的字段)
- 相同宽度的字段总是会被分配到一起
- 父类中定义的变量会出现在子类之前
- 如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙
对齐填充(Padding)不是必须的,也没有特别的含义,仅仅起到占位符的作用
8.3 对象的访问定位
8.3.1 对象访问的两种方式
句柄访问
reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改
直接指针
9. 执行引擎
执行引擎属于JVM的下层,里面包括
解释器、及时编译器、垃圾回收器
。执行引擎的作用就是将字节码指令解释/编译为对应平台上的本地机器指令。充当一个翻译的角色
9.1.1 执行引擎的工作流程
- 执行引擎在执行的过程中究竟要执行什么的字节码指令完全依赖于pc寄存器。
- 每当执行一项执行操作后,pc寄存器都会更新下一条需要执行的指令地址。
- 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
10. StringTable
字符串常量池是不会存储相同内容的字符串的
10.1 String的基本特性
- string字符串,使用一对""引起来表示
- string是生命final的,不能被继承
- string实现了Serializable接口,表示字符串是支持序列化的
- string实现了Comparable接口,标识string可以比较带下。
string在jdk8及以前内部定义了final char[] value 用于存储字符串的数据,jdk9时改成byte[]
10.2 String的内存分配
在java中有8种基本数据类型和一种特殊的类型string。这些类型为了使它们在运行过程中速度更快,更节省牛才能,都提供了一种常量池的概念
String类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号生命出来的string对象会直接存储在常量池中。
- 如果不是用双引号生命的string对象,可以使用string提供的
intern()
方法
在jdk6及以前,字符串常量池都存在永久代`
jdk7中字符串常量池的位置调整到java堆中。
StringTable为什么要调整?
在JDK 7中,内部字符串不再分配在Java堆的永久代中,而是分配在Java堆的主要部分(称为年轻代和老年代),与应用程序创建的其他对象一起。这种变化将导致更多的数据驻留在主Java堆中,而更少的数据在永久代中,因此可能需要调整堆的大小。大多数应用程序将看到由于这一变化而导致的堆使用的相对较小的差异,但加载许多类或大量使用String.intern()方法的大型应用程序将看到更明显的差异。
10.3 intern()
的使用
当调用intern方法时,如果池子里已经包含了一个与这个String对象相等的字符串,正如equals(Object)方法所确定的,那么池子里的字符串会被返回。否则,这个String对象被添加到池中,并返回这个String对象的引用。
JDK1.6中,将这个字符串对象尝试放入串池。
-
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
-
如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
JDK1.7起,将这个字符串对象尝试放入串池。
-
如果串池中有,则并不会放入。返回已有的串池中的对象的地址
-
如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
11. 垃圾回收概述及算法
关于垃圾收集有三个经典问题:
哪些内存需要回收?
什么时候回收?
如何回收?
11.1 垃圾回收概述
垃圾是指运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
如果不及时对内存中的垃圾进行回收,那么,这些垃圾对象所占的内存会一直保留到应用程序的结束,从而导出内存溢出。
11.2 垃圾回收相关算法
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要
区分出内存中哪些是存活对象
,哪些是已经死亡的对象
。只有被标记为己经死亡的对象,GC才会在执行垃圾回收
11.2.0 对象的finalization机制
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
生存还是死亡?
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
-
可触及的
:从根节点开始,可以到达这个对象。 -
可复活的
:对象的所有引用都被释放,但是对象有可能在finalize()中复活。 -
不可触及的
:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
具体过程
判定一个对象objA是否可回收,至少要经历两次标记过程:
-
如果对象objA到GC Roots没有引用链,则进行第一次标记。
-
进行筛选,判断此对象是否有必要执行finalize()方法
-
如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
-
如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
-
finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
11.2.1 标记阶段: 引用计数算法
引用计数算法比较简单,对每个对象保存一个整型的引用计数属性,用于记录对象的被引用情况。
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
-
优点:
- 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
-
缺点:
-
它需要蛋哥的字段存储计数器,这样增加了储存空间的开销
-
每次赋值都要更新计数器,伴随着加法减法,这增加了时间开销
-
引用计数器有一个严重的问题,即无法处理
循环引用
的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
-
11.2.2 标记阶段: 可达性分析算法
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
基本思路
-
可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
-
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
-
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
-
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
11.2.3 清除阶段:标记-清除算法 Mark-Sweep
在标记阶段确定了哪些垃圾可以被回收后,垃圾收集器就要进行垃圾回收,但是里面涉及到一个问题就是:如何高效的进行垃圾回收。
目前在JVM中比较常见的三种垃圾收集算法是
标记一清除算法(Mark-Sweep)、复制算法(copying)、标记-压缩算法(Mark-Compact)
Mark-Sweep | Mark-Compact | Copying | |
---|---|---|---|
速率 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍空间(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
核心思想:
- 标记清除法就是先找到内存中存活的对象并对其进行标记,然后统一把未标记的对象统一清理。

优点:
- 标记清除法特点就是简单直接,速度也非常快,适合可回收对象不多的场景。
缺点:
- 1.会造成不连续的内存空间 2.性能不稳定
11.2.4 清除阶段: 复制算法 Copying
核心思想:
- 将或者的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在内存中存活对象复制到另一块未被使用的内存快中,之后清除正在使用的内存快中的所有对象,交换两个内存角色,最后完成垃圾回收。
优点:
-
没有标记和清除过程,实现简单,运行高效
-
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点
-
此算法的缺点也是很明显的,就是需要两倍的内存空间。
-
对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小
应用场景:
在新生代,对常规应用的垃圾回收,一次通常可以回收70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
11.2.5 清除阶段:标记-压缩(整理)算法
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
执行过程:
- 第一阶段和标记清除算法一样,从跟节点开始标记所有被引用的对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放
- 之后,清理边界外所有的内存空间。

12. 引用
【既偏门又非常高频的面试题】强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?
- 强引用(StrongReference): 最传统的"引用"的定义,是指在程序代码中普遍存在的引用赋值,
垃圾回收器永远不会回收强引用的对象
- 软引用(SoftReference):
在系统将要发生内存溢出之前,将会把这些对象列入回收范围进行二次回收
。如果这次回收后还没有足够的内存,才会抛出内存溢出异常 - 弱引用(WeakReference):
被弱引用关联的对象只能生存到下一次垃圾收集之前
。当垃圾收集器工作时,无论空间是否足够都会回收掉被弱引用关联的对象 - 虚引用(PhantomReference): 一个对象是否有虚引用的存在,完全不会对其生存时间产生影响。也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
12.1 强引用(Strong Reference)——不回收
在java程序中,最常见的引用类型就是强引用,也就是最常见的普通对象引用。
强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。
StringBuffer str = new StringBuffer("hello mogublog"); //强引用例子
12.2 软引用(Soft Reference)——内存不足即回收
弱引用是用来描述一些还有用,但非必须的对象。
只被软引用关联着的对象,在系统将要生内存溢出之前,会把这些对象列入回收返回进行二次回收
,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
Object obj = new Object(); // 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用
12.3 弱引用(Weak Reference)——发现即回收
弱引用也是描述那些非必须的对象,
只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
面试题:你开发中使用过WeakHashMap吗?
WeakHashMap用来存储图片信息,可以在内存不足的时候,及时回收,避免了OOM
12.4 虚引用(Phantom Reference)——对象回收跟踪
一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。
为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
13. 垃圾回收器
13.1 7种经典的垃圾收集器
- 串行回收器: Serial、Serial Old
- 并行回收器: ParNew、Parallel Scavenge、Parallel old
- 并发回收器:
CMS、G1
13.2 7款经典收集器与垃圾分代之间的关系
-
新生代收集器:Serial、ParNew、Parallel Scavenge;
-
老年代收集器:Serial Old、Parallel Old、CMS;
-
整堆收集器:G1;
13.3 Serial回收器: 串行回收
Serial收集器是最基本,历史最悠久的垃圾收集器了。jdk1.3之前回收新生代唯一选择
Serial收集器采用复制算法、串行回收和"Stop-the-world"机制的方式执行内存回收。
**特点:**单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
13.4 ParNew回收器: 并行回收
如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本
。Par是Parallel的缩写,New:只能处理的是新生代
ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。
13.5. Parallel回收器:吞吐量优先
HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制
。
13.6 CMS回收器: 低延迟
CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the-World"
不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。
CMS整个过程比之前的收集器都要复杂,整个过程分为4个主要阶段;
-
初始阶段
在这个阶段中,程序中所有的工作线程都将因为Stop-the-world 机制而出现短暂的暂停,这个阶段主要的任务
仅是标记出GCRoot能关联到的对象
。一旦标记完成就会恢复之前暂停的应用阶段。 -
并发阶段
从GCRoots的直接关联对象开始遍历整个对象图的过程.这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发
-
重新标记
由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
-
并发清楚
此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
13.7 G1回收器: 区域化分代式
G1回收器的特点(优势):
-
并行并发
-
并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
-
并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
-
-
分代收集
- 从分代上来看,
1依然属于分代型垃圾回收器``它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区
但是从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。 - 将堆空间分为若干区域,这些区域中包含了逻辑上的年轻代和老年代
- 和之前的各类回收器不同。
同时兼顾年轻代和老年代
对比其它回收器,或者工作在年轻代或者工作在老年代
- 从分代上来看,
G1垃圾收集器的缺点
- 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
程,可以与垃圾收集线程一起并发
-
重新标记
由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
-
并发清楚
此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
[外链图片转存中…(img-nilUZwpc-1637303742264)]
13.7 G1回收器: 区域化分代式
G1回收器的特点(优势):
-
并行并发
-
并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW
-
并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
-
-
分代收集
- 从分代上来看,
1依然属于分代型垃圾回收器``它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区
但是从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。 - 将堆空间分为若干区域,这些区域中包含了逻辑上的年轻代和老年代
- 和之前的各类回收器不同。
同时兼顾年轻代和老年代
对比其它回收器,或者工作在年轻代或者工作在老年代
- 从分代上来看,
G1垃圾收集器的缺点
-
相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
-
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
笔记部分内容来自尚硅谷:尚硅谷宋红康JVM全套教程(详解java虚拟机)