www.2527.com_澳门新葡8455手机版_新京葡娱乐场网址_
做最好的网站

垃圾堆搜集器与内部存储器分配政策,垃圾回收

2019-10-06 04:11 来源:未知
  • 这篇文章主要讲垃圾收集器,下一篇文章再讲内存分配策略。

程序计数器、虚拟机栈、本地方法栈都是线程私有的,会随着线程而生,随线程而灭
栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈操作.

转载请注明出处: jiq•钦's technical Blog - 季义钦

静态内存分配和回收

  • 静态内存分配是指在程序开始运行时由编译器分配的内存,在被编译时就已经能够确定需要的空间,当程序被加载时系统把内存一次性分配给它,这些内存不会在程序执行时发生变化,直到程序执行结束时才回收内存.

  • 包括原生数据类型及对象的引用

  • 这些静态内存空间在栈上分配的,方法运行结束,对应的栈帧撤销,内存空间被回收.

  • 每个栈帧中的本地变量表都是在类被加载的时候就确定的,每一个栈帧中分配多少内存基本上是在类结构确定时就已知了,因此这几块区域内存分配和回收都具备确定性,就不需要过多考虑回收问题了.

引言:

  • 这三个区域随线程而生、随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊的进行着出栈、入栈操作。每一栈帧中分配的内存基本上在类结构确定下来是就已知的(尽管在运行期会由JIT编译器进行一些优化,但大体上是编译期可知)。因此这三个区域的内存分配和回收都具备确定性,在这几个区域就不过多考虑回收问题,因为方法结束或线程结束时,内存自然就跟着回收了。本文重点讨论堆和方法 区。

动态内存分配和回收

  • 在程序执行时才知道要分配的存储空间大小,对象何时被回收也是不确定的,只有等到该对象不再使用才会被回收.

堆和方法区的内存回收具有不确定性,因此垃圾收集器在回收堆和方法区内存的时候花了一点心思.

我们都知道JVM内存由几个部分组成:堆、方法区、栈、程序计数器、本地方法栈

1 Java堆内存的回收

JVM垃圾回收仅针对公共内存区域即:堆和方法区进行,因为只有这两个区域在运行时才能知道需要创建哪些对象,其内存分配和回收都是动态的。

  • 这部分内存的分配和回收是动态的,因为一个接口的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序运行期间才能知道会创建哪些对象。这部分的内存分配和回收是本文要讨论的。

1.1 判定回收的对象

在对堆进行对象回收之前,首先要判断哪些是无效对象即一个对象不被任何对象或变量引用,需要被回收.一般有两种判别方式:

  • 引用计数法 (Reference Counting)
    每个对象都有一个整型的计数器,当这个对象被一个变量或对象引用时,该计数器加一;当该引用失效时,计数器值减一.当计数器为0时,就认为该对象是无效对象.
  • 可达性分析法 (Reachability Analysis)
    所有和GC Roots直接或间接关联的对象都是有效对象,和GC Roots没有关联的对象就是无效对象.
