为什么启动的时候热机的效率要比冷机高呢?
因为jvm有解释器和jit即时编译器,在刚开始启动的时候,没有热点代码,因此都是解释执行,这样的响应速度虽然快但是执行速度慢了,在高并发的情况下会导致,任务堆积过多产生进而导致一些列问题。
一、垃圾回收采用的是标记清除算法,分为两个阶段,垃圾的标记,垃圾的清除下面详细的讲解两个过程
标记-清除
关于垃圾回收标记阶段的算法,采用的是可达性分析,通过从roots对象出发查找如果没有与在某个关联,则属于可回收的对象
那些事roots对象呢?
1、方法栈局部变量表上面的对象
2、本地方法栈上的对象
3、类静态变量应用的对象
4、被synchronized持有的对象
5、常量池上面的对象,string
6、类加载以及其他的临时对象比如老年代,永久代上的对象等
可达性算法解决了循环引用的问题,可以有效的标记垃圾对对象,但是由于需要查找,所以需要冻结形成快照也就是stop the world 导致性能下降。
被标记的对象一定就会被回收吗?
不一定,因为此时代表该对象不可触达,但是在被垃圾回收之前可能会复活,所以会不一定被回收
finalize方法的用途?
垃圾回收的过程,当一个对象被标记成为垃圾的之后会被加入到F-Queue的一个队列,稍后一个优先级比较低的回收线程finall会调用一次该方法但是调用时间不定,这也是在被回收之前唯一次自我拯救或者执行最后操作善后的机会,如果调用之后该对象依旧没有和引用链上的任何对象建立链接,则会被回收。
从上述过程可以看出,实际上对象被回收有两次’标记‘,一次是被判定不可达对象加入F-Qeue队列,另一次就是在被回收线程调用再次判断时候依旧是不可达对象,两次都是的话则被回收。
finalize方法的几个特点
1、仅会被调用一次
2、执行时间不定
3、如果重写之后加入了耗时或者阻塞的代码会导致,垃圾回收线程阻塞性能下降,但是jvm的机制会保证有一定的超时时间,避免破坏回收线程的运行
4、可以在方法内拯救自己,也就是与roots系列对象建立关联
在finalize方法调用的时候可能有三种状态 可触达、可复活、不可触发
注意:
1、当一个对象自我通过finalize方法自救之后再成成为垃圾的话,不会在调用finalize方法。因为如果这样会导致一个垃圾对象永远都不可被回收!
2、标记算法标记的是有效的对象,因为无效的无法从gc roots被找到,实际上就是无法被标记的才是垃圾,标记存放在对象的header上
回收
标记清除:
描述:在jvm内存不足,触发垃圾回收,通过可达性算法标记了所有有效的对象,然后进入清楚阶段,清除阶段会暂停所有的线程stw,开始清理没被标记的对象,但是并不是置空而是将对应的内存加入到空闲列表中。分配对象的时候会从空闲列表中选者并覆盖所需空间。
这个过程暴露了什么问题?
1、这个过程效率不高
2、会形成内存碎片导致,后期无法分配大对象二oom
3、会造成stw
复制整理:
描述:首先复制算法需要两块一样大的空间,但是只能利用其中一块,当需要垃圾回收的时候会把对象拷贝到另一个空闲区域并处理相关的指针指向,整个区域处理完毕之后交换两个分区的角色。新生代的s0和s1区就是采用这样的方式。
特点:整体空间利用率只有50%,但是由于没有标记查找的过程所以效率高,同时经过整理之后空间的碎片化也得到了解决。但是当垃圾占比很小的时候,效率会下降,极端情况下复制之后可能之后极少的垃圾被清除。
结论:适合垃圾占地很大的情况使用
标记压缩算法
描述:首先是采用垃圾标记算法,标记垃圾对象,然后将垃圾对象移动到内存空间的另一端,最后确定内存边界,将垃圾部分的内存清除。存在编辑移动清除指针修改等阶段,因此效率低。
特点;效率低,但是内容空间的利用率比较高,也避免了内存碎片化,但是可能导致较长的stw,一般不会再新生代里使用。
分代算法
以上的三种算法,各有优缺点,因此jvm的垃圾收集器是通过结合分代模型的特点选择适合的算法,像是GMS算法就是在新生代使用复制算法,老年代使用标记清除或者标记整理算法结合标记标记清除,这样可以实现效率的最大化。
增量收集算法
由于垃圾回收存在stw,所以垃圾回收实际上采用的是增量收集算法,实际上就是执行一个区域之后恢复应用程序,然后返回,这样就可以降低stw。但是由于上下文切换效率比较低,系统的吞吐量会降低。
分区算法
通过将不存划分成为不同的小区域,在限定的stw内按照计划去进行回收这样可以有效的控制延迟,保证系统的stw在一个合理范围内,G1就是采用这样的算法
关于手动gc的处罚说明
System.gc()与Runtime.getRuntime().gc()这样些方法可以给jvm发送信号,准备gc但是这个并不是同步的,gc的时间依旧是由jvm来决定。
通过System.finalization()可以同步的触发gc
如何从代码角度去判断某些对象是不是可以被回收的呢?
如果是方法调用出栈之后则该方法内部声明没有外泄的对象可以被回收
如果将一个对象的强引用断开则可以被回收
如果作用域范围之后没有关联的,之后槽位被占用的话也可以被回收,一般出现在{}相关的地方
谈一谈java里面的四种引用类型:
强引用:显示的引用,这个都在编程中最为常见,当对象与gc roots系列对象有关联的话,则不可被回收,也是因为这个原因可能会导致内存泄漏(无法价值也无法被回收的对象,占据一部分内存)
软引用:相对较弱的引用,当jvm内存不足的时候就会开始gc,软引用存在也坑会被回收。这样的场景通才在对一些缓存资源上使用,因为缓存与系统oom相比要没有那么重要
弱引用:更弱一些的引用,即使内存还没有满的情况下,依旧会被回收。因此它的生命周期之后到下一次gc的时候
应用weakHashMap的使用
虚引用:最弱的引用关系,无法使用虚引用去使用一个对象,其存在的唯一作用就是可以用于追踪垃圾回收的过程,在垃圾回收的时候会被加入到队列里面,可以实现系统垃圾回收的通知而已。
什么是内存溢出OOM?产生的原因是那些方面呢?
内存溢出简单的来说就是内存不够支持程序的正常运行了且经过fullgc之后依旧不能满足的情况下则会抛出OOM异常,成因可能有以下几方面
1、jvm的空间设置的过小导致,程序分配的时候无法拥有足够的空间
2、程序长时间运行,经过垃圾回收之后存在内存碎片,不连续的空间无法容纳一个大对象,如果是一个超大对象直接超过了堆的话,则不会触发gc
3、短时间内大流量的涌入,导致程序,垃圾回收对象的声明周期重叠,也就是消亡时间相近的情况下,垃圾回收与内存分配的平衡被打破,导致内存溢出
4、程序设计不合理,存在某些声明周期过长的对象,经过几次回收之后放入老年代,最后老年代无法容纳的情况下导致内存溢出
5、其他情况
什么是内存泄漏?那些情况可能带回内存泄漏呢?
内存泄漏,严格意义上来说就是对象已经不需要了,但是由于存在gc roots的关联,这样导致无法被回收,称之为内存泄漏,可能的远影有以下几个方面。
1、声明周期较长的对象引用了一些声明周期较短的对象,没有及时断开。像是单例对象内部使用了一些临时的对象就会导致,被回收失败内存泄漏
2、对于一些资源的使用之后没有及时的close导致被占用但是又没有使用的意义了就可能可能会导致内存泄漏,像是一些无效链接,网络操作,文件读写等等,流的操作。都可能会导致。
任何时候都可以进行垃圾回收吗?还是需要在某些条件才可以垃圾回收
不实在任何时候都可以进行垃圾回收,首先有两个基本的概念,那就是安全点和安全区域,之后当属于两个之一的时候才可以进行垃圾回收。
安全点:某个程序可以响应jvm stw中断线程的位置。
安全区域:实质某一段代码片段,这个片段期间引用关系不会发生改变。
也就是说stw发生的前提条件就是可以被所有线程被刮起同时不会产生问题的临界时候。
垃圾回收器的种类:
1、可以按照垃圾回收器是否是多线程,分为单线程和多线程垃圾回收器
2、按照是否与用户线程同时进行,很为并行垃圾回收器和串行垃圾回收器
3、是否按照区域划分,分为按块回收或者全局回收
4、按不同代来划分,新生代,老年代的垃圾回收器等。
4、是否进行碎片化整理,分为整理或者不整理的垃圾回收器。
如何衡量垃圾回收器的执行效率:
1、暂停时间:stw的时间
2、吞吐量:垃圾回收时间占总时间的比例
3、内存的大小:位置内存空间堆的大小
4、执行的频率:单位时间内垃圾回收的次数
这些指标比不可能同时达到最优,就像acp理论一样,所以不同的垃圾回收器实际上是为了尽可能的使得垃圾回收效率更高
最重要的就是吞吐量、暂停时间
目前的垃圾回收器最求的基本上都是在可控的暂停时间内,尽量提高吞吐量,比如说是限定每次垃圾回收的时间不超多10ms,这样可以有效的减少用户线程的挂起时长,提高响应的效率。G1,ZGC等垃圾回收器都是这样子
经典的垃圾回收器:
![](https://i-blog.csdnimg.cn/blog_migrate/6de351dea138c48e7231f79c91ac170f.png)
不同的jdk版本有不同的垃圾回收器,目前jdk1.8中拥有G1垃圾回收器,但是不是默认的垃圾回收器。最求的就是低延迟,高吞吐量。垃圾回收器的一个过程主要是 串行---并行----并发的一个主流过程,其中重点的几个垃圾回收器就是
CMS(第一款实现并发的垃圾回收器,但是使用的时候不能再内存满的时候,因此可能需要配合Paraller Old GC进行老年代的垃圾回收)、G1、ParNew、paraller Scavenge 、Paraller Old
垃圾回收器可以组合使用但是并不是任意组合的,组合方式见下图
![](https://i-blog.csdnimg.cn/blog_migrate/de785d93942646036103243c09a98aee.png)
其中G1是一个可以统一回收新生代和老年代的垃圾回收器
组合关系如下:
![](https://i-blog.csdnimg.cn/blog_migrate/3b47621034ae8cac360407e3d9e2fdcd.png)
在不同的版本可以组合放方式是不一样的
早期:图中的虚线也是可以组合的例如,serial与CMS parNew与Serrial Old等
中期:移除部分了串行于并行的垃圾回收器的组合方式,引入了新的G1垃圾回收器(1.7引入,jdk9是默认的GC)1.8的默认垃圾回收器是Paraller Scavenge于Paraller Old GC的组合
后期:移除CMS垃圾回收器 jdk14正式移除
CMS于Serial Old是主备模式,当内存要满的时候Serial Old开始工作执行Full gc操作
如何查看当前的垃圾回收器呢?
![](https://i-blog.csdnimg.cn/blog_migrate/f10afb2261628a6d5ed91fd204da0f26.png)
1、可以在执行参数上加 -XX:+PrintCommandLineFlags 程序运行的时候会答应相关的信息
可以通过Jinfo -flag 垃圾回收器参数 进程ID(Jps获取) 来获取时候启动了该垃圾回收器
![](https://i-blog.csdnimg.cn/blog_migrate/3f6266e74fda732c5c05b20de0f207a0.png)
在client模式上seriel gc于seriel Old gc 在单核心cpu或者资源比较小的场景上(嵌入式的场景)可以使用
如何设置GC呢?
可以在参数上添加- XX:+UseSerialGC 会启动一组GC SerialGC与SerialOldGC
parNew是Serial的多线程版本,实现了并行回收,但是也采用了复制整理的算法。并行实际上就是多个垃圾回收线程,但是用户线程依旧会被停止 stw
垃圾回收器的使用结合分代模型的特点,比如新生代,垃圾回收频繁可能并行垃圾回收器会更加高效,二老年代的垃圾回收比较少串行的垃圾回收器(serial Old gc)可以避免多线程切换效果也不是很差。
ParNew垃圾回收器只能配合Serial Old GC或者CMS
如果设置的垃圾回收器是多线程的则可以设置其线程数 - XX:ParallerGCThreads 2 不设置默认就是CPU的核心数
新生代的垃圾回收器基本都是采用复制算法Serial、ParNew、Paraller Scavenge
Paraller Old GC采用的是标记压缩的算法
注意:Paraller垃圾回收器 拥有的可调参数要多一些
-XX:MaxGCPauseMillis 最大的停顿时间,实际上就是控制stw的时间,单位是毫秒值
-XX:GCTimeRatio 垃圾回收时间占总时间的百分比,也就是描述的吞吐量
两个参数实际上有一些冲突,相互影响,如果最大停顿时间设置的短则可能会导致吞吐量下降,参数并不是一定保证的
UseAdaptiveSizePolicy 参数是设置自适应策略,默认是开启的,开启之后会自动调整堆的新生代老年代,伊甸园区的大小,以及进入老年代的参数设置等等。可以使得上面的两个参数能够被最大化的实现。
cms 是第一款并发的垃圾回收器,实现的是标记清除算法,该垃圾回收器主打的是低延迟,也就是短暂的stw。在bs系统的服务端也有价值,在客户端swing程序上契合,以及安卓程序程序等
cms执行的主要过程分为四个阶段:初始化标记(stw)、并发标记、重新标记(stw 修成并发的误差)、并发清除,重制线程。
虽然存在stw但是两个阶段做的事情并不复杂,因此耗时非常短,也就实现了低延迟的目标,并且采用的多线程回收,因此效率也是比较高的
但是由于并发标记、并发清除阶段,用户线程也在运行,如果此时占用的内存过高的话,可能会导致回收失败,触发Serial Old GC来执行,这样又回导致stw变长,如果想要避免内存过高的情况,因此需要提前启动垃圾回收,而不是等待空间不够的时候才执行。
由于采用的是标记-清除的方式,会导致内存碎片化(碎片化,在创建大对象的时候可能会导致Full GC),再分配对象的时候采用空闲列表
![](https://i-blog.csdnimg.cn/blog_migrate/3cf7856990ae8b727376105aba6726cf.png)
一个案例:
又一次我们线上出现了一个问题,就是服务响应超时,并且每个接口都是超时的状态,然后通过arthas分析线上的内存占用情况,发现对内存处于很高的一个状态,并且存在一些大对象,因此导致系统无响应的原因可能是垃圾回收器回收时候产生的stw造成的,然后我通过Jinfo -flag xxx发现当前jvm采用的是CMS的垃圾回收器,因此CMS有一个特点就是并发垃圾回收,在回收的时候可能会产生浮动垃圾,并且采用的是标记清除算法也存在空间碎片化的问题,这个时候恰巧由于代码编写存在不规范的情况所以导致,回收失败触发了Serial Old GC 低效率的回收垃圾,这个就是产生在本次故障的原因。
然后我们立马启动了新的服务器实例将垃圾回收器设置成G1, 并通过nginx配置将流量转移到新的服务器上,然后等待就服务器完成相关处理之后对参数进行设置重新上线,并在后面的迭代版本中修改响应的代码从源头上解决一些,滥用长生命周期对象以及创建大数组做操作的问题代码。
如何开启CMS
1、-XX:UseConcMarkSweepGC 开启CMS的垃圾回收器,这个时候新生代回采用ParNew 老年代CMS+Serial Old GC的组合进行垃圾回收。
2、- XX:CMSInitiatingOccupanyFraction 设置垃圾回收的内存阈值,达到这个值则开始进行垃圾回收,而不是在内部不够的情况下回收 ,默认是92%,如果增长很快的时候则设置的小一点
3、-XX:+UseCMSCompactAtFullCollection 用于开启是否full gc之后需要进行内存碎片化压缩整理。
4、-XX:CMSFullGCBeforeCompaction 设置多少次Full GC之后才执行一次内存压缩
5、 -XX:RarallerCMSThreads 设置CMS的线程数
![](https://i-blog.csdnimg.cn/blog_migrate/78c185557aa1564d10e130aa11f22cf4.png)
CMS在JDK9的时候被标记过时的,在jDK14开始已经被移除了