学习JVM-GC原理
1. 前言
Java和C++之间显著的一个区别就是对内存的管理。和C++把内存管理的权利赋予给开发人员的方式不同,Java拥有一套自动的内存回收系统(Garbage Collection,GC)简称GC,可以无需开发人员干预而对不再使用的内存进行回收管理。
垃圾回收技术(以下简称GC)是一套自动的内存管理机制。当计算机系统中的内存不再使用的时候,把这些空闲的内存空间释放出来重新投入使用,这种内存资源管理的机制就称为垃圾回收。
其实GC并不是Java的专利,GC的的发展历史远比Java来得久远的多。早在Lisp语言中,就有GC的功能,包括其他很多语言,如:Python(其实Python的历史也比Java早)也具有垃圾回收功能。
使用GC的好处,可以把这种容易犯错的行为让给计算机系统自己去管理,可以防止人为的错误。同时也把开发人员从内存管理的泥沼中解放出来。
虽然使用GC虽然有很多方便之处,但是如果不了解GC机制是如何运作的,那么当遇到问题的时候,我们将会很被动。所以有必要学习下Java虚拟机中的GC机制,这样我们才可以更好的利用这项技术。当遇到问题,比如内存泄露或内存溢出的时候,或者垃圾回收操作影响系统性能的时候,我们可以快速的定位问题,解决问题。
接下来,我们来看下JVM中的GC机制是怎么样的。
2. 哪些内存可以回收
首先,我们如果要进行垃圾回收,那么我们必须先要识别出哪些是垃圾(被占用的无用内存资源)。
Java虚拟机将内存划分为多个区域,分别做不同的用途。简单的将,JVM对内存划分为这几个内存区域:程序计数器、虚拟机栈、本地方法栈、Java堆和方法区。其中程序计数器、虚拟机栈和本地方法栈是随着线程的生命周期出生和死亡的,所以这三块区域的内存在程序执行过程中是会有序的自动产生和回收的,我们可以不用关心它们的回收问题。剩下的Java堆和方法区,它们是JVM中所有线程共享的区域。由于程序执行路径的不确定性,这部分的内存分配和回收是动态进行的,GC主要关注这部分的内存的回收。
对像实例是否是存活的,有两种算法可以用于确定哪些实例是死亡的(它们占用的内存就是垃圾),那么些实例是存活的。第一种是引用计数算法:
2.1 引用计数算法
引用计数算法会对每个对象添加一个引用计数器,每当一个对象在别的地方被引用的时候,它的引用计数器就会加1;当引用失效的时候,它的引用计数器就会减1。如果一个对象的引用计数变成了0,那么表示这个对象没有被任何其他对象引用,那么就可以认为这个对象是一个死亡的对象(它占用的内存就是垃圾),这个对象就可以被GC安全地回收而不会导致系统出现问题。
我们可以发现,这种计数算法挺简单的。在C++中的智能指针,也是使用这种方式来跟踪对象引用的,来达到内存自动管理的。引用计数算法实现简单,而且判断高效,在大部分情况下是一个很好的垃圾标记算法。在Python中,就是采用这种方式来进行内存管理的。但是,这个算法存在一个明显的缺陷:如果两个对象之间有循环引用,那么这两个对象的引用计数将永远不会变成0,即使这两个对象没有被任何其他对象引用。
public class ReferenceCountTest { public Object ref = null; public static void main(String ...args) {
ReferenceCountTest objA = new ReferenceCountTest();
ReferenceCountTest objB = new ReferenceCountTest(); // 循环引用 objA <--> objB
objA.ref = objB;
objB.ref = objA; // 去除外部对这两个对象引用
objA = null;
objB = null; System.gc();
}
}
上面的代码就演示了两个对象之间出现循环引用的情况。这个时候objA和objB的引用计数都是1,由于两个对象之间是循环引用的,所以它们的引用计数将一直是1,而即使这两个对象已经不再被系统所使用到。
由于引用计数这种算法存在这种缺陷,所以就有了一种称为“可达性分析算法”的算法来标记垃圾对象。
2.2 可达性分析算法
通过可达性分析算法来判断对象存活,可以克服上面提到的循环引用的问题。在很多编程语言中都采用这种算法来判断对象是否存活。
这种算法的基本思路是,确定出一系列的称为“GC Roots”的对象,以这些对象作为起始点,向下搜索所有可达的对象。搜索过程中所走过的路径称为“引用链”。当一个对象没有被任何到“GC Roots”对象的“引用链”连接的时候,那么这个对象就是不可达的,这个对象就被认为是垃圾对象。
从上面的图中可以看出,object1~4这4个对象,对于GC Roots这个对象来说都是可达的。而object5~7这三个对象,由于没有连接GC Roots的引用链,所以这三个对象时不可达的,被判定为垃圾对象,可以被GC回收。
在Java中,可以作为GC Roots的对象有以下几种:
- 虚拟机栈中的本地变量表中引用的对象,也就是正在执行函数体中的局部变量引用的对象。
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中(Native方法中)引用的对象
2.3 怎么判一个对象"死刑"
当通过可达性分析算法判定为不可达的对象,我们也不能断定这个对象就是需要被回收的。当我们需要真正回收一个对象的时候,这个对象必须经历至少两次标记过程:
当通过可达性分析算法处理以后,这个对象没有和GC Roots相连的引用链,那么这个对象就会被第一次标记,并判断对象的finalize()方法(在Java的Object对象中,有一个finalize()方法,我们创建的对象可以选择是否重写这个方法的实现)是否需要执行,如果对象的类没有覆盖这个finalize()方法或者finalize()已经被执行过了,那么就不需要再执行一次该方法了。
如果这个对象的finalize()方法需要被执行,那么这个对象会被放到一个称为F-Queue的队列中,这个队列会被由Java虚拟机自动创建的一个低优先级Finalizer线程去消费,去执行(虚拟机只是触发这个方法,但是不会等待方法调用返回。这么做是为了保证:如果方法执行过程中出现阻塞,性能问题或者发生了死循环,Finalizer线程仍旧可以不受影响地消费队列,不影响垃圾回收的过程)队列中的对象的finalize()方法。
稍后,GC会对F-Queue队列中的对象进行第二次标记,如果在这次标记发生的时候,队列中的对象确实没有存活(没有和GC Roots之间有引用链),那么这个对象就确定会被系统回收了。当然,如果在队列中的对象,在进行第二次标记的时候,突然和GC Roots之间创建了引用链,那么这个对象就"救活"了自己,那么在第二次标记的时候,这个存活的对象就被移除出待回收的集合了。所以,通过这种两次标记的机制,我们可以通过在finalize()方法中想办法让对象重新和GC Roots对象建立链接,那么这个对象就可以被救活了。
下面的代码,通过在finalize()方法中将this指针赋值给类的静态属性来"拯救"自己:
public class FinalizerTest {
private static Object HOOK_REF; public static void main(String ...args) throws Exception {
HOOK_REF = new FinalizerTest(); // 将null赋值给HOOK_REF,使得原先创建的对象变成可回收的对象
HOOK_REF = null;
System.gc();
Thread.sleep(1000); if (HOOK_REF != null) {
System.out.println("first gc, object is alive");
} else {
System.out.println("first gc, object is dead");
} // 如果对象存活了,再次执行一次上面的代码
HOOK_REF = null;
System.gc();
if (HOOK_REF != null) {
System.out.println("second gc, object is alive");
} else {
System.out.println("second gc, object is dead");
}
} @Override
protected void finalize() throws Throwable {
super.finalize();
// 在这里将this赋值给静态变量,使对象可以重新和GC Roots对象创建引用链
HOOK_REF = this;
System.out.println("execute in finalize()");
}
} #output:
execute in finalize()
first gc, object is alive
second gc, object is dead
可以看到,第一次执行System.gc()的时候,通过在方法finalize()中将this指针指向HOOK_REF来重建引用链接,使得本应该被回收的对象重新复活了。而对比同样的第二段代码,没有成功拯救的原因是:finalize()方法只会被执行一次,所以当第二次将HOOK_REF赋值为null,释放对对象的引用的时候,由于finalize()方法已经被执行过一次了,所以没法再通过finalize()方法中的代码来拯救对象了,导致对象被回收。
3. 怎么回收内存
上面我们已经知道了怎么识别出可以回收的垃圾对象。现在,我们需要考虑如何对这些垃圾进行有效的回收。垃圾收集的算法大致可以分为三类:
- 标记-清除算法
- 标记-复制算法
- 标记-整理算法
这三种算法,适用于不同的回收需求和场景。下面,我们来一一介绍下每个回收算法的思想。
3.1 标记-清除算法
"标记-清除"算法是最基础的垃圾收集算法。标记-清除算法在执行的时候,分为两个阶段:分别是"标记"阶段和"清除"阶段。
在标记阶段,它会根据上面提到的可达性分析算法标记出哪些对象是可以被回收的,然后在清除阶段将这些垃圾对象清理掉。
算法思路很简单,但是这个算法存在一些缺陷:首先标记和清除这两个过程的效率不高,其次是,直接将标记的对象清除以后,会导致产生很多不连续的内存碎片,而太多不连续的碎片会导致后续分配大块内存的时候,没有连续的空间可以分配,这会导致不得不再次触发垃圾回收操作,影响性能。
3.2 复制算法
复制算法,顾名思义,和复制操作有关。该算法将内存区域划分为大小相等的两块内存区域,每次只是用其中的一块区域,另一块区域闲置备用。当进行垃圾回收的时候,会将当前是用的那块内存上的存活的对象直接复制到另外一块闲置的空闲内存上,然后将之前使用的那块内存上的对象全部清理干净。
这种处理方式的好处是,可以有效的处理在标记-清除算法中碰到的内存碎片的问题,实现简单,效率高。但是也有一个问题,由于每次只使用其中的一半内存,所以在运行时会浪费掉一半的内存空间用于复制,内存空间的使用率不高。
3.3 标记-整理算法
标记-整理算法,思路就是先进行垃圾内存的标记,这个和标记-清除算法中的标记阶段一样。当将标记出来的垃圾对象清除以后,为了避免出现标记-清除算法中碰到的内存碎片问题,标记-整理算法会对内存区域进行整理。将当前的所有存活的对象移动到内存的一端,将一端的空闲内存整理出来,这样就可以得到一块连续的空闲内存空间了。
这样做,可以很方便地申请新的内存,只要移动内存指针就可以划出需要的内存区域以存放新的对象,可以在不浪费内存的情况下高效的分配内存,避免了在复制算法中浪费一部分内存的问题。
4. 分代收集
在现代虚拟机实现中,会将整块内存划分为多个区域。用"年龄"的概念来描述内存中的对象的存活时间,并将不同年龄段的对象分类存放在不同的内存区域。这样,就有了我们平时听说的"年轻代"、"老年代"等术语。
顾名思义,"年轻代"中的对象一般都是刚出生的对象,而"老年代"中的对象,一般都是在程序运行阶段长时间存活的对象。将内存中的对象分代管理的好处是,可以按照不同年龄代的对象的特点,使用合适的垃圾收集算法。
对于"年轻代"中的对象,由于其中的大部分对象的存活时间较短,很多对象都撑不过下一次垃圾收集,所以在年轻代中,一般都使用"复制算法"来实现垃圾收集器。
在上图中,我们可以看到"Young Generation"标记的这块区域就是"年轻代"。在年轻代中,还细分了三块区域,分别是:"eden"、"S0"和"S1",其中"eden"是新对象出生的地方,而"S0"和"S1"就是我们在复制算法中说到了那两块相等的内存区域,称为存活区(Survivor Space)。
这里用于复制的区域只是占用了整个年轻代的一部分,由于在新生代中的对象大部分的存活时间都很短,所以如果按照复制算法中的以1:1的方式来平分年轻代的话,会浪费很多内存空间。所以将年轻代划分为上图中所示的,一块较大的eden区和两块同等大小的survivor区,每次只使用eden区和其中的一块survivor区,当进行内存回收的时候,会将当前存活的对象一次性复制到另一块空闲的survivor区上,然后将之前使用的eden区和survivor区清理干净,现在,年轻代可以使用的内存就变成eden区和之前存放存活对象的那个survivor区了,S0和S1这两块区域是轮替使用的。
HotSpot虚拟机默认Eden区和其中一块Survivor区的占比是8:1,通过JVM参数"-XX:SurvivorRatio"控制这个比值。SurvivorRatio的值是一个整数,表示Eden区域是一块Survivor区域的大小的几倍,所以,如果SurvivorRatio的值是8,那么Eden区和其中Survivor区的占比就是8:1,那么总的年轻代的大小就是(Eden + S0 + S1) = (8 + 1 + 1) = 10,所以年轻代每次可以使用的内存空间就是(Eden + S0) = (8 + 1) = 9,占了整个年轻代的 9 / 10 = 90%,而每次只浪费了10%的内存空间用于复制。
并不是留出越少的空间用于复制操作越好,如果在进行垃圾收集的时候,出现大部分对象都存活的情况,那么空闲的那块很小的Survivor区域将不能存放这些存活的对象。当Survivor空间不够用的时候,如果满足条件,可以通过分配担保机制,向老年代申请内存以存放这些存活的对象。
对于老年代的对象,由于在这块区域中的对象和年轻代的对象相比较而言存活时间都很长,在这块区域中,一般通过"标记-清理算法"和"标记-整理算法"来实现垃圾收集机制。上图中的Tenured区域就是老年代所在的区域。而最后那块Permanent区域,称之为永久代,在这块区域中,主要是存放类对象的信息、常量等信息,这个区域也称为方法区。在Java 8中,移除了永久区,使用元空间(metaspace)代替了。
5. 总结
在这篇文章中,我们首先介绍了采用最简单的引用计数法来跟踪垃圾对象和通过可达性分析算法来跟踪垃圾对象。然后,介绍了垃圾回收中用到的三种回收算法:标记-清除、复制、标记-整理,以及它们各自的优缺点。最后,我们结合上面介绍的三种回收算法,介绍了现代JVM中采用的分代回收机制,以及不同分代采用的回收算法。
学习JVM-GC原理的更多相关文章
- 学习JVM虚拟机原理总结
0x00:JAVA虚拟机的前世今生 1991年,在Sun公司工作期间,詹姆斯·高斯林和一群技术人员创建了一个名为Oak的项目,旨在开发运行于虚拟机的编程语言,允许程序多平台上运行.后来,这项工作就演变 ...
- JVM GC原理
JVM原理 1.分代回收(目前JDK都采用此方式) 采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收.非堆区有CMS Perm Gen(持久化 ...
- JVM初探- 内存分配、GC原理与垃圾收集器
JVM初探- 内存分配.GC原理与垃圾收集器 标签 : JVM JVM内存的分配与回收大致可分为如下4个步骤: 何时分配 -> 怎样分配 -> 何时回收 -> 怎样回收. 除了在概念 ...
- JVM相关文章和GC原理算法
参考推荐: Java内存模型及GC原理 一个优秀的Java程序员必须了解的GC机制 Android 智能指针原理(推荐) Java虚拟机规范 Java虚拟机参数 Java内存模型 Java系列教程(推 ...
- 一篇笔记整理JVM工作原理
首先要了解的 >>数据类型 Java虚拟机中,数据类型可以分为两类:基本类型和引用类型. 基本类型的变量保存原始值,即:他代表的值就是数值本身:而引用类型的变量保存引用值.“引用值”代表了 ...
- 【深入JVM内核—原理、诊断与优化】第2期开课了
[深入JVM内核—原理.诊断与优化]的讲师“葛一鸣”,人称“一哥”,毕业于浙江工业大学,计算机软件与理论专业硕士,是国家认证系统分析师,OCP.2012年出版过<Java程序性能优化>,荣 ...
- 应用JConsole学习Java GC
应用JConsole学习Java GC 关于Java GC的知识,好多地方都讲了很多,今天我用JConsole来学习一下Java GC的原理. GC原理 在我的上一篇中介绍了Java运行时数据区,在了 ...
- Java 详解 JVM 工作原理和流程
Java 详解 JVM 工作原理和流程 作为一名Java使用者,掌握JVM的体系结构也是必须的.说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成:Java ...
- 一篇笔记带你梳理JVM工作原理
首先要了解的 数据类型 Java虚拟机中,数据类型可以分为两类:基本类型和引用类型. 基本类型的变量保存原始值,即:他代表的值就是数值本身:而引用类型的变量保存引用值.“引用值”代表了某个对象的引用, ...
- 一文读懂Java GC原理和调优
概述 本文介绍GC基础原理和理论,GC调优方法思路和方法,基于Hotspot jdk1.8,学习之后将了解如何对生产系统出现的GC问题进行排查解决 阅读时长约30分钟,内容主要如下: GC基础原理,涉 ...
随机推荐
- .net core 部署在Linux系统上运行的环境搭建总结
安装Linux用的是腾讯云的centos7.5,需要安装有环境有mysql5.7 .netcore2.1 nginx1.14 1.首先是mysql的安装 我用的链接工具是putty,首先root登入 ...
- Locust学习总结分享
简介: Locust是一个用于可扩展的,分布式的,性能测试的,开源的,用Python编写框架/工具,它非常容易使用,也非常好学.它的主要思想就是模拟一群用户将访问你的网站.每个用户的行为由你编写的py ...
- requests模块demo
import urllib.request import requests from requests.auth import HTTPBasicAuth from requests.auth imp ...
- Java编码思想之什么是高内聚低耦合?
分别描述的是模块内部特征,和模块外部引用关系. 内聚就是一个模块内各个元素彼此结合的紧密程度,高内聚就是一个模块内各个元素彼此结合的紧密程度高. 内聚是就其中任何一个模块的内部特征而言的. 耦合是就多 ...
- poj1830(高斯消元解mod2方程组)
题目链接:http://poj.org/problem?id=1830 题意:中文题诶- 思路:高斯消元解 mod2 方程组 有 n 个变元,根据给出的条件列 n 个方程组,初始状态和终止状态不同的位 ...
- P3241 [HNOI2015]开店 动态点分治
\(\color{#0066ff}{ 题目描述 }\) 风见幽香有一个好朋友叫八云紫,她们经常一起看星星看月亮从诗词歌赋谈到人生哲学.最近她们灵机一动,打算在幻想乡开一家小店来做生意赚点钱. 这样的想 ...
- 拓扑排序/DP【洛谷P2883】 [USACO07MAR]牛交通Cow Traffic
P2883 [USACO07MAR]牛交通Cow Traffic 随着牛的数量增加,农场的道路的拥挤现象十分严重,特别是在每天晚上的挤奶时间.为了解决这个问题,FJ决定研究这个问题,以能找到导致拥堵现 ...
- SP2713 GSS4 - Can you answer these queries IV
题目大意 \(n\) 个数,和在\(10^{18}\)范围内. 也就是\(\sum~a_i~\leq~10^{18}\) 现在有两种操作 0 x y 把区间[x,y]内的每个数开方,下取整 1 x y ...
- linux学习五
一.系统服务管理 1.概念 服务(service) 本质就是进程,但是是运行在后台的,通常都会监听某个端口,等待其它程 序的请求,比如(mysql , sshd 防火墙等),因此我们又称为守护进程,是 ...
- Go语言基础之4--流程控制
一.if else语句 1.1 基本语法1 语法1: if condition { //do something } 语法2: if condition { //do something } else ...