![](https://upload-images.jianshu.io/upload_images/1801191-72215219d013bf70.png)

这里写图片描述



GC Roots对象包括:
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈JNI(即所谓的Native方法)引用的对象

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

  • 两者对比
    引用计数法虽然简单,但存在无法解决对象之间相互循环引用的严重问题,且伴随加减法操作的性能影响.

因此,目前主流语言均使用可达性分析方法来判断对象是否有效.

本文主要讨论两点,一是垃圾回收策略,二是调优的方法。

2 回收无效对象的过程

当经可达性算法筛选出失效的对象之后,并不是立即清除,而是再给对象一次重生的机会,具体过程如下:

  • 判断是否覆盖finalize()
    • 未覆盖该或已调用过该方法,直接释放对象内存
    • 已覆盖该方法且还未被执行,则将finalize()扔到F-Queue队列中
  • 执行F-Queue中的finalize()
    虚拟机会以较低的优先级执行这些finalize(),不会确保所有的finalize()都会执行结束.
    如果finalize()中出现耗时操作,虚拟机就直接停止执行,将该对象清除
  • 对象重生或死亡
    • 如果在执行finalize()方法时,将this赋给了某一个引用,则该对象重生
    • 如果没有,那么就会被垃圾收集器清除

注意:强烈不建议使用finalize()进行任何操作!
如果需要释放资源,请用try-finally或者其他方式都能做得更好.
因为finalize()不确定性大,开销大,无法保证各个对象的调用顺序.

以下代码示例看到:一个对象的finalize被执行,但依然可以存活

/**
 * 演示两点:
 * 1.对象可以在被GC时自救
 * 2.这种自救机会只有一次,因为一个对象的finalize()最多只能被系统自动调用一次,因此第二次自救失败
 * @author sss
 * @since 17-9-17 下午12:02
 *
 */
public class FinalizeEscapeGC {

    private static FinalizeEscapeGC SAVE_HOOK = null;

    private void isAlive() {
        System.out.println("yes,I am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize methodd executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }


    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();

        // 对象第一次成功自救
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize方法优先级很低,所以暂停0.5s以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,I am dead :(");
        }

        // 自救失败
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,I am dead :(");
        }
    }
}

运行结果

finalize methodd executed!
yes,I am still alive :)
no,I am dead :(

一、垃圾回收机制

  • java中垃圾收集器完成的事
  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?
  • 第2节内容将围绕这3个问题进行叙述。注:这一节所讲内容是针对JVM中的堆内存,第3节讲方法区内存回收

3 方法区的内存回收

如果使用复制算法实现堆的内存回收,堆就会被分为新生代和老年代

  • 新生代中的对象"朝生夕死",每次垃圾回收都会清除掉大量对象
  • 老年代中的对象生命较长,每次垃圾回收只有少量的对象被清除

由于方法区中存放生命周期较长的类信息、常量、静态变量.
因此方法区就像堆的老年代,每次GC只有少量垃圾被清除.

方法区中主要清除两种垃圾

  • 废弃常量
  • 无用类

1.1 分代管理

将堆和方法区按照对象不同年龄进行分代:

u  堆中会频繁创建对象,基于一种分代的思想,按照对象存活时间将堆划分为新生代和旧生代两部分,我们不能一次垃圾回收新生代存活的对象就放入旧生代,而是要经过几次GC后还存活的对象,我们才放入旧生代,所以我们又把新生代再次划分为Eden区和两个Survivor区,让对象创建在Eden区,然后在两个Survivor之间反复复制,最后仍然存活的对象才复制到旧生代中。

u  方法区存放的是常量、加载的字节码文件信息等,信息相对稳定。因为不会频繁创建对象,所以不需要分代,直接GC即可。

 

由此我们JVM垃圾回收要扫描的范围是:

图片 1

注:图片来自网络

 

新生代:

1.      所有新对象创建发生在Eden区,Eden区满后触发新生代上的minor GC,将Eden区和非空闲Survivor区存活对象复制到另一个空闲的Survivor区中。

2.      永远保证一个Survivor是空的,新生代minor GC就是在两个Survivor区之间相互复制存活对象,直到Survivor区满为止。

 

旧生代:

1.      Eden区满后触发minor GC将存活对象复制到Survivor区,Survivor区满后触发minor GC将存活对象复制到旧生代。

2.      经过新生代的两个Survivor之间多次复制,仍然存活下来的对象就是年龄相对比较老的,就可以放入到旧生代了,随着时间推移,如果旧生代也满了,将触发Full GC,针对整个堆(包括新生代、旧生代和持久代)进行垃圾回收。

 

持久代:

持久代如果满,将触发Full GC

3.1 回收废弃常量

回收废弃常量和回收对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除.

1.2 垃圾回收

要执行gc关键在于两点,一是检测出垃圾对象,二是释放垃圾对象所占用的空间。

  • 在堆中存放着Java中几乎所有对象实例(有的存放在方法区中),垃圾收集器在对堆进行回收前,第一件事就是确定这些对象之中哪些还“存活”着,哪些“死了”(即不可能在被任何途径使用的对象)。第二件事才是进行内存回收。判断对象存活的算法有如下几种,其中JVM使用的是可达性分析算法。

3.2 回收无用类

判定无用类的条件则较为苛刻

  • 该类所有实例都已被回收
    即Java堆不存在该类的任何实例
  • 加载该类的ClassLoader已被回收
  • 该类的java.lang.Class对象没有被任何对象或变量引用,无法通过反射访问该类的方法
    只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class.这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除.

1.2.1 检测垃圾对象

检测出垃圾对象一般有两种算法:

1、 引用计数法

2、 可达性分析

引用计数法因为无法检测对象之间相互循环引用的问题,基本没有被采用。现在主流的语言的垃圾收集中检测垃圾对象主要还是“可达性分析”方法,下面也主要介绍JVM可达性分析方法检测垃圾对象。

 

“可达性分析”算法描述?

通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。所以JVM判断对象需要存活的原则是:能够被一个根对象到达的对象。

 

什么是能够到达呢?

就是对象A中引用了对象B,那么就称A到B可达。

 

GCRoot对象集合?

a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。(当前栈帧的对象引用)

b.方法区中的类静态属性引用的对象。(static对象引用)

c.方法区中的常量引用的对象。(final对象引用)

d.本地方法栈中JNI本地方法的引用对象。

除了堆之外,方法区也需要进行垃圾回收GC,那么如何检测出方法区的垃圾对象呢?

方法区中的“废弃常量”和“无用的类”需要回收以保证永久代不会发生内存溢出。

1、判断废弃常量的方法(常量不再需要):如果常量池中的某个常量没有被任何引用所引用,则该常量是废弃常量。

2、判断无用的类(class文件不再需要):

(1).该类的所有实例都已经被回收,即java堆中不存在该类的实例对象。

(2).加载该类的类加载器已经被回收。

(3).该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射机制访问该类的方法。

当持久代(方法区)满时,将触发Full GC,根据以上标准清除掉废弃的常量和无用的类。

2.1.1 引用计数算法(Reference Counting)

  • 描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1,引用失效时减1,当计数器值为0时对象就不可能在被使用了。
  • ** 优点**:实现简单,判定效率高。
  • 使用情况:微软的COM(Component Object Model)技术、使用ActionScript 3的FlashPlayer、Python语言使用该算法判定对象存活的。
  • 缺点:很难解决对象之间相互循环引用问题。如下案例:
/** * testGC()方法执行后,objA和objB会不会被GC呢? * @author zzm */public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过 */ private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; // 假设在这行发生GC,objA和objB是否能被回收? System.gc(); }}
  • 在上面这种情况下,实际上两个对象已经不可能在被访问了,但是因为互相引用者对象,导致计数器都不为0,如果采用引用计数器,objA和objB不会被GC收集器回收。
  • 但是在Java中这两个对象会被回收,因为JVM不是采用引用计数算法。所以很好的解决了循环引用问题。

4 垃圾收集算法

知道了判定方法,也就知道了垃圾收集器会清除哪些数据,那么接下来介绍如何清除这些数据.

1.2.2 释放空间

2.1.2 可达性分析算法(Reachability Analysis)

  • 描述:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相相连时(即从GC Roots到这个对象不可达),则证明此对象是不可用的。图片 2可达性分析算法判断对象是否可收回上图中object5、object6、object7虽然想好有关联,但是到GC Roots是不可达的,所以会被判定为是可回收的对象。
  • Java语言中可作为GC Roots的对象包括下面几种
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区类静态属性引用的对象.
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。
  • 方法区中常量引用对象。
  • 使用情况:主流程序语言都使用该算法来判定对象是否存活的。
  • 经过可达性分析算法确定了一个对象是一个可回收对象后,那么是不是就需要立即回收该对象呢?在对象要被回收前是否可以做一些事情呢?(比如复活、释放资源),这些就和java中的引用、和对象的finalize()方法有关了。

4.1 标记-清除算法(Mark-Sweep)

最基础的收集算法,后续算法也都是基于此并改进其不足而得.

首先利用刚才介绍的方法判断需要清除哪些数据,并给它们做上标记;然后清除被标记的数据.

1、垃圾回收算法

前面已经介绍了如何检测出垃圾对象,在检测出垃圾对象之后,需要按照特定的垃圾回收算法进行内存回收,常见的垃圾回收算法包括:

复制(Copying)

标记-清除(Mark-Sweep)

标记-整理(Mark-Compact)

分代(Generational Collection),借助前面三种算法实现

这里就不一一详述,感兴趣可以自行百度。

2.2.1 引用

  • 无论通过引用计数算法判断对象的引用数量、还是通过可达性分析算法判断对象的引用链是否可达,判定对象存活都与“引用”有关。jdk1.2之前,java中的对象只有被引用和没有被引用两种状态,这样的话GC收集器对该对象只有回收和不回收两种情况,但是对于一些“食之无味、弃之可惜”的对象,我们希望:当内存空间不足时,保留在内存中,如果内存进行垃圾收集后还非常紧张就抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
  • jdk1.2之后,java对引用概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phanton Reference)4种,4种强度依次减弱。
  • 强引用(Strong Reference):类似“Objcet obj = new Object()"这类的引用,只要引用还存在,垃圾收集器永远不会回收被引用的对象。
  • 软引用(Soft Reference):描述一些有用但非必要的对象,对于软引用关联着的对象,在系统发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收,为什么是第二次?因为第一次回收时发现该对象是软引用就不将其列入回收范围。第二次回收后还没有足够内存,才抛出异常。在被回收之前可以通过软引用获得对象。java提供SoftReference类来实现软引用
  • 弱引用(Weak Reference):描述一些有用但非必要的对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论内存是否足够,都会被回收。WeakReference类来实现,在被回收之前可以通过弱引用获得对象。。
  • 虚引用(Phanton Reference):最弱,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。唯一目的是当这个对象被收集时能够收到一个系统通知。Java中引用的详解JAVA四种引用方式

不足

  • 该算法标记和清除过程效率都不高,且标记清除之后存在大量不连续的内存碎片,导致以后因为存储大对象时无法找到足够的连续内存空间而提前触发另一次GC,降低了空间利用率.
![](https://upload-images.jianshu.io/upload_images/4685968-2f4485f2b21d0496)

标记-清除算法

2、垃圾收集器实现

上面算法都是理论性的东西,Java虚拟机规范没有规定垃圾收集器具体如何实现,因此不同厂商、不同版本虚拟机提供的垃圾收集器可能有所差异。下面列举HotSpot(Sun JDK和Open JDK自带)虚拟机提供的六种垃圾收集器实现:

收集器名称

应用目标

采用算法

引入版本

运行方式

Serial

新生代

复制算法

Jdk1.3.1前

串行,单线程

ParNew

新生代

复制算法

 

并行,多线程

Parallel Scavenge

新生代

复制算法

Jdk1.4

并行,多线程

Serial Old

旧生代

标记-整理

 

串行,单线程

Parallel Old

旧生代

标记-整理

Jdk1.6

并行,多线程

CMS

旧生代

标记-清除

Jdk1.5

并发,多线程

并行(Parallel):多条垃圾收集线程并行工作,而用户线程仍处于等待状态

并发(Concurrent):垃圾收集线程与用户线程一段时间内同时工作(不是并行,而是交替执行)

 

总结:

1、  两个串行收集器、三个并行收集器、一个并发收集器。

2、  ParNew收集器是Serial的多线程版本

3、  Serial Old收集器是Serial收集器的旧生代版本。

4、  Parallel Scavenge收集器以吞吐量为目标,适合在后台运算而不需要太多交互的任务。

5、  Parallel Old收集器是Parallel Scavenge的旧生代版本。

6、  Parallel Scavenge收集器和Parallel Old收集器是名副其实的“吞吐量优先”组合。

7、  除CMS外,其他收集器工作时都需要暂停其他所有线程,CMS是第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集器线程与用户线程同时工作,是一款以最短停顿时间为目标的收集器,适合交互性较多的场景,这也是与Parallel Scavenge/Parallel Old吞吐量优先组合的区别。

8、  新生代因为回收留下的对象少,所以采用标记-复制法。

9、  旧生代因为回收留下的对象多,所以采用标记-清除/标记-整理算法。

2.2.2生存还是死亡

  • 即使在可达性分析算法中不可达的对象,也并非”非死不可“,这时候它们暂时处于缓刑阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
  • 第一次标记并进行一次筛选,筛选条件是:对象是否有必要执行finalize()方法(Object的protected方法)。如果没必要执行,则会被回收。如果有必要则执行,则该对象会被放到一个F-Queue队列中,JVM会创建一个低优先级的Finalizer线程去执行队列中对象的finalize方法。对象在finalize方法中可以拯救自己,比如将this赋值给某个变量。finalize最主要目的是用来释放资源,毕竟finalize只会被调用一次。有必要执行finalize的条件是1.该对象的finalize方法被覆盖。2.该对象的finalize方法之前没有被调用过。
  • 第二次标记: 在稍后GC将对F-Queue列中对象进行第二次标记,如果这时对象没有拯救自己则就会被回收,否则会被移除”即将回收“集合

图片 3堆内存回收

  • 经过上面的可达性分析算法确定一个对象可以回收,以及通过引用类型或者finalize()方法最终确定一个对象的回收时机后,下面要做的事情就是对对象进行回收释放内存的工作了。JVM中如何进行内存回收呢?每个回收算法有什么优缺点呢?

4.2 复制算法(Copying)

将内存分成大小相等两份,只将数据存储在其中一块上

  • 当需要回收时,首先标记废弃数据
  • 然后将有用数据复制到另一块内存
  • 最后将第一块内存空间全部清除
![](https://upload-images.jianshu.io/upload_images/4685968-df08eebdab044bef.png)

复制算法

3、选择所需垃圾收集器

虚拟机提供了参数,以便用户根据自己的需求设置所需的垃圾收集器:

JVM运行参数

新生代

旧生代

-XX: UseSerialGC(Client模式默认值)

Serial

Serial Old

-XX: UseParNewGC

ParNew

Serial Old

-XX: UseConcMarkSweepGC

ParNew

CMS(Serial Old备用)

-XX: UseParallelGC(Server模式默认值)

Parallel Scavenge

Serial Old

-XX: UseParallelOldGC

Parallel Scavenge

Parallel Old

2.3.1标记-清除算法

  • 分两个阶段"标记“和”清除“:先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。(具体标记过程看上面)

  • 不足和产生产生的问题

  • 效率不高。

  • 空间问题:标记清除后会产生大量不连续内存碎片。碎片太多可能导致以后在程序中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    图片 4标记-清除算法示意图

4.2.1 分析

  • 这种算法避免了空间碎片,但内存缩小了一半.
  • 每次都需将有用数据全部复制到另一片内存,效率不高

二、性能调优

2.3.2复制算法

  • 总策略:可用内存分成两个相等的块。
  • 思想:将可用内存按容量划分为大小相等的两块。每次只使用其中一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
  • 优点
  • 对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动指针按顺序分配内存即可。
  • 缺点
  • 代价高,将内存缩小为原来一半了。

    图片 5复制算法示意图

4.2.2 解决空间利用率问题

在新生代中,由于大量对象都是"朝生夕死",也就是一次垃圾收集后只有少量对象存活
因此我们可以将内存划分成三块

  • Eden、Survior1、Survior2
  • 内存大小分别是8:1:1

分配内存时,只使用Eden和一块Survior1.

  • 当发现Eden Survior1的内存即将满时
  • JVM会发起一次MinorGC,清除掉废弃的对象,
  • 并将所有存活下来的对象复制到另一块Survior2中.
  • 接下来就使用Survior2 Eden进行内存分配

通过这种方式,只需要浪费10%的内存空间即可实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题.

2.1 性能调优的目的

减少minor gc的频率、以及full gc的次数

2.3.3改进的复制算法

  • 补充:JVM的堆中分为新生代和老年代,不同代中存放的对象生存时间不一样,生存时间不一样,那么对不同代的内存区域采用的回收算法就应该充分考虑到它们的特点。

  • 总策略: 1块Eden空间 2块Survior空间 分配担保。

  • 新生代中的对象98%都是朝生夕死,所以并不需要按照1:1的比例来划分内存空间,所以把内存分为1块较大的Eden空间和2块较小的Survivor空间

  • 回收过程每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还活着的对象一次性地复制到另外一块Survior空间上,最后清理掉Eden和刚才使用过的Survivor空间,即2个Survior轮流空着。

  • 注:2个Survivor就可以确保每次回收前至少有一个是空的,用来接收没被回收掉的。

  • Eden和Survior的分配比例HotSpot虚拟机默认Eden和Survivor大大小比例是8:1,也就是每次新生代中可用内存为整个新生代容量的90%,只有10%被浪费。

  • 分配担保基于上面的Eden和Survior的分配比例,当回收后有大于10%的对象存活的话,那么Survivor空间会不够用,这时就需要使用其他内存进行分配担保(Handle Promotion):即把新生代收集下来的存活对象通过分配担保机制复制到老年代中。然后在清理带Enden和刚才使用过的一块Survivor空间。(那么如果老年代的空间也不够存放呢?下面会讲。

  • 使用现状:现在商业虚拟机都采用这种收集算法来回收新生代.(那老年代用什么算法呢)

  • 为什么适合用在新生代中?新生代中对象产生的多、存活率低,所以复制操作就很少,回收就快,没有碎片问题,分配内存时也很快。

4.2.3 分配担保

准备为一个对象分配内存时,发现此时Eden Survior中空闲的区域无法装下该对象
就会触发MinorGC,对该区域的废弃对象进行回收.

但如果MinorGC过后只有少量对象被回收,仍然无法装下新对象

  • 那么此时需要将Eden Survior中的所有对象都转移到老年代中,然后再将新对象存入Eden区.这个过程就是"分配担保".

在发生 minor gc 前,虚拟机会检测老年代最大可用连续空间是否大于新生代所有对象总空间,
若成立,minor gc 可确保安全
若不成立,JVM会查看 HandlePromotionFailure是否允许担保失败

  • 若允许
    那么会继续检测老年代最大可用的连续空间是否 > 历次晋升到老年代对象的平均大小
    • 若大于
      则将尝试进行一次 minor gc,尽管这次 minor gc 是有风险的
    • 若小于或 HandlePromotionFailure 设置不允许冒险
      改为进行一次 full gc

2.2 性能调优的手段

1.使用JDK提供的内存查看工具,如JConsole和Java VisualVM

2.控制堆内存各个部分所占的比例

3.采用合适的垃圾收集器

2.3.4 标记-整理算法

  • 算法思想与标记-清除类似,只是标记完了,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理带哦端边界以外的内存。

图片 6标记-整理算法示意图

  • 老年代适合采用”标记-整理“老年代中对象存活时间长,使用复制算法的话需要进行太多复制操作,效率变低,还有就是如果不想浪费50%空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%活着的极端情况。所以标记整理比较适合。

4.3 标记-整理算法(Mark-Compact)

在回收前,标记过程仍与"标记-清除"一样
但后续不是直接清理可回收对象,而是

  • 将所有存活对象移到一端
  • 直接清掉端边界之外内存
![](https://upload-images.jianshu.io/upload_images/4685968-794931bd0bba6b8d.png)

标记-整理算法

手段1:内存查看工具(如jstat)和GC日志分析

n  -verbose.gc:显示GC的操作内容。打开它,可以显示最忙和最空闲收集行为发生的时间、收集前后的内存大小、收集需要的时间等。

n  -xx: printGCdetails:详细了解GC中的变化。

n  -XX: PrintGCTimeStamps:了解这些垃圾收集发生的时间,自JVM启动以后以秒计量。

n  -xx: PrintHeapAtGC:了解堆的更详细的信息。

此外还包括通过jmap MAT的方式分析可能发生的内存泄露的原因,哪些对象占用内存较大等。

2.3.5分代收集算法

  • 当前商业虚拟机的垃圾收集都采用"分代收集“(Generational Collection)算法。
  • 算法思路
  • 根据对象存活周期的不同将内存划分为几块。一般把Java堆中分为新生代和老年代,这样可以根据各个年代的特点采用最适当的收集算法。
  • 新生代中每次垃圾收集都有大批对象死去,少量存活,所以选复制算法。
  • 老年代中对象存活率高、没有额外空间对它进行分配担保,就必须使用”标记-清理“或”标记-整理“算法进行回收。
  • 上面讲的内容都是针对堆中的内存回收,那么方法区呢?堆中的内存在JVM中被分为”新生代“、“老年代”。而方法区的内存被称为“永久代”。Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低,即回收一次只有很少的内存被释放掉。
  • 永久代中垃圾收集的内容:废弃常量、无用类。

分析

这是一种老年代垃圾收集算法.
老年代中对象一般寿命较长,每次垃圾回收会有大量对象存活
因此如果选用"复制"算法,每次需要較多的复制操作,效率低

而且,在新生代中使用"复制"算法
当 Eden Survior 都装不下某个对象时,可使用老年代内存进行"分配担保"

而如果在老年代使用该算法,那么在老年代中如果出现 Eden Survior 装不下某个对象时,没有其他区域给他作分配担保

因此,老年代中一般使用"标记-整理"算法

手段2:针对新生代和旧生代的比例

如果新生代太小,会导致频繁GC,而且大对象对直接进入旧生代引发full gc

如果新生代太大,会诱发旧生代full gc,而且新生代的gc耗时会延长

建议新生代占整个堆1/3合适,相关JVM参数如下:

n  -Xms:初始堆大小

n  -Xmx:最大堆大小

n  - Xmn:新生代大小

n  -XX:PermSize=n:持久代最大值

n  -XX:MaxPermSize=n:持久代最大值

n  -XX:NewRatio=n:设置新生代和旧生代的比值。如:为3,表示新生代与旧生代比值为1:3,新生代占整个新生代旧生代和的1/4

3.2.1方法区中常量类型

  • 先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:1、类和接口的全限定名2、字段的名称和描述符3、方法的名称和描述符

4.4 分代收集算法(Generational Collection)

当前商业虚拟机都采用此算法.
根据对象存活周期的不同将Java堆划分为老年代和新生代,根据各个年代的特点使用最佳的收集算法.

  • 老年代中对象存活率高,无额外空间对其分配担保,必须使用"标记-清理"或"标记-整理"
  • 新生代中存放"朝生夕死"的对象,用复制算法,只需要付出少量存活对象的复制成本,就可完成收集

手段3:针对Eden和Survivor的比例

如果Eden太小,会导致频繁GC

如果Eden太大,会导致大对象直接进入旧生代,降低对象在新生代存活时间

n  -XX:SurvivorRatio=n:新生代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

n  -XX:PretenureSizeThreshold:直接进入旧生代中的对象大小,设置此值后,大于这个参数的对象将直接在旧生代中进行内存分配。

n  -XX:MaxTenuringThreshold:对象转移到旧生代中的年龄,每个对象经历过一次新生代GC(Minor GC)后,年龄就加1,到超过设置的值后,对象转移到旧生代。

3.2.2回收过程

  • 回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类、方法、字段的符号引用也与此类似。
  • 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

  • 加载该类的ClassLoader已经被回收。

  • 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  • 虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class及-XX: TraceClassLoading、 -XX: TraceClassUnLoading查看类的加载和卸载信息。

  • 在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

  • 这篇文章主要讲了JVM中垃圾收集器在堆上进行内存回收涉及到的内容:可达性分析算法判断对象是否无用、为对象设置不同引用类型来让对象的回收时机与堆内存是否充足相联系起来、对象的finalize()方法可以在被回收前做一些事情;内存回收涉及到的相关算法,JVM的堆使用了分代收集算法(新生代使用复制算法、老年代使用标记-清除算法)。
  • 并讲了方法区中内存回收:废弃常量、无用类。
  • 下一篇:讲完内存回收相关知识,下一节讲内存分配。堆中的对象是如何分配的,堆中不同的区中的对象怎么分配内存的?。参考文章:GC在堆和方法区的内存回收JVM方法区内存回收

5 Java中引用的种类

Java中根据生命周期的长短,将引用分为4类

  • 强引用
    我们平时所使用的引用就是强引用
    类似A a = new A();
    即通过关键字new创建的对象所关联的引用就是强引用
    只要强引用还存在,该对象永远不会被回收
  • 软引用
    一些还有用但并非必需的对象
    只有当堆即将发生OOM异常时,JVM才会回收软引用所指向的对象.
    软引用通过SoftReference类实现
    软引用的生命周期比强引用短一些
  • 弱引用
    也是描述非必需对象,比软引用更弱
    所关联的对象只能存活到下一次GC发生前.
    只要垃圾收集器工作,无论内存是否足够,弱引用所关联的对象都会被回收.
    弱引用通过WeakReference类实现.
  • 虚引用
    也叫幽灵(幻影)引用,最弱的引用关系.
    它和没有引用没有区别,无法通过虚引用取得对象实例.
    设置虚引用唯一的作用就是在该对象被回收之前收到一条系统通知.
    虚引用通过PhantomReference类来实现.

手段4:采用正确的垃圾收集器

通过JVM参数设置所使用的垃圾收集器参考前面的介绍,这里关注其他一些设置。

并行收集器设置

n  -XX:ParallelGCThreads=n:设置并行收集器收集时并行收集线程数

n  -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间,仅对ParallelScavenge生效

n  -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比,仅对Parallel Scavenge生效

 

并发收集器设置

n  -XX:CMSInitiatingOccupancyFraction:默认设置下,CMS收集器在旧生代使用了68%的空间后就会被激活。此参数就是设置旧生代空间被使用多少后触发垃圾收集。注意要是CMS运行期间预留的内存无法满足程序需要,就会出现concurrent mode failure,这时候就会启用Serial Old收集器作为备用进行旧生代的垃圾收集。

n  -XX: UseCMSCompactAtFullCollection:空间碎片过多是标记-清除算法的弊端,此参数设置在FULL GC后再进行一个碎片整理过程

n  -XX:CMSFullGCsBeforeCompaction:设置在若干次垃圾收集之后再启动一次内存碎片整理

TAG标签:
版权声明:本文由澳门新葡8455手机版发布于计算机编程,转载请注明出处:垃圾堆搜集器与内部存储器分配政策,垃圾回收