[置顶] JAVA 垃圾收集器与内存分配策略
2015-09-21 18:28
639 查看
引言
垃圾收集技术并不是Java语言首创的,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。垃圾收集技术需要考虑的三个问题是:哪些内存需要回收?
什么时候回收?
如何回收?
http://segmentfault.com/a/1190000002931555 中讲到java内存运行时区域的分布,其中程序计数器,虚拟机栈,本地方法区都是随着线程而生,随线程而灭,所以这几个区域就不需要过多考虑回收问题。但是堆和方法区就不一样了,只有在程序运行期间我们才知道会创建哪些对象,这部分内存的分配和回收都是动态的。垃圾收集器所关注的就是这部分内存。
一 对象死亡判据
垃圾收集器在对一个对象回收之前,首先要判断对象在程序中是否还有使用的可能性,充要条件就是没有被程序可访问的引用再指向这个对象实例。最简单的办法就是给对象实例添加中添加一个引用计数器,每当有一个引用指向它时,计数器就加一,当引用失效时,计数器就减一,如果计数器值为0则说明没有引用指向它,可以进行回收。但是这个方法中计数器为0并不是一个必要条件,例如,生成两个对象实例,每个对象实例的属性都指向对方,那么这个两个对象实例分别最少有一个引用。java采用的是
可达性分析算法,即找一部分对象作为
"GC Roots"节点,从这些节点开始向下搜索,当某个对象到"GC Roots"节点
没有可达路径时,说明此对象是不可用的。在java中作为"GC Roots"的节点包括:
虚拟机栈中引用的对象,
方法区静态属性引用的对象,
方法区常量引用的对象,
本地方法区中本地调用所引用的对象。
引用扩充
如果reference类型的数据中存储的数值是另一块内存的起始地址,那么这块内存就代表着一个引用。一个对象在这种状态下,只能有被引用和没有被引用两种状态。java对引用概念进行了扩充,将引用分为强引用(new),
软引用(softReference),
弱引用(WeakReference),
虚引用(PhantomReference)。如果强引用存在,则垃圾收集器不会回收该对象。如果系统即将发生内存溢出异常,那么垃圾回收集器则会回收软引用对象。弱引用对象只能存活到下一次垃圾收集之前。虚引用对象不会对其生存时间构成任何影响。
对象的自我救赎
在垃圾收集器发现某一个对象到"GC Roots"路径不可达时,先会判断该对象是否覆盖finalize()方法,或是否执行过finalize()方法。如果覆盖了且没有执行过该方法,则会将该对象放到低优先级的Finalizer线程队列中去执行finalize()方法,如果在finalize()方法中该对象又被引用,则会有一次逃脱被回收的命运。
方法区的回收
方法区中主要回收废弃的常量和无用的类。对于常量,如果没有引用指向常量,则该常量会被回收。对于类的回收则麻烦许多,首先要判断该类是无用的类,
无用的类要满足三个条件:
所有类的实例被回收。
加载该类的ClassLoader已经被回收。
Class没有被引用,不会通过反射访问该类的方法。
二 垃圾回收算法
标记-清除算法(Mark-Sweep)
该算法分为两个阶段:首先标记处要回收的对象,标记完成后统一回收所有被标记的对象。存在的问题:
标记和清除效率都不高
标记清除后会产生大量内存碎片,分配大对象时可能触发另一次垃圾收集。
复制算法(Copying)
该算法将内存分为两个等大小的区域,每次只使用一个区域。当一个区域快用完了,就将这个区域中存活的对象复制到另一个区域。优点是避免了内存碎片的产生,缺点是浪费内存空间。
有公司研究表明,新生代的对象98%都是朝生暮死,所以虚拟机把新生代内存划分为一个较大的Eden空间和两个较小的Survivor空间。每次只是用Eden空间和一个Survior空间,当进行复制清理时,将Survivor空间和Eden空间中存活的对象复制到另一块Survivor空间。当Survivor空间不够用时,就会依赖老年代进行分配担保。
标记-整理算法(Mark-Compact)
针对老年代对象存活率高的情况,复制算法明显不合适,于是采用标记整理算法,标记和标记清除算法相同,二后边的整理则是让所有存活的对象都向一端移动,然后清理掉边界外的内存。
分代收集
当前虚拟机都采用分代收集,分代的依据是对象的存活周期。一般新生代存活率低,采用复制算法。老年代存活率高采用
标记整理或
标记清除。
一般来讲,新生代空间会小很多,具体比例一般要看应用场景。而默认的大小一般是老年代的1/4到1/3。
三 垃圾收集器
由于虚拟机采用了分代收集,所以针对不同代收集器也不同。上图是HotSpot虚拟机的垃圾收集器,连线表示可以协同工作。
Serial收集器,复制算法,它是一个单线程的收集器,并且在进行收集时会暂停其他线程,它默认是client模式下的新生代收集器。
ParNew收集器是Serial收集器的多线程版,它是第一款并发收集器。
Parallel Scavenge收集器可以精确控制吞吐量(用户代码运行时间/(用户代码时间+垃圾收集时间))
SerialOld收集器是serial收集器的老年版,采用标记整理算法,同样是单线程收集器。
ParallelOld是ParallelScavenge收集器的老年版,使用多线程和标记整理算法。
CMS收集器是以最短回收停顿时间为目标的收集器,采用标记清除算法,在重视响应速度的系统中得以应用。但是缺点是对CPU资源敏感,无法处理浮动垃圾,易产生内存碎片。
G1收集器是最新推出的收集器,可应用在JDK1.7u4及以上版本。它将内存分为多个Region,新生代和老年代分别包含多个Region。G1跟踪各个Region,判断垃圾价值大小,优先回收价值最大的Region。
安全点
安全点的概念是指当进行GC时,应当让工作线程停止,这时会更容易对对象是否存活进行判断。而停止线程应当在安全的时刻,所以会有安全点的概念。暂停线程有两种方式,第一种是强制暂停,如果某些线程没有到达安全点则再让他运行到安全点,这叫做
抢先式中断。第二种是在编译过程中在安全点加入一个条件判断判断0x160100内存页是否可读,如果不可读则会暂停,这叫做
主动式中断。
四 内存分配与回收策略
对象的分配,就是在堆上分配,对象主要分配在新生代的Eden区域中,如果启动了本地线程分配缓冲,则按线程优先在TLAB中分配。少数情况也有可能直接分配到老年代。
对象在Eden区域分配时,当Eden区域没有足够空间,虚拟机会发起一次新生代垃圾收集。
如果对象需要大量连续内存空间,例如String类型和数组。大对象对于虚拟机内存分配来说是一个坏消息,朝生暮死的大对象是要命的坏消息。经常出现大对象会导致多次出发垃圾收集。对于这类对象,可以设置参数将大对象直接存入老年代。
每一个对象都有一个年龄计数器,当对象在Eden区域出生,每经过一次GC,并且存入Survivor,计数器加一。当年龄增加到一定程度(默认15),则会被存入老年代。同时,如果Survivor空间中相同年龄对象占空间超过50%,则也会直接进入老年代。
总结
垃圾收集算法:复制算法,
标记-清除算法,
标记-清理算法。
垃圾收集器特点:新生代用复制,老年代用标记清理,CMS用标记清除。
Eden空间大小和Survivor空间大小默认比率为8:1,即新生代10%的空间用来存放复制后的对象。
相关文章推荐
- 成功搭建CXFWebService服务端(集成spring)
- Java垃圾回收机制2
- eclipse maven plugin 插件 安装 和 配置
- Java垃圾回收机制1
- 第十二章:File类和流
- Java的String为什么要设计成final
- Java虚拟机1:什么是Java
- java学习笔记之HashMap经典分拣存储
- spring AOP记录日志
- java的报表下载代码excel
- 用Java实现 atoi 和 itoa
- jdk7 遍历文件树
- java中数组(Array)与列表(List)相互转换的方法
- myeclipse10破解 for ubuntu
- struts中的常用配置
- Java并发编程:线程池的使用
- org.springframework.dao.CannotAcquireLockException解决
- [leetcode]Intersection of Two Linked Lists[java]找出两个链表的公共节点
- spring手动回滚事务
- JAVA远程方法-RMI