欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JAVA垃圾收集器与内存分配策略详解

程序员文章站 2024-03-06 08:52:43
引言 垃圾收集技术并不是java语言首创的,1960年诞生于mit的lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。垃圾收集技术需要考虑的三个问题是: 1、哪...

引言

垃圾收集技术并不是java语言首创的,1960年诞生于mit的lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。垃圾收集技术需要考虑的三个问题是:

1、哪些内存需要回收

2、什么时候回收

3、如何回收

java内存运行时区域的分布,其中程序计数器,虚拟机栈,本地方法区都是随着线程而生,随线程而灭,所以这几个区域就不需要过多考虑回收问题。但是堆和方法区就不一样了,只有在程序运行期间我们才知道会创建哪些对象,这部分内存的分配和回收都是动态的。垃圾收集器所关注的就是这部分内存。

一 对象死亡判据

垃圾收集器在对一个对象回收之前,首先要判断对象在程序中是否还有使用的可能性,充要条件就是没有被程序可访问引用再指向这个对象实例。最简单的办法就是给对象实例添加中添加一个引用计数器,每当有一个引用指向它时,计数器就加一,当引用失效时,计数器就减一,如果计数器值为0则说明没有引用指向它,可以进行回收。但是这个方法中计数器为0并不是一个必要条件,例如,生成两个对象实例,每个对象实例的属性都指向对方,那么这个两个对象实例分别最少有一个引用。

JAVA垃圾收集器与内存分配策略详解

java采用的是可达性分析算法,即找一部分对象作为"gc roots"节点,从这些节点开始向下搜索,当某个对象到"gc roots"节点没有可达路径时,说明此对象是不可用的。在java中作为"gc roots"的节点包括:虚拟机栈中引用的对象,方法区静态属性引用的对象,方法区常量引用的对象,本地方法区中本地调用所引用的对象。

引用扩充

如果reference类型的数据中存储的数值是另一块内存的起始地址,那么这块内存就代表着一个引用。一个对象在这种状态下,只能有被引用和没有被引用两种状态。java对引用概念进行了扩充,将引用分为强引用(new),软引用(softreference),弱引用(weakreference),虚引用(phantomreference)。如果强引用存在,则垃圾收集器不会回收该对象。如果系统即将发生内存溢出异常,那么垃圾回收集器则会回收软引用对象。弱引用对象只能存活到下一次垃圾收集之前。虚引用对象不会对其生存时间构成任何影响。

对象的自我救赎

在垃圾收集器发现某一个对象到"gc roots"路径不可达时,先会判断该对象是否覆盖finalize()方法,或是否执行过finalize()方法。如果覆盖了且没有执行过该方法,则会将该对象放到低优先级的finalizer线程中去执行finalize()方法,如果在finalize()方法中该对象又被引用,则会有一次逃脱被回收的命运。

方法区的回收

方法区中主要回收废弃的常量和无用的类。对于常量,如果没有引用指向常量,则该常量会被回收。对于类的回收则麻烦许多,首先要判断该类是无用的类,无用的类要满足三个条件:1所有类的实例被回收2加载该类的classloader已经被回收3class没有被引用,不会通过反射访问该类的方法。

二 垃圾回收算法

标记-清除算法(mark-sweep)

JAVA垃圾收集器与内存分配策略详解

该算法分为两个阶段:首先标记处要回收的对象,标记完成后统一回收所有被标记的对象。

存在的问题:1 标记和清除效率都不高 2 标记清除后会产生大量内存碎片,分配大对象时可能触发另一次垃圾收集。

复制算法(copying)

JAVA垃圾收集器与内存分配策略详解

该算法将内存分为两个等大小的区域,每次只使用一个区域。当一个区域快用完了,就将这个区域中存活的对象复制到另一个区域

优点是避免了内存碎片的产生,缺点是浪费内存空间。

JAVA垃圾收集器与内存分配策略详解

有公司研究表明,新生代的对象98%都是朝生暮死,所以虚拟机把新生代内存划分为一个较大的eden空间和两个较小的survivor空间。每次只是用eden空间和一个survior空间,当进行复制清理时,将survivor空间和eden空间中存活的对象复制到另一块survivor空间。当survivor空间不够用时,就会依赖老年代进行分配担保。

标记-整理算法(mark-compact)

JAVA垃圾收集器与内存分配策略详解

针对老年代对象存活率高的情况,复制算法明显不合适,于是采用标记整理算法,标记和标记清除算法相同,二后边的整理则是让所有存活的对象都向一端移动,然后清理掉边界外的内存。

分代收集

当前虚拟机都采用分代收集,分代的依据是对象的存活周期。一般新生代存活率低,采用复制算法。老年代存活率高采用标记整理或标记清除。

三垃圾收集器

JAVA垃圾收集器与内存分配策略详解

由于虚拟机采用了分代收集,所以针对不同代收集器也不同。上图是hotspot虚拟机的垃圾收集器,连线表示可以协同工作。

serial收集器,复制算法,它是一个单线程的收集器,并且在进行收集时会暂停其他线程,它默认是client模式下的新生代收集器。

parnew收集器是serial收集器的多线程版,它是第一款并发收集器。

parallel scavenge收集器可以精确控制吞吐量(用户代码运行时间/(用户代码时间+垃圾收集时间))

serialold收集器是serial收集器的老年版,采用标记整理算法,同样是单线程收集器。

parallelold是parallelscavenge收集器的老年版,使用多线程和标记整理算法。

cms收集器是以最短回收停顿时间为目标的收集器,采用标记清除算法,在重视响应速度的系统中得以应用。但是缺点是对cpu资源敏感,无法处理浮动垃圾,易产生内存碎片。

g1收集器是最新推出的收集器,可应用在jdk1.7u4及以上版本。它将内存分为多个region,新生代和老年代分别包含多个region。g1跟踪各个region,判断垃圾价值大小,优先回收价值最大的region。

四 内存分配与回收策略

对象的分配,就是在堆上分配,对象主要分配在新生代的eden区域中,如果启动了本地线程分配缓冲,则按线程优先在tlab中分配。少数情况也有可能直接分配到老年代。

对象在eden区域分配时,当eden区域没有足够空间,虚拟机会发起一次新生代垃圾收集。

如果对象需要大量连续内存空间,例如string类型和数组。大对象对于虚拟机内存分配来说是一个坏消息,朝生暮死的大对象是要命的坏消息。经常出现大对象会导致多次出发垃圾收集。对于这类对象,可以设置参数将大对象直接存入老年代。

每一个对象都有一个年龄计数器,当对象在eden区域出生,每经过一次gc,并且存入survivor,计数器加一。当年龄增加到一定程度(默认15),则会被存入老年代。同时,如果survivor空间中相同年龄对象占空间超过50%,则也会直接进入老年代。

总结

垃圾收集算法:复制算法,标记-清除算法,标记-清理算法。

垃圾收集器特点:新生代用复制,老年代用标记清理,cms用标记清除。

eden空间大小和survivor空间大小默认比率为8:1,即新生代10%的空间用来存放复制后的对象。

以上就是本文的全部内容,希望大家能够喜欢。