JVM的分区+查看GC对象是否存活+3种GC算法+7种垃圾收集器+如何减少GC次数
一、JVM的分区:
1、程序计数器(私有)
程序计数器是一块较小的内存分区,你可以把它看做当前线程所执行的字节码的指示器。
在虚拟机的概念模型里,字节码解释器工作时,就是通过改变计数器的值来选择下一条需要执行的字节码指令。
程序技术器为线程私有,每个线程都有它们各自的程序计数器,这样再多线程的情况下,线程之间的来回切换,也能正确找到上次切换时执行的位置。
如果线程正在执行的是一个Java方法,那么程序计数器记录的是当前线程正在执行的字节码指令的地址;如果线程正在执行的是一个native方法,则计数器值为空。
此内存区域是唯一一个Java虚拟机规范中没有规定任何OutOfMemoryError(OOM)情况的区域。
2、Java虚拟机栈(私有)
虚拟机栈也为线程私有的,它的生命周期与线程相同;
虚拟机栈可以看做是Java方法执行的内存模型:每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个Java方法从调用到执行完的过程,就对应着一个栈帧从虚拟机栈入栈到出栈的过程;方法调用时,会创建栈帧在栈中,调用完是程序自动出栈释放,而不是gc释放。
局部变量表中存放了编译期可知的基本数据类型、对象引用、returnAddress类型(指向了一条字节码指令的地址);
在虚拟机栈中可能会出现两种异常:StackOverflowError和OutOfMemory
StackOverflowError:如果线程请求的栈深度大于当前虚拟机所允许的深度,会抛出该异常;
OutOfMemory:如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,会抛出该异常;
3、本地方法栈(私有)
本地方法栈类似与虚拟机栈,它们不同之处在于,虚拟机栈是为虚拟机执行的Java方法服务,而本地方法栈是为虚拟机使用到的Native方法服务;
在HotSpot虚拟机中直接把本地方法栈和虚拟机栈合二为一;
在本地方法栈可能会出现两种异常:StackOverflowError和OutOfMemory
4、Java堆(共享)
Java堆是被所有线程共享的一块区域,它也是Java虚拟机管理的内存中最大的一块,它在虚拟机启动时创建;
Java堆唯一的目的就是存放对象实例,几乎所有的对象实例的都在这里分配内存;
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为GC堆;
Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,在实现时既可以是固定大小也可以是可扩展的,如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemory异常;
5、方法区(共享)
方法区也是内存共享的一块区域,它用于存放已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据;
在HotSpot虚拟机中,通常把方法区称之为永久代,本质上两者并不相同,只是HotSpot虚拟机的设计团队使用永久代来实现方法区;
方法区中,垃圾收集比较少见,但并不是不进行GC,这个区域的回收目标主要是针对常量池的回收和对类型的卸载
方法区类似于Java堆,不要连续的内存和可以选择固定大小或者可扩展。它还可以选择不实现垃圾收集;
当方法区无法满足内存分配需求时,会抛出OutOfMemory异常;
方法区中还存在一个运行时常量池(字符串常量池也在这个里面),常量池用于存放编译期生成的各种字面量和符号引用,它具有动态性,不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中;
二、堆对象的分配:
1)大多数情况下,对象在新生代 eden 区中分配,当 Eden 区中没有足够的内存空间进行分配时,虚拟机将发起一次 minor GC {minor gc:发生在新生代的垃圾收集动作,非常频繁,一般回收速度也比较快 full gc:发生在老年代的 gc}。虚拟机给每一个对象定义一个对象年龄计数器,若对象在 eden 出生并经过第一次 minor gc 后仍然存活,并且能被 survivor 容纳的话,将被移到 survivor 空间中,并且对象年龄设为1.对象在 survivor 中每熬过一次 minor gc,年龄就+1,当他年龄达到一定程度(默认为 15), 就会晋升到老年代。
2)大对象直接进入老年代
3)长期存活的对象将进入老年代
4)若在 survivor 空间中相同年龄所有对象大小的总和>survivor空间的一半,则年龄>=该年龄的对象直接进入老年代,无需等到MaxTeuringThreshold(默认为15)中的要求。
三、查看GC对象是否存活的方法:
1)引用计数法
基本思想:给对象中添加要给引用计数器,每当一个地方引用时,计数器值+1,当引用失效时,计数器值-1,任何时刻计数器为0的对象就不可能再被使用。
缺点:很难解决对象之间循环引用的问题。
2)可达性分析法
基本思想:通过一系列的称为“GC roots”的对象作为起始点,从这些节点,开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC root 没有任何引用链相连(用图论的话来说,就是从 GC roots 到这个对象不可达),则证明此对象是不可用的。
若对象在进行可达性分析后发现没有与 GC roots 相连接的引用链,那么他将会被第一次标记并进行一次筛选,筛选的条件是该对象是否有必要执行 finalize()方法,当对象没有重写
finalize()方法或者 finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没必要执行。
若该对象被判定为有必要执行 finalize 方法,则这个对象会被放在一个 F-Queue 队列,
finalize 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-queue 中的对象进行第二次小规模的标记,若对象要在 finalize 中成功拯救自己—只要重新与引用链上的任何一个对象建立关联即可(即可以重写finalize()方法来实现,比如可以将自己赋值给某个类变量或者对象的成员变量),那么在第二次标记时他们将会被移出“即将回收”集合。
任何一个对象的 finalize()方法都只会被系统调用一次。
可作为 GC roots 的对象
1)java 虚拟机栈(栈帧中的本地变量表)中引用的对象
2)方法区中类的静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中 JNI 引用的对象
引用强度:强引用>软引用>弱引用>虚引用
1)强引用:类似Object obj = new Object()的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2)软引用:用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。SoftReference类实现软引用。
3)弱引用:用来描述非必须对象,但其强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference类实现弱引用。
4)虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。PhantomReference类实现虚引用。
内存分配规则:
1.对象优先分配在Eden区,如果Eden区没有足够的空间,执行一次Minor GC.
2.大对象直接进入老年代。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC年龄+1,达到16,进入老年代。
4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5.空间分配担保。
四、3种GC算法:
标记-清除算法(老年代):分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记对象。
缺点:1)产生大量不连续的内存碎片 2)标记和清除效率都不高
复制算法(新生代):它将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,则就将还存活的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。使得每次都是对整个半区进行内存回收。
优点:1)不会出现内存碎片。2)只需移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:1)将内存缩小为原来的一半。2)在对象存活率较高时会进行较多复制操作,效率较低。
商业虚拟机的分配担保机制:将内存分为一块较大的 eden 空间和两块较小的 survivor 空间,默认比例是 8:1:1,即每次新生代中可用内存空间为整个新生代容量的 90%,每次使用 eden 和其中一个 survivour。当回收时,将 eden 和 survivor 中还存活的对象一次性复制到另外一块 survivor 上,最后清理掉 eden 和刚才用过的 survivor,若另外一块 survivor 空间没有足够内存空间存放上次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
标记-清理算法(老年代): 标记过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清除,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
五、7种垃圾收集器,前 3 个是新生代,后 3 个是老年代:
并行收集和并发收集的区别:
(1)并行(Parallel)
指多个垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
如ParNew、Parallel Scavenge、Parallel Old;
(2)并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
如CMS、G1(也有并行);
(1) serial (复制算法):单线程(单线程的意义不仅仅说明它会使用一个 cpu或一条垃圾收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集的时候,必须暂停其他所有工作线程,直到他收集结束)。
应用场景:
对于运行在 client 模式下的虚拟机来说是个很好的选择。
优点:
1)简单高效(与其他收集器的单线程相比);
2)对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
3)在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的。
(2) parNew (复制算法):serial 收集器的多线程版本,是许多运行在 server 模式下的虚拟机首选的新生代收集器。
应用场景:
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
(3) parallel scaverge(复制算法):其他与ParNew类似,特别之处在于:CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而其目标是达到一个可控制的吞吐量,适合在后台运算,没有太多的交互。
应用场景:
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
(4) serial old(标记-清理):serial 的老年代版本,单线程,
应用场景:
主要用于Client模式;而在Server模式有两大用途:
(A)、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);
(B)、作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解);
(5) parallel old(标记-清理):parallel scaverge 老年代的版本,多线程
应用场景:
JDK1.6及之后用来代替老年代的Serial Old收集器;特别是在Server模式,多CPU的情况下;这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合。
(6) cms(标记-清除) :一种以获取最短回收停顿时间为目标的收集器 “标记-清除”,有 4 个过程:
1)初始标记(仅标记一下GC Roots能直接关联到的对象;速度很快;但需要"Stop The World";)
2)并发标记(进行GC Roots Tracing的过程;刚才产生的集合中标记出存活对象;应用程序也在运行;并不能保证可以标记出所有的存活对象;)
3)重新标记(为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;采用多线程并行执行来提升效率;)
4)并发清除 (回收所有的垃圾对象)。
优点:并发收集,低停顿;
缺点:
1)不能处理浮动垃圾,由于 cms 并发清除阶段,用户线程还在继续执行,伴随程序进行,还有新的垃圾产生,这一部分垃圾发生在标记之后,cms 无法在当次收集时处理他们,只能留到下一次gc。可能出现"Concurrent Mode Failure"失败。这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生。
2)对 cpu 资源敏感。并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。cms 默认启动的回收线程数是(cpu 数量+3)/4。当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
3)产生大量内存碎片 ,大对象分配困难,需要提前触发另一次Full GC动作。
(7)G1:是一款面向服务端应用的商用垃圾收集器。具备四个特点:
1)并行与并发:能充分利用多CPU、多核环境下的硬件优势;可以使用多个CPU并行来缩短"Stop The World"停顿时间;也可以并发让垃圾收集与用户程序同时进行。
2)分代收集:能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;能够采用不同方式处理不同时期的对象;虽然保留分代概念,但Java堆的内存布局有很大差别;将整个堆划分为多个大小相等的独立区域(Region);新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合。
3)空间整合,不产生碎片:从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法;都不会产生内存碎片,有利于长时间运行,不会提前触发一次GC。
4)可预测的停顿:低停顿的同时实现高吞吐量;G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。
六、如何减少GC出现的次数:
1.对象不用时显示置null。
2.少用System.gc()。
3.尽量少用静态变量。
4.尽量使用StringBuffer,而不用String累加字符串。
5.分散对象创建或删除的时间。
6.少用finalize函数。
7.如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起OOM。
8.能用基本类型就不用基本类型封装类。
9.增大-Xmx的值。