jvm垃圾回收的流程;哪些对象会被认为是垃圾;有一个对象A它有一个属性是B,B这个对象他又有一个属性是A,这个对象最终会不会被认为是垃圾;
允许GC之后,开始查找那些允许被回收的(两个算法)-> 开始回收(四个算法)
第一步:那些对象是垃圾:
1,引用计数法:通过对引用的遍历,找到对应的实例,让对应的实例计数加 1 ,如果引用取消,或者指向null,实例的引用减 1 。把找到的引用都遍历一遍之后,如果发现有对象实例的计数是0。那么这个对象 就是垃圾对象了。在通过垃圾回收算法对其进行 回收即可。
缺点:想想一下,有两个类,互相引用,也就是A对象的实例(也就是对象的全局变量)是一个指向B对象的引用,B对象实例是一个指向A对象的引用。那么这两个对象的引用计数,永远不可能是0 。也就不可能对其进行回收了。
2,可达性分析法:这个算法类似于树的遍历,学过数据结构的小伙伴应该会好理解。简单来说,按照一定的规则说明那些可以作为一个根节点(GC root),然后以这些根节点去访问其引用的对象,被访问的对象又会有其他对象的引用。想象一下,是不是像极了树的遍历。这个路径称作引用链,但凡是在引用链上的对象,都是可用的。注意,引用连的起始点都是GC root 哦。虽然有其他对象存在类似于引用链的结构,但是,起始点不是GC root的那一些,都是垃圾,可以被回收的。
一般情况下,都是使用的 可达性分析法去查找垃圾类实例。
GC root哪些对象会被认为是root;
GC root的查找规则:java栈中的引用,方法区中的静态属性(静态变量 + 静态常量),方法区中常量引用的对象(方法区中有个结构 叫做 常量池 ,存储的一部分是常量),本地方法(线程独占区中有个结构叫做 本地方法栈)。
jvm里面有一个存储虚拟s1和s2
年轻代里面有一个复制算法,这个就要说到
第二步:垃圾回收器算法(标记-清除、复制算法、标记-整理、分代算法)
1,标记-清除:找到垃圾类之后,标记一下。然后直接 清除即可。(算法很快)
缺点:产生空间碎片,不利于大对象的安排进去。
2,复制算法:将内存分为四块:新生代(Eden),生存代(Survivor * 2),老年代。有五种内存分配策略,讲完之后再说。类的升级流程是Eden->Survivor->老年代;
算法流程:1),先找到垃圾类,将可以使用的类移动到Survivor2,将Eden + 另一块Survivor1中的内存全部清除。
2),将新生成的类实例优先分配到Eden,分配不下时,放到Survivor2。进行GC时,将Survivor2中对象的满足一定条件(例如对象年龄达到某一个标准)的对象分配到老年代中。将本次GC存活下来的分配到Survivor1中,在清除Eden + Survivor2 。依次循环即可。
缺点:很容易发现吧,Survivor中每次都会浪费一个Survivor的内存没有使用,所以为了减少浪费,一般将Eden的内存扩大,Survivor的内存设置小一点。例如:HotSpot(HotSpot是8中的jvm默认虚拟机) 中设置的是 8 : 1 : 1;
3,标记-整理:看名字是不是感觉很熟悉,没错。跟标记-清除很像,也是直接标记。改算法使用到了前面两个算法的精华,改善了缺点。
算法流程:1),直接标记
2),集中,无缝隙的移动到一端,此时会发现,剩下的垃圾类,都会在其他地方。移动完成之后就会发现有一个边界,就是可用类跟其他空间的一个边界,下一步直接把边界以外的空间直接清除掉就可以了。
缺点:看起来很完美,但是越完美的,往往在时间上过不去。
4,分代算法:根据在哪里清除,选用算法不一样。
算法流程:1),新生代采用复制算法
2),老年代采用标记-清除算法(老年代GC很少访问,类也很少去直接分配到里面,内存碎片的可怕性就显得不那么重要了)
什么样的数据会往老年代里面迁移呢;
每次进行垃圾回收的时候,比如说当前这个对象存活下来了,计数器就会给他+1,默认的话,是当计数器达到16的时候就会放到老年代里面;
GC的作用域:
JVM内存分配原则:
1,对象优先分配到Eden区域;
2,大对象直接分配到老年区:大对象的就是,对象里面有很大数组或者很大的字符串;
3,长时间存活的对象存入老年区:就是上面复制算法里面说的那个对象升级流程;
4,动态对象年龄判定:jvm并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才可以进入老年代,如果Survivor空间中年龄相同的所有对象的总空间>=本servivor中的一半,那么年龄>=本年龄的对象可以直接进入老年区;
5,空间分配原则:简单来说,就是在发生Minor GC(在新生代进行GC)情况下,为了防止发生在Minor GC后,Eden有大量存活的对象,导致survivor不能全部存入,这时需要老年代去担保,把这些对象放入老年代,但是要确保老年要存的下。
1),再发生Minor GC之前,检查老年区的可用的连续空间是否是大于新生代(Eden)的所有对象的总空间,如果是,直接全部晋升老年代,保证Minor GC的安全;
2),如果不行,就检查HandlePromotionFailure(可以手工设定)参数时候允许担保失败,允许的话,直接分配。不能的话,发生一次full GC(或者是Major GC 在老年代进行GC)。
3),不允许担保失败,发生一次 full GC。
为什么不直接进行full GC ,因为速度慢呀。而且经常GC 也 效果不大,因为老年代都是一些长期存活的对象。
如果老年代内存也不够用了怎么办呢;
他会进行fullGC
fullGC的时候会有什么现象吗;有没有遇到到fullGC的时候影响业务的场景;
如果频繁的fullGC会出现cpu内存飙升的问题
收集器有:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(G1是目前最好的收集器)
上图是HotSpot的垃圾收集器的使用范围,HotSpot是现在主流的 jvm。
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。
工作原理:基于标记-清除算法实现的,流程分为4步:
1),初始标记(仅仅标记一下GC Root,)
2),并发标记(是一个单独的线程,一起跟用户的线程一起 运行)
3),重新标记(去标记一下在运行期间发生变化的对象)
4),并发清除(是一个单独的线程,一起跟用户的线程一起 运行)
其中,初始标记跟重新标记任然需要Stop The World。
图中的安全点,跟发动GC有关,任何一个GC收集器动作都会设计安全点。另附博客说明。
G1收集器:
目前最强收集器,强到什么地步。不需要其他收集器配合,自己就可以管理新生代跟老年代。G1是面向服务端应用的垃圾收集器,HotSpot开发团队称在未来可以替换掉JDK1.5中发布的CMS收集器。
工作流程:算法:整体采用了标记-整理算法,局部使用了复制算法。工作流程也是分为4步:
1),初始标记
2),并发标记
3),最终标记
4),筛选回收
跟CMS的工作流程差不多。因为G1还正在开发优化,在大数据上应用停顿时间以及吞吐量还有缺陷。还没有大方面的普及
jvm报错,OOM;
1、Java堆溢出:heap
Java堆内存主要用来存放运行过程中所以的对象,该区域OOM异常一般会有如下错误信息;
java.lang.OutofMemoryError:Java heap space
此类错误一般通过Eclipse Memory Analyzer分析OOM时dump的内存快照就能分析出来,到底是由于程序原因导致的内存泄露,还是由于没有估计好JVM内存的大小而导致的内存溢出。
2、栈溢出:stack
栈用来存储线程的局部变量表、操作数栈、动态链接、方法出口等信息。如果请求栈的深度不足时抛出的错误会包含类似下面的信息:
java.lang.StackOverflowError
另外,由于每个线程占的内存大概为1M,因此线程的创建也需要内存空间。操作系统可用内存-Xmx-MaxPermSize即是栈可用的内存,如果申请创建的线程比较多超过剩余内存的时候,也会抛出如下类似错误:
java.lang.OutofMemoryError: unable to create new native thread
3、运行时常量溢出 constant
运行时常量保存在方法区,存放的主要是编译器生成的各种字面量和符号引用,但是运行期间也可能将新的常量放入池中,比如String类的intern方法。
如果该区域OOM,错误结果会包含类似下面的信息:
java.lang.OutofMemoryError: PermGen space
4、方法区溢出 directMemory
方法区主要存储被虚拟机加载的类信息,如类名、访问修饰符、常量池、字段描述、方法描述等。理论上在JVM启动后该区域大小应该比较稳定,但是目前很多框架,比如Spring和Hibernate等在运行过程中都会动态生成类,因此也存在OOM的风险。
如果该区域OOM,错误结果会包含类似下面的信息:
java.lang.OutofMemoryError: PermGen space