声明: 以下内容均属于抽象知识!!! 没有与实践相结合!!!. 均停留在概念层!!!. 参考来源网络和书本!!! 笔者没有亲自探查过源码,目前也没有能力将这些抽象的知识直观的表达出来. 如果以后有能力对这里进行修改,应该会更新.... 请酌情食用...
1. 内存简介
垃圾回收是将被垃圾占用的空间释放掉,这个空间就是内存.可以防止内存泄漏.
jvm运行时的内存管理分为两部分
- 程序计数器,虚拟机栈,本地方法栈:具有隔离性,随着线程而创建和销毁.
一个线程创建完分配入内存,线程结束后销毁内存,这些是确定的,一般不主动进行垃圾回收. - 堆区,方法区:随着对象的创建,实例化,而分配内存.是动态的.
堆和方法区是公共的区域,变动较大,也是我们垃圾回收的主要目标.
如果想要了解一下的话,可以参考文末的内存管理五部分简介.
2. 如何判断对象是否存活
知道我们的垃圾回收的主要目标后,我们就要判断到底那些对象能够清除,那些对象不能清除.
堆中和方法区中的对象因为有所不同,因此我们所要考虑的方法也不同.
我们通常有两种方法.
- 引用计数法.
- 可达分析法.
2.1 回收区域
回收堆和回收方法区有所不同,主要原因是存放的对象不同:
- 堆中存放的是实例化的对象,我们可以显式的直接看到这个对象有没有用了.
- 方法区中存放的是常量,构造函数等.因此我们判断的时候需要多注意.
方法区中如何判断一个类是"无用类".
- 该类的所有实例都已被回收.
- 该类的类加载器(
ClassLoader
)已被回收. - 该类对应的Class对象没有任何其他地方被引用,没有在任何地方通过反射访问该类的方法.
2.2 判断方法简介
2.2.1 引用计数法
引用计数法:给对象增加一个引用计数器,每当一个地方引用它时,计数器就+1.当引用失效时,计数器-1.当计数器为0时对象被回收.
但是存在一个循环依赖问题(A,B互相依赖),因此不被主流JVM引用.
2.2.2 可达分析法
通过一系列GC Roots
的对象作为起点开始搜索,所有没有被搜到的对象说明不可用,需要被回收.
可以理解成连通图,从若干个节点出发,所有联通的节点都是我们需要的,其他的都要扔掉.(图论选手欢喜:让我Trajan跑一波强连通!)
可作为GC Roots
的对象主要有:
- 虚拟机栈中的引用对象.
- 方法区中的静态属性引用对象.
- 方法区中的常量引用对象.
- 本地方法栈中的引用对象.
这其中还分为不同的销毁级别.比如,当系统内存充足时,什么都可以存在.但是当执行完垃圾回收后还不够时,就需要根据引用级别进行回收了.
引用级别分为四种(依次递减):
- 强引用:程序代码中普遍存在的:类似
Object o = new Object()
.这样的,永远不会回收. - 软引用:描述还有用但是不是必须的对象.回收发生在内存溢出(
out of memory
)之前,如果执行完软引用清除之后还会内存溢出,就会报错out of memory
.
通过SoftReference类
来实现软引用. - 弱引用:描述非必需对象.垃圾回收器主动回收.
通过WeakReference类
来实现弱引用. - 虚引用:没什么用,无法通过虚引用获得对象实例.唯一作用就是能够让该对象被垃圾回收器回收时收到一个系统通知.
通过PhantomReference类
来实现虚引用.
当然,这只是一次死亡宣告,(被告知:即将被销毁).
如果该对象没有重写finalize()
方法,或者已经执行过finalize()
方法,该对象就直接被回收,但是如果重写了finalize()
方法并且还没有被执行过.就会进入F-Query
队列中,并由JVM虚拟机自行建立的低优先级的线程finalizer
来执行.在这个执行过程中,只要和引用链上的对象建立联系,就不会被销毁,否则就会被销毁.
3. 垃圾回收算法
我们取得了可以回收的对象后,就可以执行垃圾回收算法了.
我们首先有一个理论:即分代收集理论,
该理论描述的就是一个符合大多数程序运行实际情况的经验法则.建立在分代假说之上,
- 弱分代假说(新生代):绝大多数对象都是朝生夕灭的.
- 强分代假说(老年代):熬过越多次垃圾收集过程的对象就越难被消灭.
- 跨代引用假说(非常少)
在这个分代假说之上,我们得出了很多常用的垃圾收集器的设计原则:
- 垃圾收集器应将Java堆划分成不同的区域,然后来针对性的回收.
主流的垃圾回收算法有三个
- 标记-清除算法.(基础)
- 复制算法(新生代回收算法).
- 标记整理算法(老年代回收算法).
3.1 标记-清除算法
"标记-清除算法"是最基础的收集算法,其他的收集算法都是在该算法的基础上进行优化的.
算法分为标记和清除两个阶段:
- 标记出所有需要回收的对象.参考上文的判断对象是否存活方法.
- 标记完成后统一进行回收.
清理流程如下所示:图片引用自<深入理解Java虚拟机第三版>侵删
但是由于我们标记的对象在内存中可能是散乱着分布的,也因此可能会产生大量的内存碎片,对以后的大内存分配造成影响触发更多的垃圾回收机制.影响效率.因此我们通常不采用这种方法.
3.2 复制算法(新生代回收算法)
为了解决"标记清除"的效率问题.
我们可以将内存按容量划分为大小相等的两块,每次只使用其中的一块.当这块内存需要进行垃圾回收时,将此区域的所有能活着的对象全都复制到另一块上,然后将这一块全部清除.
这样的话我们每次只用对半个区域的内存进行清除,而且也不需要考虑内存碎片.只需要移动堆顶指针,按顺序分配即可.算法简单高效.
清理流程如下所示:图片引用自<深入理解Java虚拟机第三版>侵删
现在很多的虚拟机都采用这种收集算法来回收新生代.
比如著名的HotSpot
虚拟机
3.3 标记整理算法(老年代回收算法)
对于存活率较高的老年区而言,复制算法可能就会效率较低(要复制的东西太多了!),因此我们需要一种更加适用于老年代的算法-“标记整理算法”.
首先进行标记,仍是一样的.
区别在于整理,对于整理时,我们先让所有存活的对象向一段移动.然后直接清理掉边界以外的所有内存即可.
清理流程如下所示:图片引用自<深入理解Java虚拟机第三版>侵删
注意一点:如果要对老年代这样每次回收都有很多对象存活的区域进行整理,那么消耗是比较大的.甚至需要用户应用程序全部暂停才可以.而3.1介绍的"标记-清除算法"就不会这样,只不过会产生过多的内存碎片,这就需要我们进行权衡了.
通过更加复杂的内存分配访问器来解决?比如"分区空闲分配链表"一样的东西?能够在碎片化的磁盘上进行读写.但是内存访问也是用户程序最为频繁的操作.复杂的内存访问机制也会影响程序的吞吐量.
所以我们就需要在这之间进行权衡了.
- 关注吞吐量的
HotSpot
虚拟机用的是"标记-整理算法". - 关注延迟的
CMS
虚拟机用的就是"标记-清除算法". - 还有一种结合起来,先使用"标记-清除算法",然后在碎片太多的时候使用"标记-整理算法"收集.
3.4 补充
我们可以根据上面的算法的优劣来对不同的区域进行不同的算法
部分收集(Partial GC
):目标不是完整收集整个Java堆的垃圾收集.
- 新生代GC(
Minor GC/Young GC
):指目标只是新生代的垃圾收集 - 老年代GC(
Major GC/Old GC
):目标只是老年代的垃圾收集. - 混合收集(
Mixed GC
):指收集全部新生代和部分老年代.(目前只有G1收集器).
整堆收集(Full GC
):收集整个Java堆和方法区的垃圾收集.
4. 内存管理的简介
java内存分为两大部分:
- 程序计数器,虚拟机栈,本地方法栈:具有隔离性,随着线程而创建和销毁.
一个线程创建完分配入内存,线程结束后销毁内存,这些是确定的,一般不主动进行垃圾回收. - 堆区,方法区:随着对象的创建,实例化,而分配内存.是动态的.
堆和方法区是公共的区域,变动较大,也是我们垃圾回收的主要目标.
4.1 程序计数器
作用
是一块较小的内存空间.可以看作是当前线程所执行的字节码的行号指示器.
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程回复等基础功能的.
特点
- 线程私有:Java虚拟机的多线程进行切换的时候并不会记录线程执行的位置.所以需要每个线程各自独立的一个程序计数器.
- JVM中唯一没规定
OutOfMemoryError
的区域:由于程序计数器存除的是字节码文件的行号(可以理解成指针),这个范围是可知晓的,因此一开始分配内存时就可以分配一个绝对不会溢出的内存. - 如果正在执行的是
Native
方法,则这个计数器的值为空:Native
大多是通过C
来实现的,所以不需要存除字节码文件的行号.
如何保证执行native
方法后(或者方法中断后)仍能继续运行?
这里的"pc寄存器"是指抽象在JVM层上的概念.当执行Java方法时,这个"pc寄存器"有两种形式:
butecode index
:相对于该方法字节码开始处的偏移量.bytecode pointer
:该Java字节码指令在内存里的地址.
我的理解:在JVM中还有虚拟机栈和本地方法栈.native
保存在本地方法栈中,执行完毕后,通过返回值来确定下一步的指令.
4.2 虚拟机栈和本地方法栈
作用
- 虚拟机栈是用于描述Java方法执行的内存模型.服务对象为Java方法.
- 本地方法栈用于存放
native
的内存模型.服务对象为native
方法.
虚拟机栈
其中的存储单元为栈帧:
- 栈帧:存在于虚拟机栈中,每个线程调用java方法时都会产生一个栈帧,结束时销毁.
- 栈帧用来记录一些信息(局部变量表,方法返回地址等…)
本地方法栈
其中的主要包含本地变量表和操作数表.
- 操作数表由若干个
Enty
组成.可以存储java虚拟机中定义的任意数据类型的值.
异常
都会碰.碰到两种异常:
- 存在栈容量限制,超出来会报
StackOverflowError
异常. - 可以动态扩展,但是如果没有足够的内存时,就会报
OutOfMemoryError
异常.
特点
- 具有线程隔离特性(线程私有),参考程序计数器.
- 用于存放java方法的内存模型.
4.3 堆
每个Java引用都对应一个JVM实例,每个实例都唯一对应一个堆.
应用程序在运行中所创建的类实例或者数组都存放在堆中,并由所有的线程共享.
Java中的堆内存分配是自动的.我们常见的垃圾回收也是回收的堆内存.
4.4 方法区
JVM有一个被所有线程共享的方法区.类似编译后的代码存储区.
方法区不是堆.
存储每个类的结构:比如常量池,构造函数等.
Java中的变量分为:静态变量,实例变量,临时变量.
- 静态变量:位于方法区.
- 实例变量:位于堆中.
- 临时变量:在栈中,随着线程而创建和销毁.
参考资料
简书-JVM垃圾回收机制:https://www.jianshu.com/p/23f8249886c6
知乎回答native
的线程问题:https://www.zhihu.com/question/40598119/answer/87381512
简书-Java虚拟机栈(三):https://www.jianshu.com/p/ecfcc9fb1de7
CSDN-深入理解JVM的垃圾回收机制:https://blog.csdn.net/yubujian_l/article/details/80804708
深入理解Java虚拟机-JVM高级特性与最佳实践(第三版)-周志明