垃圾定义:在运行程序中没有任何指针指向的对象
为什么需要GC:如果不进行垃圾回收,内存迟早会被消耗完。垃圾回收除了释放没用的对象,也会清理内存的记录碎片,随着程序的运行,所占内存会越来越多,没有GC就无法保证程序的正常运行
java的GC:java采用的是自动内存管理,无需开发人员手动参与内存的分配与回收,降低内存泄漏与溢出的风险。java堆湿垃圾收集器的工作重点,当然也会包括方法区,但基本不做收集,一般也没有太多的垃圾
垃圾回收器工作时,首先要明确哪些对象需要进行回收。在理论上,判断对象是否可回收一般有两种方法
定义:
优点:
缺点:
java没有采用引用计数法,是因为难以处理循环引用关系。python的垃圾处理机制采用的是引用计数法,没有绝对好与坏的技术,只有场景,只要不怎么出现循环引用,引用计数法的效率一般是优于可达性分析的。
定义:
GC Roots(根集合)是一组必须活跃的引用:
优点:
缺点:
对象的finalization机制
当垃圾回收期发现没有引用指向一个对象,垃圾会输此对象之前,总会先调用这个独享的finalize()方法。主语不要主动调用这个方法,应当交给垃圾回收机制调用。
基于这个机制,java对象在虚拟机中存在三个状态:
判断对象是否可回收,至少需要两次标记:
代码如下:
public class Finalization { public static Finalization finalization; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("over write the finalize"); finalization = this; } public static void main(String[] args) throws InterruptedException { finalization = new Finalization(); // release finalization finalization = null; System.out.println("start first gc"); // start GC System.gc(); // stop the thread, wait the finalizer thread run and watch the status Thread.sleep(10000); if(Objects.nonNull(finalization)){ System.out.println("finalization still alive"); }else { System.out.println("finalization has been dead"); } // release finalization finalization = null; System.out.println("start second gc"); System.gc(); Thread.sleep(10000); if(Objects.nonNull(finalization)){ System.out.println("finalization still alive"); }else { System.out.println("finalization has been dead"); } /** * start first gc * over write the finalize * finalization still alive * start second gc * finalization has been dead */ } }
当完成垃圾表完之后,GC接下来就是执行垃圾回收,释放无用的对象所占用的内存空间
一种非常基础和常见的垃圾收集算法。首先标记出所有需要回收的对象,然后对标记的对象进行统一清除,清空对象所占的内存区域
缺点:
这里的清除,并非是真的质控,只是把需要清楚的对象地址保存到空闲的地址列表中。如果有新的对象需要加载时,判断垃圾的位置空间是否足够,如果够就存放
针对标记-清除早垃圾收集效率与内存碎片方面的缺陷,提出了复制算法。
将内存空间分为两块(运行区域,预留区域),每次只使用其中一块,在垃圾回收时将运行区域的内存中的活着的对象复制到预留区域的内存块中,之后清除运行区域的所有对象,交换两个内存角色,最后完成垃圾回收
优点:
缺点:
如果系统的中垃圾非常多,复制算法需要复制的对象不会太对,或者很少,那么这个算法就会很有优势,比如新生代中,我们都是到大部分对象是朝生夕死,复制算法非常适用这种情况,进本现在虚拟机的新生代都是用的是复制算法
对于老年代的内存空间,大部分对象存活时间都很长,并且空间比较大,复制算法不太适用,因此又提出这个标记-压缩算法,又称为标记-整理算法。
该算法在第一阶段与标记-清除算法一致,但是在压缩过程就是将存活对象压缩到内存的一端,按顺序排放,之后清除边界外的空间。因此它的效果等同于标记-清除算法完成之后,在进行一次内存碎片压缩。标记存活对象将会被整理,并且按照内存地址一次排列,而未被标记的内存会被清理,这样如果给新对象分配内存的时候,jvm仅需要持有一个内存的起始地址即可,这比维护一个空闲列表少了很多开销。
优点:
缺点:
从效率角度考虑,复制算法是最好的,但是浪费一倍的内存。
但是从速度,空间开销,移动对象角度考虑,标记-压缩算法相对比较平滑,但是效率是最低的,比复制算法多了一个标记过程,比标记-清除多了一个整理内存过程。每个算法都有各自的优势以及特点,适用于不同的垃圾回收场景,这个时候按照java堆的分代收集算法应运而生。
目前几乎所有的垃圾收集器都是基于分代收集算法进行回收
年轻代:
区域相对于老年代较少,对象生命周期短,存活率低,回收比较频繁,这种情况下采用复制算法回收整理,速度是最快的。复制算法的效率只跟当前存活对象的大小有关,很适合年轻代回收。而针对内存效率不高,采用两个幸存区(survivor)区来缓解
老年代:
区域较大,对象生命周期长,存活率高,回收不及年轻代频繁。复制算法就不太适合,一般采用标记-清除或者标记-压缩算法。
标记清除,复制,标记压缩算法,在来季回收过程中,应用软件都将处于STW状态,而STW会将所有用户线程挂起,暂停一切正常的工作,如果垃圾回收时间过长,将严重影响用户体验或者西永稳定行,这时增量收集算法诞生了
垃圾收集线程每次只收集一小片的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成。增量收集算法基础仍然是上述基础算法。增量收集算法通过对线程之间冲突的处理,允许牢记收集分阶段完成标记,清理或者复制工作
缺点:垃圾回收过程中,间接性的还执行了应用程序代码,所以可以减少系统停顿时间。但是因为会增加线程切换以及上下文转换消耗,会使得垃圾回收总成本上升,造成系统吞吐量下降。
响应时间与吞吐量总是两个比较矛盾的选择,我们需要结合具体业务场景选择
在相同条件下,堆空间越大,一次GC所用时间越长。为了更好的控制GC所产生的停顿时间,将一个大的内存区域分割为多个小块,根据设定的停顿时间,每次合理收集若干个小区间,从而减少一次GC产生的停顿。
每一个区间都独立使用,独立回收,这样的好处就是可以控制一次回收多少个小区间。内存空间越大,这个算法优势越明显。
没有空闲内存,并且垃圾收集器也无法提供更多的内存
对象不会再被程序用到,但是GC又不能回收这些对象,例如单例模式持有外部对象,如果这个外部对象不再使用,但是这个对象不会被回收,或者一些文件或者数据库连接忘记关闭
GC时间发生过程中,会产生应用程序的停顿。例如可达性分析算法中枚举根节点,会导致所有的java线程停顿,因为分析工作必须在一个快照中完成,否则无法保证数据准确性,所有的GC都有这个事件
程序并非所有的位置都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置成为安全点。一般选取安全点会根据是否有让程序长时间执行的特性为标准,比如方法调用,循环跳转或者异常跳转等。
一般发生GC的时候,会设置一个中断标志,每个线程到达安全点之后会主动轮询这个标志,如果终端标志为真,自己中断挂起。
在一段代码块中,对象的引用关系不会发生变化,在这个区域内任何位置开始GC都是安全的。比如线程进入wait或者bolcked状态。
当线程运行到安全区域是,首先会标识已经进入安全区域,如果发生GC,jvm会护士这个线程
当线程即将离开安全区域是,会检查jvm是否已经完成GC,如果完成,继续运行,否则就会一直等到GC完成信号,才可以继续运行
终结器引用是包内可见,用不到
程序代码中普遍存在的引用赋值,只要强引用还在,垃圾收集器就永远不可能回收掉被引用的对象
强引用可以直接访问目标对象,虚拟机即使抛出内存溢出异常,也不会回收强引用
系统将要发出内存溢出之前,就会对这些对象进行二次回收,如果空间还不够,才会抛出内存溢出异常
软引用一般是用来描述一些还有用,但是非必需的对象,通常用来实现内存敏感的缓存
// 虚拟机参数-Xms10m -Xmx10m public static void main(String[] args) { Object object = new Object(); SoftReference<Object> softReference = new SoftReference<>(object); object = null; // cancel string reference // get object from soft reference System.out.println(softReference.get()); System.out.println("soft reference->" + softReference.get()); System.gc(); System.out.println("soft reference after gc->" + softReference.get()); try { byte[] bytes = new byte[1024 * 1024 * 9]; } catch (Throwable e) { e.printStackTrace(); } finally { // before OOM, the reference will be re-cycle System.out.println("soft reference after make bytes->" + softReference.get()); } /** * java.lang.Object@7c30a502 * soft reference->java.lang.Object@7c30a502 * soft reference after gc->java.lang.Object@7c30a502 * soft reference after make bytes->null * java.lang.OutOfMemoryError: Java heap space * at com.yang.reference.Soft.main(Soft.java:40) */ }
只能生存到下一次垃圾收集之前,无论内存是否足够都会回收
public static void main(String[] args) { Object object = new Object(); WeakReference<Object> weakReference = new WeakReference<>(object); WeakReference<Object> weakReference2 = new WeakReference<>(new Object()); System.out.println("weak reference=> " + weakReference.get()); object = null; System.gc(); System.out.println("weak reference after gc => " + weakReference.get()); System.out.println("weak reference after gc => " + weakReference2.get()); /** * weak reference=> java.lang.Object@1554909b * weak reference after gc => null */ }
一个对象是否有虚引用,对其生存事件不构成任何影响,也无法通过虚引用获取实例。唯一目的就是能够在对象被收集器回收之前得到一个系统通知
新建对象首先会先分配在伊甸园区
年轻代空间不足时,会触发Minor GC,伊甸园和幸存0(from)区的存货对象项使用复制移动到幸存1(to)区,存货对象的年龄加1,
如果对象的年龄满足晋升阈值,最大寿命时,默认是15(4bit),对象会直接从from区移动到老年代,或者幸存区放不下的大对象也会直接进入老年代,还有一种情况就是如果相同年龄的对象占据幸存者1区的一半,name年龄等于或大于这个年龄的对象会直接晋升老年代
Minor GC会触发STW,暂停其他用户新城,等垃圾回收结束之后,用户线程才恢复运行
当老年代空间不足,会先尝试触发Minor GC(取决于垃圾回收器,非绝对),如果空间仍不足,则会触发Full GC或者Major GC,STW的时间会很长。
垃圾回收器按照线程数分,可以分为串行垃圾回收器和并行垃圾回收器
按照工作模式可以分为并发式垃圾处理器与独占式垃圾回收器
按照碎片处理方式,分为压缩式垃圾回收器和非压缩式垃圾回收器
按照工作内存区间封你为年轻代垃圾回收器和老年代垃圾回收器。
这三个与CAP理论一样,不能同时做少,只是随着技术的成熟逐步在变好,一般我们主要抓住:吞吐量以及暂停时间
串行回收器:Serial,Serial Old
并行垃圾回收器:ParNew, Paraller Scavenge,Paraller Old
并发垃圾回收器:CMS,G1
年轻代垃圾收集器:Serial, ParaNew, Parallel Scavenge
老年代垃圾收集器:Serial Old, Parallel Old, CMS
整堆收集器:G1
垃圾回收器之间的组合关系:
一个单线程的收集器(只有一个线程在运行,并不是只有一条线程去完成),在进行垃圾收集时,必须暂停其他所有工作线程。这种单线程的垃圾收集器在目前的java web很少用到,因为现在都不再是单核服务器了。使用-XX:UseSerialGC启用该垃圾回收器,但是根据组合关系,秒人老年代也会启用Serial Old GC
也就是Parallel New收集器,采用的就是并行回收年轻代的垃圾,除了并行其余与Serial GC没有区别。使用 -XX:UseParNewGc手动指定使用ParNewGC收集年轻代,同时可以使用 -XX:ParallelGCThreads限制线程数,默认开启与cpu相同的线程数
采用复制算法,并行回收,也是用STW机制,该处理器主要目标是达到一个可控制吞吐量的垃圾处理器,具有自适应调节策略。手动开启 -XX:UseParallelGC,根据上面的组合关系,默认也会开始老年代使用Parallel Old GC
-XX:ParallelGcTHreads设置年轻代并行收集器的线程数,默认时cpu的核数,如果超过,则默认值为 3 + (5 * cup核数/8)
-XX:MaxGcPauseMilles设置垃圾收集器的最大停顿时间,设置之后,Parallel Scavenge收集器将会在工作时调整java对的代销或者其他参数,这个值设置需要谨慎,如果设置过小,堆调整的很小,就会影响吞吐量
-XX:GCTimeRatio设置垃圾收集时间与总时间的占比,公式为(1/(n+1)),默认值时99,也就是垃圾回收时间不超过1%
-XX:UseAdaptiveSizePolicy设置收集器具备自适应调节策略,这种模式下,jvm会自动调节年轻代大小,伊甸园区与幸存去的比例以及今生老年代的年龄值大小。手动调有比较困难场景,建议设置这个参数以及垃圾回收时间与总时间的占比
第一款真正意义上的并发收集器,实现了用户线程与垃圾收集线程同时工作。
CMS关注点就是尽可能缩短垃圾收集时用户线程停顿时间,适合多交互的场景,采用的时标记清除算法。
主要分为四个节点:
CMS在比较耗费时间的并发标记以及并发请理阶段并没有挂起用户线程,因此整体的回收是低停顿的。如果早CMS运行期间预留的内存无法满足程序需要,就会执行后备方案,临时启用Serial Old GC进行老年代垃圾收集
优点:
缺点:
-XX:+UseConcMarkSweepGC:使用CMS收集器,年轻代就会启用ParNew,备用老年代就是Serial Old
-XX:CMSInitiatingOccupanyFraction:设置堆内存的使用阈值,因为是并发执行,因此默认阈值时68,当老年代使用空间达到68%之后就会执行垃圾回收,如果阈值设置过大,有可能导致垃圾回收时,空间不足,使用Serial Old进行回收
-XX:+UseCMSCompactAtFullCollection:指定垃圾回收完成之后进行碎片整理,但是会导致停顿时间百年城
-XX:CMSFullGCsBeforeCompaction:设置多少此Full GC之后急性压缩整理
-XX:ParallelCMSThreads:设置CMS线程数量,默认是 年轻代垃圾收集器的线程树做运算(ParallelGCThreads+3)/4
如果最小化的使用内存与并行开销使用Serial GC;
如果最大化应用程序吞吐量,使用Parallel GC;
如果最小化GC的停顿时间,使用CMS
为适应不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。G1诞生,目标就是在延迟可控的前提下,尽可能获得更高的吞吐量。
G1是一个并行回收器,他采用的就是分区算法,将堆内存分割为很多不相关的区域(Region),使用不同的Region来表示Eden,幸存者0区(from),幸存者1区(to),老年代等
G1 GC会尽量避免在整个java堆中进行全区域垃圾收集,G1会记录每个区域的空间大小,以及垃圾数量,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域
G1是JDK9之后默认的垃圾回收器,主要面对的就是服务端应用,针对配备多核以及大容量内存的机器,以极高概率满足GC停顿时间的同时,兼顾高吞吐量的特征。
并行与并发:
分代收集:
空间整合
-XX:+UseG1GC 手动指定使用G1,但JDK9之后就是默认的
-XX:MaxGCPauseMillis 设值期望达到的最大GC停顿时间(默认时200ms),jvm会尽力实现,但是不保证
-XX:ParallelGCThreads 设置STW的工作线程,最多设置8个
-XX:ConcGCThreads 设置并发标记的线程数 设置并发标记的线程数。一般设置为(parallelDCThreads)的1/4左右
-XX:InitiatingHeapOccupancyPercent 设置复查并发GC周期的java堆占用率预支,超过此值,就会触发GC,默认是45
G1设计原则就是简化jvm的性能调优,一般只需要开启G1垃圾收集器,设置堆的最大内存,设置最大的停顿时间就可以
具有大内存,多处理器的机器(一般需要堆的大小超过6G),G1在GC时,如果GC线程处理缓慢,系统会调用应用程序线程帮忙加速垃圾回收过程,其他垃圾收集器都只会使用内置的JVM线程
在G1中最突出的问题就是一个区间的对象可能被其他区间的对象引用,在进行标记阶段,如果不做任何处理,就需要扫描整个java堆才行,这样会降低GC的效率,其他垃圾回收器也存在这个问题,但是G1更为突出。jvm一般就是采用Remembered Set来比秒全局扫描。每一个Region(G1)或者内存空间(其他垃圾收集器)都有一个Remembered Set来避免全局扫描,每次有一个引用(reference)类型加入region(或者内存区域时)会暂时中断操作,判断将要写入的引用关系的对象是否与当前对象在一个region(内存空间),如果有就将引用信息记录到当前remember Set中,这样在GC Roots枚举的时候就会加入RememberSet,保证不进行全局扫描也不会遗漏。
伊甸园区的内存用尽,就会开始年轻代回收过程,是一个并行的独占式收集器,会暂停所有的用户线程,年轻代垃圾回收只会回收伊甸园区与幸存者区域,年轻代回收过程回收集包含全部的伊甸园区与幸存者区
当堆内存使用达到一定的阈值(默认45),开始老年代并发标记过程
如果上面的方式无法正常工作,就会STW,进行单线程,独占式,高强度的GC。就是一种GC失败之后的保护机制。类似于CMS使用Serial Old GC最补偿是一样的
一般会触发的原因就是没有足够的空间存储晋升对象或者并发处理过程中内存耗尽
在进行选择垃圾回收器是一般原则就是:
jvm参数的官方网站:https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html
原文:https://www.cnblogs.com/yangshixiong/p/14078248.html