程序计数器、虚拟机栈、本地方法栈随着线程而生,也随着线程而灭;栈帧随着方法的开始而入栈,随着方法的结束就出栈。这几个区域的内存分配和回收都有着确定性。
而对于Java堆和方法区,我们只有运行的时候知道会创建哪个内存,这个内存的创建和回收都是动态的,垃圾收集器所关注正是这部分内存。
判定对象是否存过
如果一个对象不被任何对象或者变量引用,那么它就是无效的对象,需要被回收。
引用计数器法
在对象头上维护一个counter计数器,被对象引用则计数器+1,若引用失效则计数器-1。当计数器为0的时候就认为该对象无效了。但是如果出现了循环引用的情况,循环的对象的计数器不可能为0。
可达性分析
所有的GC Roots直接或者间接关联有效对象的,和GC Roots 没有关联的对象被回收
CG Roots 指的是
- java虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中常量引用对象
- 方法区中类静态属性引用的对象
CG Roots 不包括堆中对象所引用的对象,这样就不会出现循环问题
引用的种类
强引用
类似“Object a = new Object()” 这类的引用,就是强引用,只要存在强引用,垃圾回收期就不会回收强引用,如果错误的给了强应用,对象就会存在很长一段时间内不会被回收,会产生内存泄露
软引用
软引用通常是来实现内存敏感的缓存,当jvm认为内存不足的时候吗,才会试图回收软引用所指向的对象
弱引用
弱引用的强度比软引用更弱一些,当jvm进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象
虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,它仅仅是提供了一种确保对象被finalize之后,做某些事情的机制。
回收堆中的无用内存
对于可达性分析不可达的内存,也并不是没有存活的可能。需要判断对象的finalize()是否被执行过,如果没有执行,就会被jvm放到一个队列中执行,如果出现了耗时操作,则直接清除对象。如果执行成功,讲this赋给某一个引用,那么改对象就重生了,如果没有,那么就会被垃圾收集器清除
回收方法区内存
方法区中存放生命周期比较长的类信息、常量、静态变量每次垃圾收集只有少量的垃圾被清除。方法区中主要清除两种垃圾:
- 无用的类
- 废弃常量
判定废弃常量
只要常量池中的常量不被任何变量或者对象引用,那么这些常量就会被清除掉
判定无用类
判定一个类是否是无用类,条件比较苛刻
- 该类的所有对象都已经被清除
- 加载该类的ClassLoader已经被回收
- 该类的 java.lang.Class对象没有在任何地方被引用,无法通过反射来访问该类的方法
垃圾收集算法
标记-清除算法
遍历所有的GC Roots,然后将所有的GC Roots对象标记为活对象。
清除过程中将遍历堆中的所有对象,将没有被标记的对象被清除掉。与此同时,清除那些被标记过的对象的标记,以便下次垃圾回收
这种方法有两个不足的地方
- 效率问题: 标记和清除两个过程的效率都不高
- 空间问题:标记清除之后会出现大量的空间碎片,碎片太多可能导致以后分配较大的对象,无法找到足够的连续内存,而不得不提前触发下一次垃圾回收操作
复制算法(新生代)
为了解决效率问题,将可用内存按照容量分为大小相等的两份,每次只用其中一份,当这块内存用完,需要进行垃圾收集时,就将活着的对象复制到另外一块上面,然后将第一块内存全部清掉。
- 优点:不会有内存碎片
- 缺点:内存缩小为原来的一半,浪费空间
为了解决空间问题,可以将内存分为三个部分:Eden、FromSurvivor、ToSurvivor,比例是8:1:1,每次使用Eden和其中一块内存,回收时,讲Eden和Survivor中活着的对象的那部分一起复制到另外一个Survivor中,最后清理带哦Eden和Survivor的空间。这两只有十分之一的内存被浪费,但是如果如果活着的内存大于10%的内存空间,就需要其他内存用来担保分配
标记-整理算法(老年代)
- 标记:同标记清除算
- 整理:移动所有存活的对象,按照内存地址次序排序,然后将内存地址末端以后的内存全部回收
这是一种老年代的算法,老年代相对比较大,如果复制就会有效率影响
分代收集算法
- 新生代 复制算法
- 老年代 标记清除算法