目录
4.1 垃圾收集的算法有哪些? 如何判断一个对象是否可以回收?
1 JVM 内存模型
程序计数器(PC,Program Counter Register):在JVM规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。
Java 虚拟机栈:之前也叫做Java 栈,每个线程在创建的时候都会创建一个虚拟机栈,对应着Java 方法调用。
堆(Heap):存放Java 对象实例。堆被所有的线程共享,在虚拟机启动时,通过 “ Xmx” 之类的参数来指定堆大小。
方法区:所有线程共享的一块内存区域,用于存储元数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。
运行时常量池:常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号的引用。
2 OOM
谈到Java 内存模型,不可避免的要设计到 OutOfMemory (OOM)问题,那么在Java 里面存在哪种OOM 可能性,分别对应哪个内存区域的异常状况呢 ?
OOM通俗点说,就是JVM 内存不够用了。
public class OutOfMemoryError extends VirtualMachineError
javadoc 中对 OOM Error 的解释是:Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector 。当Java虚拟机无法分配对象时抛出,因为它内存不足,垃圾回收器无法再提供内存。
这里还隐藏着另外一个意思:在抛出OutOfMemoryError 之前,会触发垃圾回收收集器,尽可能去清理出空间。
OOM 常见原因,简单总结下:
堆内存不足是常见的OOM 原因。可能是堆内存泄漏;也可能堆大小不合适,比如要处理的数据量比较大,但是没有指定JVM 堆大小;或者JVM 处理引用不及时,导致堆积起来,内存无法释放等。
Java 栈也会导致OOM。比如我们写一个方法不停的递归调用,而不退出,就会导致不断的进行压栈,这时候会抛出 StackOverFlowError 错误,如果JVM 试图去扩展栈空间的时候失败,则会抛出 OutOfMemoryError。
3 堆内部的结构
从对象年代视角来看:
- 新生代:在Java应用中,绝大部分对象生命周期都很短暂,这些生命周期比较短暂的对象叫做新生代对象。其内部又分为 Eden 区域和两个Survivor 区域。Eden 区域 是对象初始分配的区域;两个Survivor 区域,被用来防止从Minor GC 中保留下来的对象。
- 老年代:放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。也有特殊情况,如果对象比较大,完全无法在新生代找到足够长的连续空闲的空间,JVM 就会直接分配到老年代。
- 永久代:JDK 8 之后就不存在永久代了。之前存放元数据、常量池等。
- virtual: 表示暂时不可用的空间内存。。因为在JVM 内部,肯定不会将堆的大小扩展到其上限。
JVM 参数:
-Xmx value | 最大堆体积 | 默认情况下是堆内存的1/4 |
-Xms value | 初始的最小堆体积 | 默认情况是堆内存的1/64 |
-XX:NewRatio=value | 老年代和新生代的比例 | 默认数值是2,也就是老年代是新生代的2倍大,换句话说新生代是堆大小的1/3 |
-XX:NewSize=value | 指定新生代内存大小数值 | 不用比例的方式调整新生代,直接指定具体的大小 |
-XX:SurvivorRatio=value | 指定新生代内部区域Eden、 Survivor 区域的大小比例 | 默认是8,那么Survivor区域就是Eden 区域的1/8,也就是新生代一共10份,Eden占8/10,Survivor 占 1/10。因为有两个Survivor区域。 YoungGen=Eden + 2*Survivor
|
如何查看堆内和堆外的使用情况呢?
图形化工具: JConsole、VisualVM 等,对于Linux 系统可以 remote 连接。
命令行工具:Jstat 、Jmap、 Jmx
4 Java 常见的垃圾收集器
- Serial GC: 收集工作是单线程的,并且在进行垃圾收集过程中,会进入“stop-the-world” 状态。
- ParNew GC:新生代GC,它实际上是Serial GC 的多线程版本,最常见的场景是配合老年代的 CMC GC 工作
- CMS GC:基于标记-清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于Web等反应敏感的应用非常重要。缺点是存在内存碎片化的问题,所以很难避免在长时间运行情况下发生 full GC,导致恶劣的停顿。另外,既然是并发,CMS 会占用更多的CPU 资源,并和用户线程争抢资源。在JDK 9中被标记为废弃deprecated。
- Parallel GC:吞吐量优先的GC,其特点是新生代和老生代 GC 都是并行进行的,在常见的服务器环境中更加高效。Parallel GC 可以设置吞吐量等目标。
- G1GC:这是一种兼顾吞吐量和停顿时间的GC实现,是 JDK9 以后默认的GC 选项。其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region,可以有效的避免内存碎片,尤其是当Java 堆非常大的时候,G1的优势更加明显。
serial GC | -XX:+UseSerialGC | |
parNew GC | -XX:+UseConcMarkSweepGC -XX:+UseParNewGC | |
parallel GC | -XX:+UseParallelGC | -XX:MaxGCPauseMillis=value -XX:GCTimeRatio=N // GC 时间和用户时间比例 = 1 / (N+1) |
G1 GC | -XX:+DisableExplicitGC -XX:+UseG1GC |
说明:client vm mode(win 32)一直是Seiral GC,servermode下,9以后改为了G1,以前是Parrallel Gc
4.1 垃圾收集的算法有哪些? 如何判断一个对象是否可以回收?
GC 主要回收堆中的实例对象,通过可达性分析一个对象的引用是否存在,如果不存在,则对象会被回收。回收有一下几种算法:
复制算法(Copying):新生代GC 都用到复制算法,将活着的对象(可能在eden,也可能在from-survivor)复制到to-survivor区域。代价是,既然要进行复制,就需要提前预留空间,有一定的浪费。对于G1这种region 块 GC,复制而不是移动,那么需要维护各个region 之间对象引用的关系,内存和时间的开销都比较大。
标记-清除(Mark-Sweep)算法:首先进行标记工作,标识出所有要回收的对象,然后进行清除。缺点是 标记、清除的过程效率比较低,也不可避免的会出现内存碎片化问题,这就导致其不适合特别大的堆。否则,一旦full GC,暂时时间会比较长。
标记-整理(Mark-Compact)算法:类似于标记-清除,但为了避免内存碎片化,它会在清除过程中将对象移动,以确保移动后的对象占用连续的内存空间。
4.2 垃圾收集器工作的基本流程
新生代对象垃圾回收:
1、大部分对象创建都是在Eden的,除了个别大对象外。当其空间占用达到一定阈值时,会触发Minor GC
2、仍然被引用的对象存货下来,被复制到JVM 选择的survivor区域,这个区域这个时候叫做:from-survivor,并且对象年龄+1
3、第一次Minor GC 之后,Eden空闲下来了,再次触发 Minor GC的时候,另一个空闲的survivor区域 则会成为 to-survivor,Eden的存活对象都copy到to-survivor中,from-survivor的存活对象也复制to-survivor中。其中所有对象的年龄+1
4、from-survivor清空,再下次Minor GC的时候,成为新的to-survivor,带有对象的to-survivor变成新的from-survivor。重复回到步骤3
以上步骤会发生多次,直到对象年龄计数达到一个阈值,超过阈值的对象会被晋升为老年代对象。这个阈值可以通过MaxTenuringThreshold 来指定。
-XX:MaxTenuringThreshold=<N>
然后对于老年代对象的GC 算法 取决于 不同的GC,算法主要有 标记-整理、标记-清除。
以G1 垃圾收集来说,新生代对象采用的是Copying 复制算法,老年代对象采用的 标记-整理算法。
老年代中的无用对象被清除后,GC 会将对象进行整理,以防止内存碎片化。通常我们把老年代的GC叫做 Major GC,将对整个堆进行的清理叫做 Full GC,包括young gen、old gen等。只要老年代的连续空间大于 新生代所有对象的总大小或者历次晋升的平均大小 时,会发生Minor GC,否则会进行Full GC。
Minor GC | 新生代对象的回收 | copying 算法 |
Major GC | 老年代对象的回收 | 标记-清除、标记-整理 |
Full GC | 对整个堆进行回收,包括新生代、老年代 | 1、老年代的连续空间 小于 历次晋升的平均代销; 2、老年代的连续空间 小于新生代所有对象的总大小 |
GC 调优的话,主要还是主要看业务。从性能方面,通常关注三个方面: 内存占用、延时(latency)、吞吐量。大多数情况下会侧重某一个或者两个方面。