java内存管理之垃圾回收及JVM调优

GC(garbage Collector 垃圾收集器)
作用:a、内存的动态分配;b、垃圾回收
注:Java所承诺的自动内存管理主要是针对对象内存的回收和对象内存的分配。

一、垃圾标记

  程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,而且会随着线程的创建而创建,线程的结束而销毁。那么,垃圾收集器在何时清扫这三块区域的问题就解决了。
  然而,堆和方法区中的内存清理工作就没那么容易了。 堆和方法区所有线程共享,并且都在JVM启动时创建,一直得运行到JVM停止时。因此它们没办法根据线程的创建而创建、线程的结束而释放。因此需要垃圾标记。

1)引用计数法

  每个对象都有一个计数器,当这个对象被一个变量或另一个对象引用一次,该计数器加一;若该引用失效则计数器减一。当计数器为0时,就认为该对象是无效对象。

2)可达性分析法(也叫根搜索算法)

所有和GC Roots直接或间接关联的对象都是有效对象,和GC Roots没有关联的对象就是无效对象。
GC Roots是指

  1. Java虚拟机栈所引用的对象(栈帧中局部变量表中引用类型的变量所引用的对象);
  2. 本地方法栈所引用的对象 ;
  3. 方法区中静态属性引用的对象;
  4. 方法区中常量所引用的对象(即常量池中的对象引用)

注意:GC Roots并不包括堆中对象所引用的对象!这样就不会出现循环引用

引用计数法虽然简单,但存在一个严重的问题,它无法解决循环引用的问题。 因此,目前主流语言均使用可达性分析方法来判断对象是否有效。

二、回收无效对象的过程

  Object类的finalize()方法,即所有类都有这个方法,如果子类重写(即覆盖)了该方法,就会在垃圾回收之前不是直接回收,而是去执行它。即当JVM筛选出失效的对象之后,并不是立即清除,而是再给对象一次重生的机会,具体过程如下:

  1,判断该对象是否覆盖了finalize()方法
  若已覆盖该方法,并该对象的finalize()方法还没有被执行过,那么就会将finalize()扔到F-Queue队列中;若未覆盖该方法,则释放对象内存。

  2,执行F-Queue队列中的finalize()方法
  虚拟机会以较低的优先级执行这些finalize()方法们,也不会确保所有的finalize()方法都会执行结束。如果finalize()方法中出现耗时操作,虚拟机就直接停止执行,将该对象清除
  3,对象重生或死亡
  如果在执行finalize()方法时,将this赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。

强烈不建议使用finalize()函数进行任何操作!如果需要释放资源,请使用try-finally。因为finalize()不确定性大,开销大,无法保证顺利执行。

三、垃圾回收算法

  现在我们知道了判定一个对象是无效对象、判定一个类是废弃类、判定一个常量是废弃常量的方法,也就是知道了垃圾收集器会清除哪些数据,那么接下来介绍如何清除这些数据。

分代收集算法

将内存划分为老年代和新生代。老年代中存放寿命较长的对象,新生代中存放“朝生夕死”的对象。然后在不同的区域使用不同的垃圾收集算法,如下:

1)标记-清除算法

首先利用刚才介绍的方法判断需要清除哪些数据,并给它们做上标记;然后清除被标记的数据。
分析:
这种算法标记和清除过程效率都很低,而且清除完后存在大量碎片空间,导致无法存储大对象,降低了空间利用率。

2.)复制算法(用于新生代)

将内存分成两份,只将数据存储在其中一块上。当需要回收垃圾时,也是首先标记出废弃的数据,然后将有用的数据复制到另一块内存上,最后将第一块内存全部清除。

分析:
这种算法避免了碎片空间,但内存被缩小了一半。
而且每次都需要将有用的数据全部复制到另一片内存上去,效率不高。

解决空间利用率问题:
  在新生代中,由于大量的对象都是“朝生夕死”,也就是一次垃圾收集后只有少量对象存活,因此我们可以将内存划分成三块:Eden、Survior1、Survior2,内存大小分别是8:1:1。分配内存时,只使用Eden和一块Survior1。当发现Eden+Survior1的内存即将满时,JVM会发起一次MinorGC,清除掉废弃的对象,并将所有存活下来的对象复制到另一块Survior2中。那么,接下来就使用Survior2+Eden进行内存分配。
  通过这种方式,只需要浪费10%的内存空间即可实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题。
  但是,当一个对象要申请内存空间时,发现Eden+Survior中剩下的空间无法放置该对象,此时需要进行Minor GC,如果MinorGC过后空闲出来的内存空间仍然无法放置该对象,那么此时就需要将对象转移到老年代中,这种方式叫做“分配担保”。

什么是分配担保?
  当JVM准备为一个对象分配内存空间时,发现此时Eden+Survior中空闲的区域无法装下该对象,那么就会触发MinorGC,对该区域的废弃对象进行回收。但如果MinorGC过后只有少量对象被回收,仍然无法装下新对象,那么此时需要将 Eden+Survior 中的所有对象都转移到老年代中,然后再将新对象存入Eden区。这个过程就是“分配担保”

3)标记-整理算法(用于老年代)

在回收垃圾前,首先将所有废弃的对象做上标记,然后将所有未被标记的对象移到一边,最后清空另一边区域即可

分析:
它是一种老年代的垃圾收集算法。老年代中的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,因此如果选用“复制”算法,每次需要复制大量存活的对象,会导致效率很低。而且,在新生代中使用“复制”算法,当Eden+Survior中都装不下某个对象时,可以使用老年代的内存进行“分配担保”,而如果在老年代使用该算法,那么在老年代中如果出现 Eden+Survior 装不下某个对象时,没有其他区域给他作分配担保。因此,老年代中一般使用“标记-整理”算法。

JVM调优

1,减少Full GC

因为Full GC要整理整个堆区,会很慢(几秒甚至数十秒),因此应该让尽量少的对象进入老年代。

1)确保对象都是“朝生夕死”的
一个对象使用完后应尽快让他失效,然后尽快在新生代中被Minor GC回收掉,尽量避免对象在新生代中停留太长时间。
2)提高大对象直接进入老年代的门槛
通过设置参数-XX:PretrnureSizeThreshold来提高大对象的门槛,尽量让对象都先进入新生代,然后尽快被Minor GC回收掉,而不要直接进入老年代。

2,当高并发时

应该适当增加S区的大小,因为发生YGC时,大量对象(高并发时)被引用而无法被GC掉,S区太小则只能到老年代;

当然增加s区自然会减小Eden区,从而增加YGC的频率,但是换来的却是高吞吐量。

 默认:

新生代占jvm内存 3/8;
新生代中Eden区:s区:s区=8:1:1