文章目录
垃圾收集器和内存分配
在垃圾收集需要考虑完成的三件事情:
- 那些内存需要回收?
- 什么时候回收?
- 如何回收?
为什么我们要了解垃圾收集和内存分配?
很简单,当需要排查各种内存溢出,内存泄漏时,当垃圾收集器成为系统达到高并发的瓶颈时,
就必须对这些“自动化”的技术实施必要的监控和调节。
在Java内存运行时区域的各部分中,其中程序计数器,虚拟机栈,本地方法3个区域随线程生,随线程灭。
每个栈桢基本是在类结构确定下来的,大体是编译器确定的。因此这几个区域的内存分配和回收具备确定性。当方法结束或线程结束后,内存自然会随之回收了。
而Java堆和方法区有明显的不确定性。一个接口的多个实现类的内存可能不一样,一个方法执行中的控制流语句需要的内存也可能不一样。只有在运行期间,才能确定程序会创建那些对象,创建多少对象,这两个区域的内存和回收是动态的。也是垃圾收集器关注的就是该部分内存 应该如何管理。
判断对象活着没有?
堆中存放着Java中几乎所有的对象实例。垃圾收集器在堆进行回收前,第一件事情就是判断这些对象中那些还“活着”,那些已经“死去”(即不可能被任何用途使用的对象)。
引用计数算法
这种算法判断对象是否存活的算法是这样的:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一。任何时刻计数器为零的对象就是不可能在被使用的。
可达性分析算法
一些主流的程序语言的内存管理,都是通过可达性分析算法判定对象是否存活的。
这个算法的基本思路是通过一系列称为“GC Roots”的跟对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain)。
如果某个对象到GC Roots间没有任何引用链相连。或说从GC Roots到这个对象不可达时,则证明这个对象是不可能再次被使用的。
Java体系中,固定作为GC Roots的对象包括以下几种:
垃圾收集算法
分代收集理论
分代收集理论,实质上遵循了程序运行实际情况的经验法则,建立在两个分代假说之上:
- 弱分代假说:绝大数对象都是朝生夕灭。
- 强分代加锁:熬过多次垃圾收集器过程的对象就难以被消灭。
这两种分代假说给出了一个设计规则,Java堆划分不同的区域,然后将回收对象依据年龄分配到不同的区域进行存储。很明显,将大多数对象是朝生夕灭放到一个区域集中起来,每次回收只关注如何保留少量的存活的而不是标记那些大量将要被回收的对象。如果剩下少量存活的对象,可以将其集中在一起,虚拟机就可以使用较低的频率回收这个区域。那么,前一个区域就为新生代,后一个区域就为老年代。
Java堆从GC的角度来看,可以细分为:新生代 Young 和老年代 Old两个区域。
新生代
是存放新生的对象。由于频繁创建对象,所以会频繁触发新生代的垃圾收集。进而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法” 等针对性的垃圾收集算法。
老年代
主要存放应用程序中生命周期长的内存对象。
在新生代中,每次垃圾收集时都会发现大批对象死去,而每次回收后存活的少量对象,将逐步晋升到老年代存放。
标记-清除算法
最基础的垃圾收集算法,分为两个阶段:标记和清除。即首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象。
主要两个缺点
- 随着对象的不断增加,大量的标记和清除的动作,导致这个两个过程的执行效率会随对象数量的增大而降低。
- 内存空间的碎片化。产生出大量不连续的内存碎片,会导致应用程序运行过程中分配较大的对象时无法找到足够连续的内存而不得不提前触发一次垃圾收集动作。
标记-复制算法
按照内存容量将内存划分为等大小的两块。每次只使用其中的一块,当这一块内存满后将尚存活的对象复制到另一块上去。然后在把已使用过的内存空间清理掉。
优点是实现简单,运行高效。其最大问题为将可用的内存缩小为原来的一般,空间浪费太多了。
标记-整理算法
这个算法中标记的过程和“标记-清除”算法一致。但后续步骤不是直接对可回收对象进行清理,而是让所有存活的都向内存空间一端进行移动,然后清除端边界外的对象。
GC垃圾收集器
Java堆内存被划分为新生代和年老代两个部分。新生代主要使用复制和标记-清除垃圾回收算法。
年老代主要使用标记-整理垃圾回收算法。
Serial垃圾收集器 (单线程,复制算法)
Serial是最基础,历史最悠久的收集器。曾在JDK1.3.1之前是HotSpot虚拟机新生代收集器的唯一选择。
Serial是一个单线程的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
ParNew垃圾收集器
ParNew垃圾收集器其实是Serial收集器的多线程并行版本。除了同时使用多线程进行垃圾收集外,其余行为和Serial收集器完全一致。如收集算法,Stop The World(其在垃圾收集过程中同样也要暂停所有其他工作线程)。重要的一点是:除了Serial收集器外,目前只有它能够与CMS收集器配合工作。
ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。
Parallel Scavenge 收集器
parallel Scavenge收集器也是一个新生代垃圾收集器,也是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。它重点目标是达到一个可控制的吞吐量。即处理器用于运行用户代码的时间与处理器总消耗的时间(吞吐量 = 运行用户代码时间 / 运行代用户码时间 + 垃圾收集时间)。
Serial Old 收集器(单线程 标记整理算法)
Serial Old是Serial收集器的老年代版本,同样为一个单线程收集器,使用标记-整理算法。这个收集器主要供客户端模式下的HotSpot虚拟机使用。在Server模式下,主要有两个用途:
- JDK1.5及之前版本与新生代的Parallel Scavenge收集器搭配使用。
- 另外一个是作为老年代中使用CMS收集器的后备垃圾收集方案。
Parallel Old收集器 (多线程 标记整理算法)
Parallel Old收集器时Parallel Scavenge收集器的老年版本,基于标记-整理算法实现。
在JDK1.6之前,新生代的Parallel Scavenge只能配置老年代的Serial Old收集器。只能保证新生代的吞吐量,无法保证整体的吞吐量。
Parallel Old收集器出现后,“吞吐量优先”收集器有了名副其实的搭配。如果系统对吞吐量要求比较高,可以优先考虑新生代的Parallel Scavenge和Parallel Old收集器的搭配。
CMS收集器
CMS(concurrent mark sweep)是一种老年代垃圾收集器,其最主要目标是获取最短停顿时间。和其他老年代使用的标记-整理算法不同,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS工作机制相比其他的垃圾收集器来说比较复杂。整个过程分为以下4个阶段:
- 初始标记
只是标记一下GC Roots能够直接关联的对象,速度很快,仍然需要暂停所有的工作流程。
- 并发标记
进行GC Roots跟踪的过程。和用户线程一起工作,不需要暂停工作线程。
- 重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然需要暂停所有的工作线程。
- 并发清除
清除GC Roots不可达对象,和用户线程一起工作,不需要暂定工作线程。由于耗时最长----