悄悄地告诉你:大白话带你认识JVM( 四 )


而且当老年区执行了full gc之后仍然无法进行对象保存的操作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xmx来调整 。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们 。

悄悄地告诉你:大白话带你认识JVM

文章插图
 
补充说明:关于-XX:TargetSurvivorRatio参数的问题 。其实也不一定是要满足-XX:MaxTenuringThreshold才移动到老年代 。可以举个例子:如对象年龄5的占30%,年龄6的占36%,年龄7的占34%,加入某个年龄段(如例子中的年龄6)后,总占用超过Survivor空间*TargetSurvivorRatio的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即例子中的年龄6对象,就是年龄6和年龄7晋升到老年代),这时候无需等到MaxTenuringThreshold中要求的15
#3.3.8 如何判断一个对象需要被干掉 
悄悄地告诉你:大白话带你认识JVM

文章插图
 
图中程序计数器、虚拟机栈、本地方法栈,3个区域随着线程的生存而生存的 。内存分配和回收都是确定的 。随着线程的结束内存自然就被回收了,因此不需要考虑垃圾回收的问题 。而Java堆和方法区则不一样,各线程共享,内存的分配和回收都是动态的 。因此垃圾收集器所关注的都是堆和方法这部分内存 。
在进行回收前就要判断哪些对象还存活,哪些已经死去 。下面介绍两个基础的计算方法
1.引用计数器计算:给对象添加一个引用计数器,每次引用这个对象时计数器加一,引用失效时减一,计数器等于0时就是不会再次使用的 。不过这个方法有一种情况就是出现对象的循环引用时GC没法回收 。
2.可达性分析计算:这是一种类似于二叉树的实现,将一系列的GC ROOTS作为起始的存活对象集,从这个节点往下搜索,搜索所走过的路径成为引用链,把能被该集合引用到的对象加入到集合中 。搜索当一个对象到GC Roots没有使用任何引用链时,则说明该对象是不可用的 。主流的商用程序语言,例如Java,C#等都是靠这招去判定对象是否存活的 。
(了解一下即可)在Java语言汇总能作为GC Roots的对象分为以下几种:
  1. 虚拟机栈(栈帧中的本地方法表)中引用的对象(局部变量)
  2. 方法区中静态变量所引用的对象(静态变量)
  3. 方法区中常量引用的对象
  4. 本地方法栈(即native修饰的方法)中JNI引用的对象(JNI是Java虚拟机调用对应的C函数的方式,通过JNI函数也可以创建新的Java对象 。且JNI对于对象的局部引用或者全局引用都会把它们指向的对象都标记为不可回收)
  5. 已启动的且未终止的Java线程
这种方法的优点是能够解决循环引用的问题,可它的实现需要耗费大量资源和时间,也需要GC(它的分析过程引用关系不能发生变化,所以需要停止所有进程)
#3.3.9 如何宣告一个对象的真正死亡首先必须要提到的是一个名叫 finalize() 的方法
finalize()是Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用 。
补充一句:并不提倡在程序中调用finalize()来进行自救 。建议忘掉Java程序中该方法的存在 。因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用) 。在Java9中已经被标记为 deprecated ,且 java.lang.ref.Cleaner(也就是强、软、弱、幻象引用的那一套)中已经逐步替换掉它,会比 finalize 来的更加的轻量及可靠 。
悄悄地告诉你:大白话带你认识JVM

文章插图
 
判断一个对象的死亡至少需要两次标记
  1. 如果对象进行可达性分析之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选 。判断的条件是决定这个对象是否有必要执行finalize()方法 。如果对象有必要执行finalize()方法,则被放入F-Queue队列中 。
  2. GC对F-Queue队列中的对象进行二次标记 。如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合 。如果此时对象还没成功逃脱,那么只能被回收了 。


    推荐阅读