GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。
JVM
JVM可以分为5大类
- 方法区(永久代)
- 堆内存
- 栈内存
- 虚拟机栈
- 程序计数器
其中方法区和堆内存是线程共享的,本地方法栈,虚拟机栈,程序计数器是独占的。
堆内存可以分成
年轻代:主要存放新建对象,垃圾回收会比较频繁。创建对象在堆是,将进入年轻代的Eden(最常见的GC处理部分)
幸存区Space:垃圾回收器进行垃圾回收时,扫描Eden Space ,如果对象仍然存活,则复制到Suvivor Space
年老代Tenured:主要存放JVM认为生命周期较长的对象,或是内存占用较大的对象。在扫描Suvivor Space时,对象经过多次扫描仍然存在,将其转化为持久代
持久代:主要存放类定义,字节码和常量等很少更变的信息,永久代在逻辑上是和堆分开的,但是物理上属于堆。
1.8之后持久代更改为元空间,不在虚拟内存JVM中,而是设置在在本地内存中
年轻代大小默认比例是 eden:from survivor:to survivor=8:1:1
GC算法的基础
1.引用计数法:无法结局循环引用的问题,不被java采纳
循环引用:A引用B,B应用C,C应用A。此时无法认为ABC是无效数据,无法进行回收
2.跟搜索算法,从根节点扫描,只要这个对象在引用链中,那就是可触及的有效数据
现在虚拟机中的垃圾搜索算法:
标记清除,
复制算法(新生代)
标记压缩(老年代)
这三种算法都扩充了跟搜索算法
标记清除算法:
标记清除法是现代垃圾回收算法的思想基础,标记清楚将垃圾回收分为两个阶段:标记阶段和清除阶段
标记阶段:首先通过根节点开始的可达对象,因此被标记的对象就是未被应用的垃圾对象
清楚阶段,清除所有未被标记的对象
实践:
在队中有效空间被耗尽是,就会停止整个程序,然后进行标记删除
标记,遍历所有的GC Root,然后将所有的GC Roots可达的对象标记为存活的对象
清除:清除的过程将遍历堆中的所有对象,将没有标记的对象清除
缺点:时间长(递归和权堆遍历,导致stop the world的时间很长)
不能解决内存碎片的问题
复制算法:
将原先的内存分为两块,每次只使用一块,垃圾回收时,将对象复制到未使用的内存块中,之后清楚正在使用的内存块的所有对象,交换两个内存的角色,垃圾回收完成
不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC),标记算法相比解决了内存碎片的问题
复制算法的缺点:
空间浪费,复制算法每次只对半区内存进行回收,复制算法想使用,最起码对象的存活率要很低才行,浪费了一半的空间
如果应对所有对象都100%存活的极端情况,就必须有很大的额外空间做担保,所以这种算法不适合很多的对象存活率都很高的时候(老年代)
新生区:eden区,surivor区(From区和to区)
将一块大的区域(8:1)划分为Eden区,剩下的部分划分为from区,to区。
每次GC使用Eden区和form区。先将年龄较大的对象,占用较大的内存直接放入老年代。
然后将Eden区和From区存活的对象复制进入to区,之后清除Eden区,From区。
被清空的From区变成To区等待下次被复制。全新的Eden区继续存储新的对象,
3.标记-整理算法
在标记-清除的基础上做了一些优化,和标记-清除算法应用,首先需要从根节点开始,对可标记的对象进行标记,之后将存活的对象压缩到内存一端,之后开始清理外面的空间
实践:
标记:遍历所有的GC Roots,然后将所有的GC Roots对象标记为存活的对象
整理:移动所有存活的对象,按照内存地址依次排列,然后将末端内存地址以后的内存全部回收
标记-压缩算法不仅可以弥补标记清除算法的内存碎片问题,也清除了复制算法中,额外内存空间的问题
缺点:效率很低,不仅奥标记所有存活的对象,还要整理存活对象的引用地址。效率是上面两个算法最低的
小结
效率:复制算法>标记-清除算法>标记-整理算法
分代收集算法
概念:
根据对象存活周期的不同,将堆内存划分为老年代和新生代。存活时间短的对象为新生代,寻获时间长的对象为老年代
事件
少量对象存活,适合复制算法,在新生代中,每次GC都会有大量对象被回收,那么就使用复制算法
大量对象存活,适合标记清理,标记压缩算法,在老年代中,对象存活率高,就用标记清理/压缩算法进行GC
Stop The_World(Stop:咋瓦鲁多)
java中以这种全局暂停的现象,全局提那个盾,所有代码停止,native可以执行,但不能和JVM交互,多半情况下是GC引起的
危害
长时间停止服务,无响应
解决方法:一般情况下,会有多台服务器互相形成主机与备机,以保证一台服务器进行GC停顿之后,有备份的服务器作为代替。但是主机不工作只是暂时的,当GC结束之后,主机又开始工作了,那么这样的话,主机和备机就同时工作了主机和备机同时工作其实是非常危险的,很有可能会导致应用程序不一致、不能提供正常的服务等,进而影响生产环境。
为什么会有全局停顿(Stop-the_world)?
避免无法彻底清除感觉
GC的工作必须在一个能确保一致性的快照中
JVM调优
1.首先打印GC信息
-XX:+PrintGC
2.根据不同场景设置GC的类型
web接口交互的系统:使用ParNew+Cms组合,可以提高响应时间
服务器的系统:使用parallel scavenge +parallel old可以提高吞吐量
3.设置堆大小
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize/XX:MaxNewSize-设置新生代的大小/最大大小
-Xmn:年轻代的大小
-XX:SurvivoRatio=n;设置年轻代和老年代的比值
什么时候会触发GC
新生代生成,Eden申请空间失败,就会触发Mubor GC
年老代(Tenured)被写满
持久代(Perm)被写满
System.gc()被显示调用。Runtime.getRuntime().gc()
上一次GC之后Heap的各区域分配策略动态变化
GC的类型
1.年轻代垃圾回收器
1)Serial
单线程GC,在垃圾回收时,必须暂停其他所有线程的工作,直至它收集结束
2.ParNew:
就是Serial的多线程版本,除了使用多条检查进行垃圾收集之外,其余都和Serial完全一样。老年代使用CMS时,新生代会默认使用ParNew。也只有ParNew可以和CMS配合使用
3.Paraller Scavebge
并行多线程收集器,server模式下默认GC的方式,停顿时间短,良好的响应速度可以提升用户体验,有自适应调节策略
2.老年代垃圾回收器
1.Serial Old
是Serial的老年代版本,同样是一个单线程收集器,每次进行回收都很耗时
2.Parallel Old:
Parallel Scavenge收集器的老年代版本,多线程
3.CMS:
一种获得最短停顿时间为目标的GC,目前重视响应速度的服务端上,
使用XX:+UseConcMarkSweepGC指定使用
优点:响应快速
缺点,堆CPU资源很铭感,会导致应用变慢,吞吐量降低
4 G1:
并行与并发:充分利用多CPU的优势,使用多个CPU缩短Stop-the-world的时间。通过并发的形式让java程序继续执行
分代收集:既可以收集新生代也可以收集老年代
空间整合:采用标记-整理算法
可预测停顿:可以建立可预测的停顿时间模型,从而到达降低停顿时间的要求。