JVM垃圾回收
1.内存泄漏与内存溢出
1.1什么是内存泄漏?
内存空间使用完毕之后未回收,导致一直占据着该部分内存,直到程序运行结束。
常见的内存泄露造成原因:
1.单例模式造成
由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。
-
创建非静态内部类或者匿名内部类的对象的可能会造成内存泄漏,当外部类结束,内部类还持有外部类的引用,就可能会导致内存泄漏;
-
资源未关闭
-
使用集合容器
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。
在退出程序之前,应该将集合里的东西clear,然后置为null,再退出程序
1.2什么是内存溢出?
指程序在申请内存时,没有足够的内存空间供其使用。(就是内存不够用了)
1.3常见的OOM
java堆溢出(对象数量达到超过堆的最大容量)
虚拟机栈溢出:在单线程情况下,虚拟机栈容量太小或者定义了大量的本地变量
**如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常 **
如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常
本地方法栈溢出: 在多线程下,大量创建新线程,会抛出OOM,每个线程的栈分配的空间越 大,越容易产生
运行时常量池溢出: 代码在运行时创建了大量的常量,超出了常量池的上限
方法区溢出: 在运行时,ClassLoader动态加载了大量的Class信息,超出了方法区的上限。
1.4两者的区别?
内存泄漏的堆积最终会导致内存溢出
内存溢出就是你要的内存空间超过了系统实际分配给你的内存空间
内存泄漏就是指你向系统申请内存进行使用, 使用完却不归还, 结果这块内存你 自己也不能再访问, 系统也不能把他分配给别的程序;
内存泄漏影响相对来说没oom大,可以通过临时方案:重启解决来解决(治标不治本)。
2.内存回收机制
2.1概述
在Java中,我们不用显示的去释放一个对象的内存,而是由虚拟机自动执行的; 在JVM中,有一个垃圾回收线程,他是低优先级的,在正常情况下是不会执行的,只 有在虚拟机空闲或者当前堆内存不足的情况下,才会触发执行,扫描那些没有被任 何引用的对象,并对他们进行回收;
2.2垃圾回收的时机
原理: 对于GC来说,当创建对象时,GC就开始监控这个对象的地址,大小以及使用 情况了;
1.显示调用System.gc(),只是建议而非一定。
2.JVM垃圾回收机制决定
finalize() 方法:当JVM 确定不再有指向该对象的引用时,垃圾收集器在对象 上调用该方法。finalize() 方法有点类似对象生命周期的临终方法,JVM 调用该方法,表示该对象即 将“死亡”,之后就可以回收该对象了
2.3判断一个对象是否"死亡"/是否可以回收
引用计数算法:
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效的时候,计数器减1,当计数器等于0的时候,这个对象就是不能再被使用的,可以回收,
引用计数器算法实现简单,效率也很高,但是有一个缺点:不能解决对象之 间互相引用的问题 , 在主流的JVM中没有选用引用计数法来管理内存
可达性分析算法:
通过一系列的"GC Roots"对象作为起点,从这些节点开始往下搜索,搜索 走过的路径称为"GC Roots 引用链",当一个对象到GC Roots没有任何引用链连接时,这个对象就是不可用的。
2.4java中的引用类型?(了解)
强引用:指在代码中普遍存在的,如Object obj = new Object(); 这类引用,只 要强引用还在,垃圾收集器永远不会回收被引用的对象;
软引用:用来描述一些还有用但是并不是很需要的对象,对于软引用关联着的 对象,在系统将要发生内存溢出异常之前(OOM)会被回收。
弱引用:描述非必需对象,被弱引用关联着的对象只能生存到下一次GC之前, 在下一次GC的时候,无论内存是否足够,都会回收掉别弱引用关联着的对象;
虚引用:也叫幽灵引用。虚引用不会对一个对象的生存空间造成任何影响,也 无法通过一个虚引用来获取到一个对象的实例,为一个对象关联一个虚引用的 唯一目的就是能在这个对象被GC时收到一个系统通知。
2.5需要垃圾回收的内存
方法区(jdk1.7)/元空间(jdk1.8)
jdk1.7的方法区一般称为永久代 (Permanent Generation)
jdk1.8的元空间存在于本地内存。
方法区主要回收:废弃常量和无用的类
堆
Java堆是垃圾收集器主要管理的场所,因此很多时候也被称为“GC 堆”;
由于现在的垃圾收集器基本都是采用分代收集,所以Java堆还可以细分为:
-
新生代(Young Generation):新生代又可分为Eden区和Survivor区 新生代的垃圾回收称为:Minor GC或者Young GC; 新生代的垃圾回收指发生在新生代的垃圾回收,
因为新生代对象具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收动作也比较快; (方法调用,在方法返回后,方法出栈帧,局部变量消亡,对象不可达)
-
老年代(Old Generation) 老年代垃圾回收称为:Major GC
老年代垃圾回收指发生在老年代上的垃圾回收。 出现了Major GC,一般都会伴随着至少一次的Minor GC;
Major GC 一般比Minor GC慢十倍以上;
2.6垃圾回收算法
1.标记清除算法(Mark-Sweep算法)
老年代的垃圾收集算法。
算法分为"标记"和"清除"两个阶段:首先标记处所有需要回收的对象,在标 记完成后,统一回收所有标记的对象; (类似遍历文件夹下的子文件,标记废弃文件然后删除)
缺点:效率问题和内存空间。 空间碎片太多可能会导致以后在程 序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾 收集动作
2.标记整理算法(Mark-Compact)
老年代的垃圾收集算法
标记过程和标记清除阶段一样,但是第二个阶段不是清除,而是将所有存活 的对象都移到一端,然后直接清除掉边界以外的内存
回收后的内存空间是连续的,不会产生内存碎片。
3.复制算法(Copying)
新生代的垃圾收集算法
将可用内存划分为两部分,每次只用掉其中的一部分,当这一部分内存块用完的时候,就将存活着的对象复制到另外一块没有使用的内存上,然后把已 经用掉的那一部分内存全部清理掉
缺点:复制算法的代价就是将内存缩小为原来的一半,代价太高
4.分代收集算法( Generation Collection)
不同的内存划分,根据对象的生存周期将对象划分为几块,每块内存使用不同的收集算法,
新生代因为每次回收的时候都有大量的对象死去,所以采用复制算法;(Eden:s0:s1=8:1:1,内存使用率为90%)
老年代对象存活率高,同时也没有额外的空间为它担保,所以只能采用"标记-清除"或者"标记-整理"算法;
新生代GC主要的特点:
(1)大部分对象生存周期短,采用复制算法。
(2)大部分情况下,s1空间足够存放存活对象(只使用一个Survivor空间)
(3)特殊情况,s1不足存放–>晋升到老年代。
2.7 分析垃圾回收的过程
用户线程创建对象时会优先分配到Eden区,如果Eden区空间不足,会触发Minor GC,即先从90%的空间中取出存活的对象,复制到这10%的空间(s1),然后清理掉刚才使用过的Eden区和s0区,如果survivor(s1)空间足够,进行Minor GC;如果survivor空间不够,会检查老年代的最大连续空间(分配担保机制),如果大于,执行Minor GC ;
如果小于,继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小(类似银行的信誉值),如果小于,Full GC,
如果足够的话,会尝试进行Minor GC,具有风险,失败的话,会进行Full GC(类似担保人破产申请,把资产全部拍卖归还)
3.内存分配策略
3.1.对象优先在Eden区分配
大多时候,对象在新生代的Eden区分配,如果空间不足JVM就会进行一次Minor GC
3.2.大对象直接计入老年代
大对象:需要大量的连续空间的Java对象,最典型的就是很长的字 符串或者数组;
大对象容易触发老年代的Major GC
3.3.长期存活的对象会进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生在 Survivor 区中每熬过一次 Minor GC,年龄加 1 岁,当年龄到一定程度(默认15岁),就会晋升到老年代中。
3.4.动态年龄的判断
在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
3.5.空间分配担保
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可能连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这个 Minor GC 是有风险的;若小于,HandlePromotionFailure 设置不允许冒险,那这次也要改为进行一次 Full GC。
简单来说就是如果survivor空间不够的话,会先检查老年代最大连续空间能否存下这些对象,存的下就可以进行 Minor GC,存不下会检查老年代的最大 可用连续空间是否大于每次晋升到老年代的对象的平均大小,大于等于的话就会尝试Minor GC,失败的话会进行Full GC;不够的话也是进行Full GC
类似于一个人去银行贷款,老年代就相当于一个担保人,如果担保人能还起贷款的话就会给这个人贷款,如果担保人资金不足的话,会查一下担保人的信誉值,根据信誉值来尝试去贷款,有可能就还不上了,还不上的话担保人就进行破产申请,把资产全部拍卖偿还,类似Full GC
4.垃圾收集会造成的影响
垃圾收集器中的并行和并发:
并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态。
并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程 序继续运 行,而垃圾收集程序在另外一个CPU上。
4.1.Stop The World(STW)
垃圾回收工作是在垃圾回收线程中执行的,在很多情况下,执行垃圾回收工作,或是执行垃圾回收其中 某一步骤时,需要暂停用户线程,因为GC会标记对象为不可达,用户线程在并发执行时,可能会将该对象重新拉入引用链,垃圾回收时就会回收这个不该回收的对象,发生问题。
在类加载完成的时候,虚拟机就把对象内什么偏移量上是什么类型的数据计算出来,在JIT即时编译器编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得 知这些信息了。这些特定的位置称为安全点(Safepoint),让用户线程跑到安全点再暂停。
4.2用户线程和GC线程执行的关系
(1)并行-触发GC,而且没有安全问题
(2)用户线程暂停,GC线程运行,-触发GC,有安全问题
(3)用户线程执行,GC线程暂停–没有触发GC
4.3吞吐量和用户体验
吞吐量优先:用户线程执行时间/用户线程执行时间+用户线程暂停时间 用户总的停顿时间越短,吞吐量越高,即使单次停顿时间长点也可以接受;
用户体验优先:单次用户线程暂停时间(STW)尽量越短越好
用户体验优先和吞吐量优先是成相反的关系;
5.垃圾收集器
CMS+ParNew:用户体验优先
ParallelScavenge+Parallel Old:吞吐量优先
G1:全堆
Serial收集器(新生代收集器,串行GC)
特性: 单线程 使用复制算法 STW
应用场景: Client模式下的默认新生代收集器
优势: 对于单个CPU来说,Serial收集器没有线程交互的开销,可以专心做垃圾收集,收集效率最高
ParNew收集器(新生代收集器,并行GC)
特性: 多线程 使用复制算法 STW
应用场景: 搭配CMS收集器在用户体验优先的程序中使用 ParNew是运行在Server模式下的新生代垃圾收集器,其中还有一个很重要 的原因:除了Serial收集器外,目前只有他才可以和CMS搭配使用
优势:相比于单个CPU环境,随着可以使用CPU数量的增加,他对于CPU资源的利 用还是很有好处的
Serial Old收集器(老年代收集器,串行GC)
特性: 单线程 标记-整理算法
应用场景:
给Client模式下的虚拟机使用
在Server模式下:
与Parallel Scavenge收集器搭配使用
作为CMS的备用方案,在并发收集发生Concurrent Mode Failure时使 用
Parallel Old收集器(老年代,并发GC)
特性: 多线程 标记-整理算法
应用场景: 吞吐量优先场景
CMS收集器(老年代收集器,并发GC)
**特性:**并发收集、低停顿;会用“标记-清除”算法; 用户体验优先
步骤:
初始标记(CMS initial mark) 初始标记仅仅只是标记一下GC Roots能直接关联到的对象, 速度很快, 需要暂停线程“Stop The World”。
并发标记(CMS concurrent mark) 并发标记阶段就是进行GC Roots Tracing的过程。 (从GC Root开始对堆内对象进行可达性分析,找引用链)
重新标记(CMS remark): 是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间一般会比初始标记阶段稍 长一些,但远比并发标 记的时间短,仍然需要“Stop The World”。 (修复)
并发清除(CMS concurrent sweep) 并发清除阶段会清除对象。
缺陷:
(1)内存碎片:由标记清除算法导致,会引起老年代连续空间不足导致Full GC
(2)浮动垃圾:并发清理的过程中,由于用户线程还在执行,因此就会继续产生对象和垃圾,这些并没有被标记,如果用户线程产生的对象进入老年代,而老年代空间不足,会出现"Concurrent Mode failure",再次触发老年代GC,使用Serial Old收集器。
(3)抢占Cpu资源
G1收集器(全区域的垃圾回收器)
特性:用户体验优先**
用在heap merory(堆内存)很大的情况下,把heap划分为很多很多的region块,动态设置为Eden,Survivor,Tenured(老年代)然后并行的对其进行垃圾回收。
G1垃圾回收器回收region的时候基本不会STW,而是基于 most
garbage优先回收的策略来对region进行垃圾回收的。(整体看基于标记整理,局部看是两个region间基于复制算法)
步骤:
1.标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短
2.并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
3.最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录(和CMS相比只是采用算法不同)
4.筛选回收:它有一个 Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的
6.常见JVM参数
-Xmssize
设置堆的初始化大小。如设置堆空间初始化大小为6m:-Xms6291456 或 -Xms6144k 或 -Xms6m
-Xmxsize
设置堆的最大值。如设置堆空间的最大值为80m:-Xmx83886080 或 -Xmx81920k 或 -Xmx80m
-Xmnsize
设置年轻代的大小(初始化及最大值)。如设置256m大小的年轻代:-Xmn256m 或 -Xmn262144k 或 -Xmn268435456
-XX:NewSize
设置年轻代的初始化大小。
-XX:MaxNewSize
设置年轻代的最大值。
-XX:PermSize=size
设置永久代的大小(jdk1.7方法区)
-XX:MaxPermSize=size
设置永久代的最大值(jdk1.7方法区)
-XX:MetaspaceSize=size
设置永久代的大小(jdk1.8元空间)
-XX:MaxMetaspaceSize=size
设置永久代的最大值(jdk1.8元空间)
-XX:SurvivorRatio=ratio
设置Eden区与Survivor区的空间比例,默认为8,即Eden=8,Survivor From=1,Survivor To=1。
-XX:MaxTenuringThreshold=threshold
对象晋升到老年代的年龄阈值
-XX:PretenureSizeThreshold=size
在老年代分配大对象的阈值,单位只能使用byte,如3m,只能写为3145728
-XX:+PrintGCDetails
打印gc详细信息