垃圾回收
垃圾回收机制:
不定时的去堆内存中清理不可达对象.垃圾回收器执行是自动的,程序员只能通过System.gc去建议垃圾回收器进行垃圾回收,但是是否执行,什么时候执行都是不可控的.
finalize方法:Java中使用finalize()方法在垃圾回收器将对象从内存中清除出去前,做必要的清理工作.这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
GC线程是守护线程.
Java堆内存可化为分新生代,老年代. 新生代,老年代内存比例默认1:2(该值可以通过参数 -XX:NewRatio来指定),而新生代可细分为Eden区,from Survivor, to Survivor区,比例8:1:1(可通过-XX:SurvivorRatio来指定)如下:
图片.png
新生代(Young):刚出生不久的对象,存放在新生代里,存放不是经常使用的对象.
老年代(Old):存放比较活跃的对象,或者说是存在较久的年老的对象.
判断对象是否已死:
1.引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的.
>引用计数法弊端:很难解决对象之间的互相循环引用的问题,导致,本已经没有再引用的对象,因为相互引用着而导致计数器不为0,而无法回收,所以,现在Java虚拟机不采用引用计数法.
2.根搜索算法(可达性分析算法):通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的.如下图所示:
图片.png
可作为GC Roots对象包括下面几种:
>>虚拟机栈(栈帧中的本地变量表)中引用的对象.
>>方法区中类静态属性引用的对象
>>方法区中常量引用的对象
>>本地方法栈中JNI(native 方法)引用的对象.
引用:
如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用.
JDK1.2以后,对引用概念进行扩充,将引用分为了强引用,软引用,弱引用,虚引用,4种,引用强度依次减弱.
强引用:就是指在程序代码中普遍存在的,类似Object() object = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象.
软引用:是用来描述一些还有用但并非必需的对象.对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中,进行二次回收.(例如:缓存对象),如果我们要使用软引用的话,编码的时候,把对象采用jdk中提供的SoftReference类型来包装
弱引用:也是用来描述非必须的对象的,但是它的强度比软引用更弱一些.被弱引用关联的对象只能生存到下一次垃圾收集发生之前.
虚引用:也称为幽灵引用或者幻影引用,他是最弱的一种引用关系.一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例.为一个对象设置虚引用的目的就是能再这个对象被收集器回收时收到一个系统通知.
虚引用PhantomReference是一种特殊的引用,也是最弱的引用关系,用来实现Object.finalize功能,在开发中很少使用;
不可达对象被回收的过程(两次标记):
当一个对象被垃圾收集器认为不可达时,此时这个对象会被第一次标记,并进行一次筛选,筛选的条件就是该对象有没有必要执行finalize()方法.当对象没有覆盖finalize()方法,或着已经被虚拟机执行过,则认为没有必要执行(一个对象的finalize方法只会被调用一次).
虚拟机会将这些有必要执行的对象放置到一个叫做F-Queue的队列中,并在稍后由虚拟机自动建立的,低优先级的Finalizer线程中取执行它的finalize()方法,但是虚拟机只去触发调用,不保证会等待finalize()执行结束.
执行finalize()是对象逃离死亡的最后的机会,只要在finalize()方法中重新与引用链连接上即可.重新建立上连接的对象,会在回收器在队列里做第二次标记时,将之移除队列,使其逃出升天.而,没有重新建立上连接的对象在第二次被标记后,就真的被回收了.
方法区的回收:
1.废弃的常量
2.无用的类
>该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例.
>加载该类的ClassLoader已经被收回
>该类对应的Java.lang.Class 对象没有任何地方被引用,无法再任何地方通过反射访问该类.
满足以上3点,才能认为是无用的类而可以进行回收.需要进行-Xnoclassgc参数设置,才能被回收.
垃圾收集算法:
image.png
1>标记-清除算法:
算法分为"标记"和"清除"两个阶段,首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象.
缺陷:1.效率问题,标记 和 清除 两个过程效率都不高.
2.空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作.
图片.png
2.复制算法:
复制算法将内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清除掉.
因为IBM公司的研究表明,98%的对象都是"朝生夕死",所以,一般不会按1:1去划分空间,而是分为一个Eden区和两个survivor区,每次都使用一个Eden区和一个survivor区的空间.当执行回收时,将Eden和from survivor区的还存活的对象一次性的复制到to survivor区中,然后清理Eden和from survivor区.HotSpot 默认Eden:survivor=8:1.
但是,当survivor区内存不够用时,需要依赖老年代进行分配担保(Handle Promotion),大概意思是,在垃圾回收时,如果to survivor的空间不足以存放Eden和 from survivor空间存活的对象时,这些对象将直接通过分配器担保机制进入老年代(一般对象在from , to之间来回15此才会被移进老年代,15次这个是由JVM参数MaxTenuringThreshold决定的,默认是15).
优点:在存活对象不多的时候,效率比较高,并解决碎片化问题.
缺点:会造成一部分内存的浪费,并且如果存活的对象比较大,复制的效率会比较低.
3.标记-整理算法(标记-压缩算法):
标记-整理算法 与标记-清除算法基本一样,只是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后再直接清理掉端边意外的内存.
图片.png
优点:解决了内存碎片化的问题.
缺点:压缩阶段,由于移动了可用对象,则需要更新对象的引用.
4.分代收集算法
分代收集算法将堆空间分为新生代,老年代.根据各个年代的特点采用最适用的手机算法.新生代中,每次垃圾收集(young GC)时都会有大批对象死去,只有少量存活,一般采用复制算法.老年代(Full GC)中因为对象对象存活率高,没有额外的空间进行分配担保,就必须使用"标记-清除"或者"标记-整理"算法.
young GC(Minor GC):
当Eden空间满的时候会触发,survivor满的时候不会触发.一般young GC 较为频繁,回收速度也比较快.
Full GC(Major GC):
当老年代满了的时候会引发Full GC,Full GC 将会同时回收年轻代和老年代,即一般Full GC 会伴随至少一次的young GC.当永久代(方法区)满的时候也会引发Full GC,会导致Class,Method 元信息的卸载(Java 8中,移除了永久代,新加了一个元数据区的native内存区).Full GC速度比较慢,一般会比young GC慢10倍以上.
JVM 参数配置:
-Xmx20M -Xms20M -Xmn1M -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
1>在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率
图片.png
Java 堆溢出:
Java堆用于存储对象实例,只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾收集器清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出(Java.lang.OutOfMemoryError: Java heap space).
要解决这个问题,一般需要通过内存映像分析工具(idea JVM Debugger Memory View)对Dump出来的堆转存储快照进行分析,确认内存中的对象是否是必要的.
对象是必要的则是内存溢出,对象不必要的话就是内存泄露.
如果是内存泄露,进一步查看泄露对象到GC Roots的引用链,找到泄露对象为什么与GC Roots相关联,定位问题,解决问题.
如果是内存溢出,查看代码是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期间的内存消耗.另外,确认-Xms,-Xmx参数,与物理内存相比较,确认是否可以对堆内存进行增加.
虚拟机栈和本地方法栈:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError.(可-Xss增大,一般出现在方法循环递归时)
如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError(虚拟机栈一般不会出现).
方法区和运行时常量池溢出:
方法区溢出是比较常见的,因为一个类要被判定为可回收的比较苛刻.在经常动态生成大量Class 的应用,或者加载大量JSP等应用,会经常出现这用异常(Java.lang.OutOfMemoryError:PermGen space),增加方法区空间 -XX:PermSize ,-XX:PermMaxSize
垃圾收集器:
image.png
如上图,一共有7种作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用,垃圾收集器所处区域表示它是属于新生代收集器还是老年代收集器;
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
1>serial收集器:
serial收集器是最基本发展历史最久的收集器,也是JDK1.3之前,唯一的收集器.serial收集器是一个单线程的收集器,它只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且更重要的是它在进行垃圾收集时,必须暂停其他所有的线程工作,直到它收集结束.
它是虚拟机运行在Client模式下的默认新生代收集器.
优点:与其他收集器相比,serial收集器简单高效,对于限定单个CPU环境,serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率.
image.png
2>ParNew 收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了在收集垃圾时,采用多线程之外,与Serial基本一样.
但是ParNew是运行在server模式下的虚拟机中首选的新生代收集器.因为在单CPU下效率随没有serial高,但是在多CPU环境下效率显著,一般它默然开启的线程数与CPU数量相同.另外一个原因就是,目前只有它可以与CMS(老年代)收集器配合工作.
3>Parallel Scavenge收集器
Parallel Scavenge收集器与ParNew收集器类似,不同的是Parallel Scavenge收集器提供了两个参数
-XX:MaxGCPauseMillis :该参数允许设定一个大于0的毫秒数,表示收集器尽量在垃圾收集时不超过这个时间,但是这个是以牺牲吞吐量和新生代空间内存为代价的.设置的时间越短,那收集的就越频繁.
-XX:GCTimeRatio :参数允许设置一个大于0且小于100的整数.也就是垃圾收集时间占总时间的比率,默认值是99,即允许最大的垃圾回收时间占比为1/100%
由于它与吞吐量关系密切,所以也称作 吞吐量优先收集器.
4>CMS收集器 -XX:+UseConcMarkSweepGC
CMS收集器是一种以获取最短回收停顿时间为目标的收集器.应用于互联网或者B/S系统的服务端上,采用 标记-清除 算法,收集老年代空间的垃圾.
CMS收集器收集分为四个步骤:
1>初始标记,标记GC Roots能够直接关联到的对象,速度很快.
2>并发标记,根据GC Roots 进行tracing追踪
3>重新标记,修正在并发标记过程中,因用户程序继续运作而导致的标记变动的那一部分对象的标记记录.
4>并发清除,将所有标记的对象清除.
CMS收集器的四个过程中,只有初始标记和重新标记的时候需要 stop the world,而耗时最久的并发标记和并发清除,均是并发进行,所以效率极高.
缺点:
1>CMS收集器对CPU资源非常敏感.因为面向并发设计,所以对CPU资源比较敏感,它虽不会让用户应用程序停止,但是它会占用一部分线程资源,而导致用户应用程序变慢.默认收集线程为(CPU数+3)/4.
2>CMS收集器无法收集浮动资源(由于CMS并发清理过程用户线程还在运行着,所以还有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS无法再档次收集中处理他们,只好留待下一次,这样的垃圾叫做浮动垃圾),可能导致出现Concurrent Mode Failure 失败而导致一次serial Old 收集器的 Full GC.
因为在垃圾清除的过程中,用户程序还在进行,那么就需要保留一定的内存来供用户使用,可以通过-XX:CMSInitiatingOccupancyFraction 来设置,JDK1.5中默认当老年代使用了68%时就会触发CMS收集器回收垃圾.而JDK1.6中默认为92%.
如果在CMS运行期间,预留的内存无法满足用户程序的需求时,就会出现 Concurrent Mode Failure失败,这时会启动备案,临时启动Serial Old收集器重新进行老年代的垃圾收集.这样停顿时间就会很长.
3>因为采用的是标记清除法,所以会有碎片化的问题
5>G1收集器
G1收集器是当今收集器最前沿的技术,G1是一款面向服务端应用的垃圾收集器.
G1对内存的划分与传统的不同,G1把内存划分为多个大小相同的Region(默认512K),Region逻辑上连续,物理内存地址不连续.同时每个Region被标记成E、S、O、H,分别表示Eden、Survivor、Old、Humongous。其中E、S属于年轻代,O与H属于老年代。如下图
H表示Humongous。从字面上就可以理解表示大的对象(下面简称H对象)。当分配的对象大于等于Region大小的一半的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝
图片.png
G1收集器过程如下:
1>初始标记:标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一个阶段用户程序并发运行时,能在正确的可用的region中创建对象,这阶段需要停顿线程,但耗时很短.
2>并发标记:是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时时间较长,但可用与用户程序并发进行.
3>最终标记:是为了修正在并发标记期间因用户程序继续进行而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs中,最终标记阶段需要把 Remembered Set Logs中数据合并到 Remembered Set中,这段时间需要停顿线程,但是可并行进行.
4>筛选回收:最后在筛选回收阶段首先对各个region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段是可以和用户线程一起并发执行的,但是因为一次只回收一部分region,时间是可以根据用户控制的,而且停顿用户线程将大幅度提高收集效率.
G1收集器特点:
1>G1可以回收年轻代和老年大的空间,年轻代采用复制算法,老年代采用标记整理算法,解决碎片化问题
2>可预测停顿,能够让用户设定在M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒.
GC日志:
-XX:+PrintGC
允许在每个GC上打印消息。默认情况下,此选项处于禁用状态。
XX:+PrintGCApplicationConcurrentTime
启用打印自上次暂停(例如GC暂停)以来经过的时间。默认情况下,此选项处于禁用状态。
-XX:+PrintGCApplicationStoppedTime
允许打印暂停(例如GC暂停)持续的时间。默认情况下,此选项处于禁用状态。
-XX:+PrintGCDateStamps
启用在每个GC上打印日期戳。默认情况下,此选项处于禁用状态。
PrintGCDetails
允许在每个GC上打印详细消息。默认情况下,此选项处于禁用状态。
-XX:+PrintGCTaskTimeStamps
启用为每个GC工作线程任务打印时间戳。默认情况下,此选项处于禁用状态。
-XX:+PrintGCTimeStamps
启用在每个GC上打印时间戳。默认情况下,此选项处于禁用状态
-Xloggc:filename
设置要将详细GC事件信息重定向到其中进行日志记录的文件。写入此文件的信息类似于-verbose:gc的输出,其中包含自每个记录的事件之前的第一个gc事件以来经过的时间。-Xloggc选项重写-verbose:gc,如果这两个选项都是用同一个java命令给出的。
-XX:+HeapDumpOnOutOfMemoryError
启用在引发Java.lang.OutOfMemoryError异常时使用堆探查器(HPROF)将Java堆转储到当前目录中的文件。可以使用-XX:heap dump path选项显式设置堆转储文件的路径和名称。默认情况下,此选项被禁用,并且在引发OutOfMemoryError异常时不会转储堆。
-XX:HeapDumpPath=path
设置在设置-XX:+HeapDumpOnOutOfMemoryError选项时用于写入堆分析器(HPROF)提供的堆转储的路径和文件名。默认情况下,文件是在当前工作目录中创建的,名为java_pidpid.hprof,其中pid是导致错误的进程的标识符。下面的示例演示如何显式设置默认文件(p表示当前进程标识符):
-XX:HeapDumpPath=./java_pid%p.hprof
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:d:/jvm/jvmgc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/jvm/heapdump.hprof
image.png image.png