深入理解JVM内存回收机制(不包含垃圾收集器)
目录
- 垃圾回收发生的区域
- 如何判断对象是否可以被回收
- HotSpot实现
- 垃圾回收算法
- JVM中使用的垃圾收集算法
- GC的分类
- 总结
- 参考资料
垃圾回收发生的区域
堆是java
创建对象的区域(String
对象在常量池中),也是垃圾回收最多的地方。但是除了堆空间还有方法区存在需要回收的垃圾
回收方法区
废弃的常量
在常量池中存在一个字面量A
,如果系统中没有一个地方引用`A``,这时候发生垃圾回收,如果有必要这个字面量就会被清理出常量池。
注意是如果有必要。比如上一篇文章中引用的例子,就没有回收字符串。
无用的类
当满足以下条件时,这个类就可以被回收,而不是一定会回收。
- 所有的类实例都已经被回收也就是java堆里面不存在该类的任何实例
- 加载类的
ClassLoader
已经被回收- 该类对应的
Java.long.Class
对象任何地方被引用,无法通过反射访问该类的方法。
如何判断对象是否可以被回收
java
有一个非常大的好处就是会自动进行垃圾回收,而不用手动释放对象所占用的内存。当以一个对象不再被引用的时候就可以进行垃圾回收,那么如何判断一个对象是否在被使用呢?
引用计数法
引用计数法很简单,只需要在对象创建之初给对象加一个引用计数器,每当有一个地方引用他就+1,引用失效就-1,当引用计数器为0,则对象不再被引用。每次垃圾回收,
只需要遍历一遍所有的引用计数器就可以。但是对于循环引用,引用计数法则无法释这两个对象。
可达性分析算法
通过一系列被称为GC Root
的对象为起点,从这些节点往下搜索,搜索走过的路径称之为引用链,当一个对象到GC Root
没有任何引用链的时候,则证明此对象不可达。
在JVM
中,可以被用作GC Root
的对象有:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
HotSpot实现
枚举根节点
对于根节点的枚举有如下的问题:
可以作为根节点(
GC Roots
)的节点主要是全局性的引用(方法去中静态属性引用的对象和方法区中常量引用的对象)与执行上下文(栈中引用的对象)在一次可达性分析过程中,不能出现分析过程中对象引用关系还在不断变化的情况,否则无法保证分析结果的准确性,为了达到这一目的,
GC
过程中就必须停顿所有的java线程垃圾收集时,手机线程会对栈上的内存进行扫描,看看哪些位置存储了
Reference
类型,如果发现某个位置确实存的是Reference
类型,整个Reference
所引用的对象就可以作为根节点,
他所能到达的对象都不能被回收。栈上的本地变量表中只有一部分是
Reference
类型,而那些非Reference
类型的数据对于垃圾回收毫无用处,但是如果对于栈进行全栈扫描将会是一种对时间和资源的浪费,尤其是暂停了用户线程
解决方法
是否可以用额外的空间记录下每个Reference
的位置,这样的话GC
的时候从这个结构中直接读取这个结构,而不用进行全栈扫描。事实上,大部分主流的虚拟机也确实是这样做的,
以HotSpot
为例,它使用一种OopMap
的数据结构来保存这类信息。
一个栈意味着一个线程,而一个栈桢代表了一个方法,每个被JIT
编译过后的方法会在一些特定的位置记录下OopMap
记录了执行到该方法的某条指令的时候,栈上和寄存器的哪些位置是引用,
这样GC
在扫描到这些栈的时候就会查询这些OopMap
就知道哪里是引用。这些位置主要在:
- 循环的末尾
- 方法临返回前/调用方法的
call
指令之后 - 可能抛出异常的位置
而这些位置就被称之为“安全点”,之所以要选择一些特定位置来记录OopMap
,是因为如果对每条指令的位置都记录OopMap
的话,这些记录就会比较大,那么空间开销就会显得不值得。
GC
发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的OopMap
,枚举根节点时,递归遍历每个栈桢的OopMap
,通过栈中记录的被引用的对象的内存地址,即可找到这些对象。
安全点与安全区域
安全点
程序在执行时并不是任何时间都可以进行GC
,只有到达有OopMap
记录的位置才可以执行GC
,整个位置称之为安全点
安全点的选定基本是以程序“是否具有让程序长时间执行的特征”为标准选定的。程序一般不会因为指令流太长而长时间执行(每个指令执行的时间都很短)。“长时间执行”
的典型特征就是指令序列的服用,例如:循环、递归、方法调用。所以具有这些功能的指令才会产生安全点。
安全区域
安全区域指在这一段代码之中,引用关系不会发生变化,在这一段代码之中,任一点都是安全点。任何一个地方都可以中断线程开始GC
。
当线程执行到安全区域后,首先标识自己已经进入安全区域,那么这段时间JVM
要发起GC
时就不用管标记自己进入安全区的线程。线程要离开安全区时,首先需要先检查
系统是否已经完成了根节点的选举,如果完成则线程继续执行,否则要继续等待收到可以安全离开安全区的信号。
如何保证GC
发生时,所有的线程都跑到了安全点上呢?
当要进行GC
的时候,会让所有的线程都在安全点中断,就有两种方式:
- 抢占式中断:不需要代码配合。当
GC
发生时,让所有的线程都终端,然后让不在安全点的线程继续执行到安全点上。不过一般不采用这种方式 - 主动式中断:当
GC
需要中断线程时,不对线程进行操作,仅设置一个标识。各个线程轮询这个标识,当发现这个标识被设置时,使得程序运行到最进的安全点时,主动挂起。
标识的设置和安全点是重合的,标识的设置和安全点是重合的。除此之外还有一个创建对象需要分配内存的地方。
垃圾回收算法
假设存在如下的内存区域:
下文将以这块内存为例进行垃圾收集算法的分析
标记-清除算法
顾名思义,标记清除算法会为两个阶段,1-标记,2-清除。
- 标记:垃圾收集器从
GC Roots
出发,进行搜索,然后对所有可以访问的对象打上标识,标记其为可达的对象,标记一般保存在header中
- 清除:垃圾收集器对堆内存进行线性遍历,如果发现某个对象没有被标记为可达,就会将其回收,回收后效果如下图
优点
- 实现简单
- 与保守式
GC
算法兼容
缺点
- 内存碎片化严重
- 分配速度缓慢,由于空闲块的维护是用链表实现的,分块可能不连续,每次分配都需要遍历链表,极端情况下要遍历震整个链表。
- 标记和清除的效率都不高,
复制算法
复制算法,就是将内存划分为相等的两块,每次只是用其中一块,当这块内存使用完了就将还存活的对象复制到另一块,然后将这块空间清理掉,这样使得每次对内存的回收都是半区回收。
复制算法的示意图如下图:
优点
- 内存分配时不用考虑碎片的情况只需要移动栈顶指针分配内存即可
- 实现简单,高效
缺点
- 可用内存缩小为原来的一半
标记—整理算法
复制算法在对象存活较多的时候会进行较多的操作,如果对象全部存活复制将会进行100%,并且浪费50%的内存空间作为担保。
标记—整理算法和标记—清除算法前半部分一样,只是后续不是清理,而是让所有存活的对象都向一端移动,然后清理掉边界以外的内存。
JVM中使用的垃圾收集算法
在当前主流的垃圾收集器当中(g1
除外),基本都采用一种分代收集算法。根据对象存活周期,将java堆分为新生堆和老年堆。对于新生堆,采用复制算法,对于老年堆采用标记-清除或者标记-整理算法。
研究人员发现大多数的对象都是“朝生夕灭”,对于这样的对象,生存周期很短,可以将其放入新生堆,因为其生存时间很短,所以新生堆采用复制算法的时候没有必要使用1:1的比例划分内存。
而是分为较大的Eden
空间和两块较小的Suvivor
空间;HotSpot
的Eden
和Suvivor
的比例为8:1。回收时将Eden
和一块Suvivor
上还存活的对象,一次性copy到另一块Suvivor
上,然后清理掉以前的两块区域。这样每次新生代可用的内存空间占整个新生堆的90%,只有10%会被浪费。
我们没有办法保证新生代回收的时候只剩下不多于10%的对象存活。当Suvivor
空间不够用时,就需要依赖其他内存(老年堆)进行分配担保。对于存活过一定gc次数的对象放进老年堆。
老年堆对象存活率高,使用复制算法可能就需要1:1的空间,这样就会浪费内存,因此使用的是标记-清除或者标记-整理算法。
GC的分类
保守式GC
HotSpot
虚拟机在栈上使用OopMap
记录下了哪些位置是引用类型,根据记录的类型类型开始查找堆中存活的对象。
虚拟机最初的实现当中是没有记录每个数据的类型的,JVM
也无法区分内存里某个位置的数据到底应该解读为引用类型还是其他数据类型,这种条件下,实现出来的GC
就是“保守式GC
”。在进行GC
时,JVM
开始从一些已知的位置(例如栈)开始扫描内存,扫描的时候每看到一个数字就看看它“像不像是一个指向GC堆中的指针”。
这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),之类的。然后递归的这么扫描出去。
优点
- 实现简单
缺点- 会有部分对象本来应该已经死了,但有疑似指针指向它们,使它们逃过
GC
的收集。会有一部分已经不需要的数据占用着GC
堆空间,但是所有应该存活的对象都会活着,对程序语义来说时安全的- 由于是疑似指针,那么就不知道这个到底是不是指针,所以这些值就都不能改写。移动对象就需要改写指针,也就是说对象不可移动,因此一般使用标记-清除的方式来进行垃圾回收。
有一种办法可以在使用保守式GC
的同时支持对象的移动,那就是增加一个间接层,不直接通过指针来实现引用,而是添加一层“句柄”(handle
)在中间,所有引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容即可。
半保守式GC
保守式GC
没有在JVM
中记录任何类型信息,半保守式GC
会在对象上记录类型信息,这样的话,扫描栈的时候仍然和保守式GC
一样,但是扫描到堆上的时候,对象上带了足够的类型信息,
JVM
就能判断出栈中这个位置是不是一个指向堆中对象的指针,以及这个对象内什么位置数据是引用类型,这种是“半保守式GC
”,也称之为“根上保守”。
由于半保守式GC
在堆内部的数据是准确的,所以它可以在直接使用指针来实现引用的条件下支持部分对象的移动,方法是只将保守扫描能直接扫到的对象设置为不可移动(pinned
),而从它们出发再扫描到的对象就可以移动了。
准确式GC
对于垃圾回收,JVM
关心的就是扫描的根节点是不是一个指向堆内存的指针,那么就是在栈上记录下那个位置式引用类型,是指向堆上对象的指针,在HotSpot
虚拟机中这个数据结构就是OopMap
总结
- 垃圾回收不止是发生在堆区,对于方法区中产生的垃圾有可能会被回收。在之前的从JDK源码理解java引用一文中举了不会被回收的例子
- 虚拟机一般采用引用可达性分析算法来寻找不被使用的对象,其实寻找到的是正在被使用的对象,剩下的就是不再被使用的对象。
- 除了
g1
垃圾收集器。其他的垃圾收集器都有明显的区分老年代和新生代进行垃圾回收,由于老年代和新生代对象存货时间不一样,采用不同的垃圾回收算法
参考资料
- 深入理解Java虚拟机 周志明著
- JVM 之 OopMap 和 RememberedSet
- 找出栈上的指针/引用
深入理解JVM内存回收机制(不包含垃圾收集器)的更多相关文章
- JVM内存回收机制简述
JVM内存回收机制涉及的知识点太多了,了解越多越迷糊,汗一个,这里仅简单做个笔记,主要参考<深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)> 目前java的jdk默认虚拟机为H ...
- JVM内存回收机制
1. JVM内存回收机制简述 http://www.cnblogs.com/lzrabbit/p/3826738.html
- JVM内存回收机制——哪些内存需要被回收(JVM学习系列2)
上一篇文章中讨论了Java内存运行时的各个区域,其中程序计数器.虚拟机栈.本地方法栈随线程生灭,且创建时需要多少内存,基本上在译期间就决定的了,所以在内存回收时无需特殊的关注.而堆和方法区则不同,首先 ...
- JVM中内存回收深入分析,各种垃圾收集器
JVM启动有两种模式,client和server 一般JVM启动时会根据主机情况分析选择采用那种模式启动 可发现是server模式 JVM中尤其需要关注的就是HEAP堆区 堆区分为新生代和老年代 新生 ...
- Java jvm 内存回收机制
http://blog.csdn.net/yaerfeng/article/details/51291903 在Java中,它的内存管理包括两方面:内存分配(创建Java对象的时候)和内存回收,这两方 ...
- Java技术专题之JVM逻辑内存回收机制研究图解版
一.引言 JVM虚拟机内存回收机曾迷惑了不少人,文本从JVM实现机制的角度揭示JVM内存回收的原理和机制. 一.Java平台逻辑架构 二.JVM物理结构 通过从JVM物理结构图我们可以看到: 1.JV ...
- Java进阶3. 内存回收机制
Java进阶3. 内存回收机制 20131029 前言: 学过C++的都知道,C++中内存需要程序员自己维护.说道这里,很多开发的同学就感觉很痛苦,当他转向Java的时候,就会说你看Java多好啊,程 ...
- Java虚拟机(三):JVM垃圾回收机制
概述 垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了. jvm 中,程序计数器.虚拟机栈.本地方 ...
- jvm系列(三):java GC算法 垃圾收集器
GC算法 垃圾收集器 概述 垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了. jvm 中,程序计 ...
随机推荐
- java 拦截器解决xss攻击
一.xss攻击 XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序.这些恶意网页程序通常是JavaScript,但实际上也 ...
- Spring整合JDBC temple
一.Spring对Jdbc的支持 Spring为了提供对Jdbc的支持,在Jdbc API的基础上封装了一套实现,以此建立一个 JDBC 存取框架. 作为 Spring JDBC 框架的核心, JDB ...
- 深入理解跨域SSO单点登录原理与技术
[本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 一:SSO体系结 ...
- mysql错误详解(1819):ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
O(∩_∩)O哈哈~ 在学习 Mysql 的时候又遇到了新问题了 o(╥﹏╥)o 当我在准备为用户授权的时候: grant all privileges on *.* to 'root'@'%' id ...
- SQL注入之注入点的寻找
注入点的判断 判断一个链接是否存在注入漏洞,可以通过对其传入的参数(但不仅仅只限于参数,还有cookie注入,HTTP头注入等) 进行构造,然后对服务器返回的内容进行判断来查看是否存在注入点. 注入点 ...
- layui动态添加的元素click等事件触发不了的解决办法
在页面加载完成时候 '.add_project' 元素是可以触发click时间的,当动态添加 '.add_project' 时候,新添加的元素却触发不了click事件,类似下面的写法: $(" ...
- vim 常用指令-持续更新
1. 查询文件夹file1内文件数量:ls file1 | wc -l 2. 生成文件夹file1内的文件列表:find file1 -type f > list.txt 3. git管理文件, ...
- GIT更换连接方式
1-使用 git remote -v 查看对应的克隆地址: git remote -v origin https://github.com/username/repository.git (fetch ...
- IIS 发布页面后或者vs平台运行后显示“未能加载文件或程序集“WebApi”或它的某一个依赖项。试图加载格式不正确的程序。”
一般情况下出现这样的问题是因为.dll文件不存在或者路径不正确. 但今天我遇到的情况都不在这两个内. 我确定.dll文件是存在的,路径也是正确的. 但是程序死活都是“未能加载文件或程序集“xxx”或它 ...
- CentOS7 安装rz和sz命令,安装netstat
yum install lrzsz CentOS7 安装netstat命令 yum install net-tools