目录
- 往期博客:Java课堂篇3_初识JMM、常量池简单理解(字符串常量池、静态常量池、大整型常量池)
- 为什么要了解垃圾收集和内存分配?
- 如何判断对象已死?
- 引用计数算法
- 可达性分析算法
- JDK1.2之后引用的扩充
- 回收方法区
- 垃圾收集算法分代收集理论
- 标记清除
- 标记复制
- 标记整理
- 对象分配
- 虚拟机性能监控故障处理工具
1、为什么需要了解垃圾收集和内存分配?
当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到高并发量的瓶颈时,我们必须对这些“自动化”的技术实
施必要的监控和调节。
2、如何判断对象已死?
2.1、引用计数法
- 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当有一个地方取消引用它时,计数器值减1
- 虽然额外占用内存空间,但是他的原理简单,判定效率也很高
- Java领域主流的虚拟机未采用没有选择用,因为这个看似简单的算法有很多例外的情况要考虑,必须配合额外的大量处理才能确保正确工作,如单纯的引用计数很难解决对象之间的互相引用问题。
2.2、可达性分析
- 通过一系列的GC Roots的根对象作为起始点集,从这些结点开始,根据引用关系向下搜索,搜索走过的路径称为引用链,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是不可达,证明这个对象不再被使用
- 固定可以作为GC Roots的对象包括:
- 虚拟机栈引用的对象(方法参数、局部变量、临时变量)
- 方法区的静态属性引用的对象
- 方法区的常量池引用的对象
- 本地方法栈Native方法引用的对象
- 虚拟机内部的引用
- 同步锁synchrionized持有的对象
2.3、JDK1.2之后引用的扩充
四种新扩充的引用
- 强引用:传统的"引用"定义如
Object o = new Object()
,只要强引用的关系还在,垃圾收集器就永远不会回收掉被引用的对象 - 软引用:描述还有用,但是非必须的对象,在系统将要发生内存溢出异常前,会把这些对象列入回收范围之中进行二次回收,如果回收之后还是内存不够,就会抛出异常。JDK1.2版之后提供了SftReference类来实现软引用
- 弱引用:也是描述那些非必须的对象,但是它的强度比弱引用更弱一些,被弱引用引用的·对象只能生存到下一次垃圾收集发生为止。
- 虚引用:不对对象存活造成影响,位一各对象设置虚引用的目的只是为了垃圾收集器回收该对象时收到有个系统通知。
3、回收方法区
- 相比于堆内存的回收,方法区的回收由于苛刻的回收条件,其区域垃圾收集的成果往往很低
- 方法区的垃圾收集主要涉及两部分的内容:
- 废弃的常量:字符串常量池里面的常量等
- 不在使用的类型:类、接口、方法、字段的符号引用等
- 判断一个类不在使用需要考虑
- 该类的所有实例都已经被回收
- 加载该类的类加载器已经被回收
- 该类对java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
- 在大量使用反射、动态代理、CGLIB等字节码的框架,动态生成JSP等这类频繁自定义类加载器的场景,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
4、垃圾收集算法
垃圾收集算法可以划分为
- 引用计数式垃圾收集(对应前面的 引用计数算法)
- 追踪式垃圾收集(对用前面的 可达性分析算法)
分代收集设计原则
- 弱分代假说:任何对象都是朝生夕灭
- 强分代假说:熬过越多次垃圾收集的对象就越难消亡
因此存在
- 部分收集Partical GC
- 新生代收集Young GC
- 老年代收集Old GC
- 混合收集Mixed GC
- 整堆收集Full GC
4.1、标记清除算法
算法分为 标记 和 清除 两个阶段
- 标记:首先标记出所有要回收的对象(或者存活的对象),在标记完成后
- 清除:统一回收掉被标记的对象
缺点
- 执行效率不太稳定:如果Java堆中包含大量对象,而且其中大部分都是需要回收的,这时需要大量标记和清除操作
- 内存空间碎片化问题:清除之后,产生大量不连续的内存空间,当需要分配大对象是,有可能放不下还需要进行一次GC
4.2、标记复制算法
相比标记清除算法
- 半区复制:将内存容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就还将存活的对象复制到另外一块,再把使用过的一块内存全部回收清理。
- 缺点:这种算法产生大量的内存空间复制的开销,可用内存缩小为原来的一半,空间浪费。
半区复制分代策略
- Appel式回收:虚拟机的新生代使用此种回收算法,将新生代分为 Eden和Survivor区域,比例为8:1:1;每次将Eden的存活对象放到Survivor中
- 需要老年代内存担保,也就是Survivor需要像老年代传送对象。
4.3、标记整理算法
相比于新生代使用标记复制算法、标记清除算法
- 老年代不能使用标记复制算法,因为老年代对象存活率高,进行复制会浪费内存空间
- 针对老年代对象的死亡特征,需要使用标记-整理算法,区别于标记-清除算法本质区别就是 不是直接对可回收对象清理,而是将存活的对象往一段移动,清除边界以外的内存。
特点
- 移动存活对象的·时候,尤其是老年代,移动对象地址需要全部用户程序才能进行Stop The World
- 不移动时候采用标记清除算法,内存分配复杂
- 移动的时候采用标记整理算法,回收时更复杂
- 有一种和稀泥方式,碎片化程度到达一定程度开启移动
5、对象分配
对象的内存分配从概念上讲都是堆上分配,(实际可能有即时编译后被拆散为标量类型并间接的在栈上分配),在经典分代的设计下,新生对象会直接分布在新生代,一些超过阈值的大对象可以直接分布在老年代
- 对象优先在Eden分配:没有足够空间会触发YoungGC,存活的对象会进入Survivor
- 大对象直接进入老年代:为了避免大对象内存复制的开销,直接将大对象分配到老年代,通过设置
-XX:PretenureSizeThreshold=3145728
参数指定阈值 - 长期存活的对象进入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器存储在对象头中,对象通常在Eden单上,如果第一次经理YongGC能够存活下来并且Survivor能够放的下,该对象就会被移动到Survivor并且年龄加1岁,当年龄增加到一定程度(默认15岁)就会被放到年代。可以通过
-XXMaxTenuringThreshold
设置 - 动态对象年龄判断:除了对象年龄增长进入老年代,如果Survivor空间相同年龄的对象综合大于Survivor空间的一半,年龄大于等于该年龄的对象可以直接进入老年。
- 空间分配担保:在发生YoungGC的时候,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
- 如果这个条件成立,那么这一次GC是确保安全的
- 如果这个条件不成立,则虚拟机会检查参数
-XX:HandlePromotionFailure
参数是否允许担保失败,如果允许,则会检查老年代可用连续空间是否大于历届上升到老年代对象年龄的平均大小
- 如果大于,进行一次YoungGC
- 如果小于,或者是
-XX:handlePromotionFailure
设置不允许冒险,这时候就需要进行一次FullGC
6、虚拟机性能监控故障处理工具
6.1基础故障处理工具
- jsp:虚拟机进程状况工具
虚拟机进程查看定位工具 - jstat:虚拟机统计信息监视工具
显示类加载、内存、垃圾收集器、即时编译等运行时数据,定位虚拟机性能问题 - jinfo:Java配置信息工具
- jmap:Java内存映像工具
用于生成堆转储快照 - jhat:虚拟机堆转储快照分析工具
- jstack:Java堆栈跟踪工具
- jcmd:Java7开始提供的虚拟机诊断命令工具
基本Java工具
- javadoc:API文档生成器
- javap:字节码分析工具
6.2可视化故障处理工具
- jhsdb:Java9开始提供的进程调试器,基于服务代理的调试工具
首先使用jps查看进程号,然后jhsdb hsdb --pid xxx
进行操作 - JConsole:Java监视与管理控制台
控制台输入jconsole,本地、远程进行连接 - VisualVM:多合-故障处理工具