前言
程序计数器, 虚拟机栈, 本地方法 3 个区域随线程而生, 随线程而灭, 栈中的栈帧随着方法的进入和退出有条不紊地执行着出栈和入栈操作, 每一个栈帧中分配多少内存, 基本上是在类结构确定下来就已知. 因此这几个区域的内存分配和回收都具备确定性. 在这几个区域就不需要考虑太多回收问题. 垃圾收集器主要关注于 Java 堆和方法区.
一, 如何判断对象是否存活?
首先说为什么要判断是否存活, 当垃圾收集器在对堆进行回收前, 第一就是要确定对象哪些是还在被引用的或者后面还需要被引用的, 即存活, 哪些是已经 "死去"(即不可能再被任何途径使用)
1, 引用计数算法
在对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加 1, 引用失效时就减 1. 任何时刻计数器为 0 的对象就是不可能再被使用的. 这个方法效率挺高, 大部分情况下也是很不错的算法.
但是在 JVM 中会很难解决对象之间相互循环引用的问题, 就如果两个对象之间相互调用, 这时候就会发生类似死锁的情况, 即这个地方相互调用会使得引用计数法始终认为有对象在引用当前对象, 就一直计数值大于或等于 1, 也就无法通知 GC 收集器回收它们. 但是实际的情况是这两个对象后面已经不再调用, 所以这个方法虽然简单高效, 但不是我们的首选. 虚拟机也不是通过这个算法来判断对象是否存活的.
2, 可达性分析算法
使用一系列的 GC Roots 的对象 (包括: 虚拟机栈中引用的对象, 方法区中类静态属性引用的对象, 方法区中常量引用的对象, 本地方法栈中 JNI 引用的对象) 作为起点, 从节点开始向下搜索, 当没有被 GCRoots 链接到的对象就可以回收, 如下图的对象 4 和 5 就判断为可回收对象.
在 JDK1.2 之后, Java 对引用这个概念进行了扩充, 也就是对象不仅仅只有引用和没有引用两个概念, 而是扩展到了 4 个:
强引用: 类似于 "Object obj=new Object()" 只要强引用在, 垃圾收集器永远不会回收掉被引用的对象.
软引用: 是用来描述一些还有用但是并非必需的对象, 对于软引用对象, 在内存溢出异常之前, 会把这些对象列进回收范围之中进行第二次回收.
弱引用, 比软引用更弱一点, 被弱引用关联的对象只能生存到下一次垃圾收集发生之前. 当垃圾收集发生时无论内存是否足够, 都会只回收弱引用的对象.
虚引用, 最弱的引用关系, 对象是否有虚引用对其生存时间是没有影响的. 唯一目的就是能在这个对象被收集器回收时收到一个系统通知.
对象要想真正宣告 "死亡" 需要至少两次的标记过程, 当对象在可达性分析时候发现没有被 GC Roots 链到那么对象就会进行第一次标记并且进行第一次筛选, 筛选的条件就是判断该对象有没有必要执行 finalize()方法, 需要执行的话就会把对象放入 F-Queue 的对列中去执行该对象中的 finalize()方法. 如果 finalize()方法让对象重新被 GC Roots 链到那么对象就重新活下来, 否则就会进行第二次标记, 等待垃圾回收的到来
二, 垃圾收集算法
目前来说 Java 堆中的对象是分为新生代和老年代, 对于新生代中的对象采用的是复制算法清理
1, 复制算法
它将可用内存空间划分为一块较大的 Eden 空间和两块较小的 From Survivor(S0)和 To Survivor(S1)空间. 每次使用时只使用 Eden 和其中一块 S 区. 比如这次使用的是 S0 区. 回收时将 Eden 和 S0 区中的中还存活的对象一次性复制到 S1 中最后再清理 Eden 和 S0 中的对象, HotSpot 虚拟机默认 Eden:S0:S1 之间大小比例是 8:1:1, 这是因为新生代中对象大多数甚至 98% 的都是 "朝生夕死". 如果 S 区的大小不够那么就会依赖老年代的内存进行分配担保.
2, 标记 - 清理与标记 - 整理算法
在老年代中因为对象存活率高, 没有额外的空间对它进行分配担保, 所以会采用标记 - 清理或标记 - 整理算法来进行回收对象
标记 - 清理算法: 首先标记出所有需要回收的对象, 在标记完成之后统一回收所有标记的对象
标记 - 整理算法: 先标记所有可回收对象, 让存活的对象向一端移动, 然后直接清理掉端边界以外的内存
上面的标记过程都是根据可达性分析算法中对象标记判定来实现的.
三, 内存设置参数
上面介绍了这么多, 那我们到底怎么操作里面的一些参数呢?
-Xms: 初始堆大小, JVM 启动的时候, 给定堆空间大小.
-Xmx: 最大堆大小, JVM 运行过程中, 如果初始堆空间不足的时候, 最大可以扩展到多 少.
-Xmn: 设置年轻代大小. 整个堆大小 = 年轻代大小 + 年老代大小 + 持久代大小. 持久代一 般固定大小为 64m, 所以增大年轻代后, 将会减小年老代大小. 此值对系统性能影响较大, Sun 官方推荐配置为整个堆的 3/8.
-Xss: 设置每个线程的 Java 栈大小. JDK5.0 以后每个线程 Java 栈大小为 1M, 以前每 个线程堆栈大小为 256K. 根据应用的线程所需内存大小进行调整. 在相同物理内存下, 减 小这个值能生成更多的线程. 但是操作系统对一个进程内的线程数还是有限制的, 不能无限生成.
-XX:NewSize=n: 设置年轻代大小
-XX:NewRatio=n: 设置年轻代和年老代的比值. 如: 为 3, 表示年轻代与年老代比值为 1: 3, 年轻代占整个年轻代 + 年老代和的 1/4
-XX:SurvivorRatio=n: 年轻代中 Eden 区与两个 Survivor 区的比值. 注意 Survivor 区有两个. 如: 3, 表示 Eden:Survivor=3:2, 一个 Survivor 区占整个年轻代的 1/5
-XX:MaxPermSize=n: 设置持久代大小
-XX:MaxTenuringThreshold: 设置垃圾最大年龄. 如果设置为 0 的话, 则年轻代对象不经 过 Survivor 区, 直接进入年老代. 对于年老代比较多的应用, 可以提高效率. 如果将此值设置为一个较大值, 则年轻代对象会在 Survivor 区进行多次复制, 这样可以增加对象再年轻代 的存活时间, 增加在年轻代即被回收的概率.
来源: https://www.cnblogs.com/Cubemen/p/10905130.html