总结:
面试一般会问 java垃圾回收机制 GC算法 java内存分区(结构) java内存模型 类加载机制
GC,内存的分区,堆,类放在哪个区,什么时候会内存溢出,GC root
CMS G1收集器要能说出来, OOM StackoverFlow , 这还是要认真理解并适当强化记忆,有时书看了,但不总结的话,看了也白看,两天之后就忘了,技术类的书籍一定要多总结,别偷懒,有时现在怕麻烦,将来更麻烦
1. 概述
GC的历史比java久远, Lisp语言就用到了GC
GC要解决的三个问题: 哪些内存需要回收?什么时候回收?如何回收?
在java里面,内存区域中的程序计数器、虚拟机栈、本地方法栈这3个区域随着线程而生,线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈的操作,每个栈帧中分配多少内存基本是在类结构确定下来时就已知的。在这几个区域不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。
而Java堆和方法区则不同,一个接口中的多个实现类需要的内存可能不同,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的
(1)引用计数算法
给对象添加引用计数器,有一个地方引用就加1, 失效了就减1,计数器为0的对象不可能被再次使用
缺点:无法解决对象之间循环引用的问题
(2)可达性分析算法
java C# Lisp都是采用的这种
概念:从根(GC Roots)的对象作为起始点,开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没 有任何引用链相连时,则证明此对象是不可用的。
在java中, 可以作为GC Roots的对象
1、栈(栈帧中的本地变量表)中引用的对象。
2、方法区中的静态成员。
3、方法区中的常量引用的对象(全局变量)
4、本地方法栈中JNI(一般说的Native方法)引用的对象。
再谈引用:四大引用
强引用:Object o=new Object() 只要强引用存在,就不会被回收
软引用:SoftReference类来实现 软引用关联的对象,在将要发生内存溢出异常之前,才会被GC。
弱引用:WeakReference 强度比soft弱,关联的对象只能生存到下一次GC发生之前
虚引用:PhantomReference 最弱 关联对象的唯一目的 就是被GC时 收到一个系统通知
3. 垃圾收集算法
1.标记--清除算法(Mark--Sweep)
标记-清除算法:最基础的收集算法“标记--清除”(Mark-sweep)算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,对象的标记过程是采用“可达性分析算法”来进行的。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而来的。
主要缺点:a、效率问题,标记和清除两个过程的效率都不高。
b、空间问题,标记清除后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续空间而不得不提前触发另一次垃圾收集活动。
2.复制算法(新生代采用)
为了解决效率问题,复制的收集算法出现了
他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,当然再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这个算法的代价是将内存缩小为了原来的一半,未免太高了点。
现在的商业用的虚拟机都是采用这种算法,HotSpot默认的Eden : survivor =8:1
3. 标记-整理算法(老年代采用)
复制算法在对象存活率较高的时候进行较多的复制操作,效率将会变得更低
老年代一般不适用复制算法
标记过程仍然与“标记--清除”算法一样,但后续步骤不是直接对回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
4.分代收集算法:当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记--清理”或者“标记--整理”算法来进行回收。
(二)HotSpot的算法实现
1. Serial收集器(单线程收集)
JDK1.3新生代收集的唯一选择
单线程的收集器:只使用一个CPU或一条收集线程进行垃圾收集时,它在工作时,必须暂停其他所有工作线程,直到它收集结束
2.ParNew收集器(Serial多线程版本的收集)
他是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop the world、相当多的代码、回收策略等都与Serial收集器完全一样。
注意在谈及垃圾收集器 几款并发和并行的收集器时 根据上下文语句, 解释两个概念
&1.并行: 指多条垃圾收集线程并行工作,但此用户仍处于等待状态
&2. 并发: 指用户线程和垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另外的CPU上
3.Parallel Scavenge收集器(吞吐量优先的收集器):是一个新生代收集器,他也是使用复制算法的收集器,又是并行的多线程收集器。看上去和ParNew都一样,但他的特点是他的关注点与其他收集器不同,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
4.Serial Old收集器:它是Serial收集器的老年代版,它同样是一个单线程收集器,使用“标记--整理”算法。这个收集器的意义在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要有两大用途:一种是在jdk1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后预案,在并发收集发生Concurrent Mode Failure时使用。
5。Parallel Old 收集器:是Parallel Scavenge收集器的老年代版,使用多线程与“标记--整理”算法。这个收集器在jdk1.6中才开始提供的,直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加 Parallel Old收集器。
前面的收集器作为了解的话,接下来的收集器比较重要
6. CMS收集器(Concurrent Mark Sweep)
CMS:是一种以获取最短回收停顿时间为目标的收集器。基于标记--清除算法实现的
目前很大一部分的java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
过程分为4步:初始标记 : 标记GC Roots能直接关联的对象
并发标记 : 进行GC Roots Tracing的过程
重新标记 : 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那部分标记记录
并发清除
初始和重新标记需要 stop the world 两个并发过程耗时较长
CMS收集器是低停顿的收集器,但是还有三大缺点
1)对CPU资源敏感(并发设计都对CPU敏感)
2)无法处理浮动垃圾(CMS在当次收集无法处理,只好留到下次GC处理的),可能出现Concurrent Mode failure失败导致另一次full GC,老年代使用60%就激活,JDK1.6 时92%才激活
3)收集结束时产生大量空间碎片(空间碎片过多 导致大对象分配困难 老年代还有很多空间但是没有足够的连续空间 不得不提前Full GC)
7.G1收集器
(1)最先进的收集器
G1是一款面向服务器端应用的垃圾收集器。与其他GC收集器相比,G1具备如下特点:
a、并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
b、分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能够独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
c、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记--整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。这个特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前出发下一次GC。
d、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时java(RTSJ)的垃圾收集器的特性了。
使用G1收集器时,java堆的内存布局就与其他收集器有很大差别,它将真个java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代与老年代的概念,但新生代与老年代不再试物理隔离的了,他们都是一部分Region(不需要连续)的集合。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下几个步骤:
a、初始标记(Initial Marking)
b、并发标记(Concurrent Marking)
c、最终标记(Final Marking)
d、筛选回收(Live Data Counting and Evacuation)
三. 理解GC日志
|
(1)最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。
(2)GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的,例如下面这段新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC(System)”。
[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]
(3)接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。GC发生区域日志与GC收集器对照列表如下:
GC日志区域名 | 对应GC收集器名 |
[DefNew (Default New Generation) | Serial收集器 |
[ParNew (Parallel New Generation) | ParNew收集器 |
[PSYoungGen | Parallel Scavenge收集器 |
[ParOldGen | Parallel Old收集器 |
(4)后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。
而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”。
(5)再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。
有的收集器会给出更具体的时间数据,如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。
CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。
四.内存分配和回收策略
1. 对象的内存分配:主要分配在堆内存的新生代的Eden区
2. 对象优先在Eden分配,
当没有足够的空间进行分配时 JVM发起一次Minor GC (minor:较小的,少数的 未成年的)也就是轻量级GC Eden : from: to=8:1:1
3. 大对象直接进入老年代
大对象:就是需要大量连续内存空间的java对象 就是很长的字符串和数组byte[ ]就是典型的大对象
4. 长期存活的对象将进入老年代
JVM给每个对象一个对象年龄age计数器,Eden--->Survivor 就是1岁,此后每次在Survior熬过一个Minor GC就增加1岁, 到15岁就晋升到老年代
5.空间分配担保
发生在Minor GC之前,JVM先检查老年代最大可以使用的连续内存空间 > 新生代所有对象总空间,这将确保Minor GC安全, 如果小于 或者HandlePromotionFailure设置为不允许冒险 改为进行一次Full GC
-Xms -Xmx java heap
-Xmn 新生代内存