深入理解Java虚拟机-GC&运行时数据区

系列阅读
1.深入理解Java虚拟机-GC&运行时数据区
2.深入理解Java虚拟机-类文件结构及加载
3.深入理解Java虚拟机-内存模型及多线程

1. Java虚拟机概念

JDK(Java Developement Kit)
包括Java程序设计语言、Java虚拟机、Java API类库。
JRE(Java Runtime Environment)
包括Java虚拟机和Java API类库中的Java SE API子集。
JVM(Java虚拟机)
是指一种能够运行Java字节码的虚拟机。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统,屏蔽了与具体操作系统平台相关信息。
HotSpot VM
是Sun JDK和Open JDK中所带的虚拟机,为目前使用范围最广的Java虚拟机。OpenJDK是JDK的开源版。Sun JDK归Oracle所有。Unix内置或者通过软件源安装JDK的话,一般是Open JDK。参考

2. Java虚拟机运行时数据区

运行时数据区

程序计数器
是当前线程所执行的字节码的行号指示器,在分支/循环/跳转/异常处理/线程恢复等有应用。

Java堆
用于分配和存放对象实例。

虚拟机栈
也称Java栈,描述Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
注意:如果局部变量是一个reference类型,它引用的对象在Java堆中可被各个线程共享,但是reference本身在Java栈的局部变量表中,它是线程私有的。

本地方法栈
与虚拟机栈作用相似,但本地方法栈为虚拟机使用到的Native方法服务,虚拟机栈为Java方法(字节码)服务。

方法区
主要存放类的元数据,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 运行时常量池是方法区的一部分。

直接内存
不是虚拟机运行时数据区的一部分。NIO(New Input/Output)类是基于通道和缓冲区的I/O方法,使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用来进行操作。

垃圾回收器可以对方法区,Java堆和直接内存进行回收.

扩展
HotSpot虚拟机不区分虚拟机栈和本地方法栈。
线程具有独立的程序计数器和Java虚拟机栈。
方法区和Java堆是被所有线程共享的内存区域。
局部变量表在栈帧中,用于保存函数的参数和局部变量.
在JDK1.6/1.7中,方法区可以理解为永久区(Perm),但是在JDK1.8中,永久区被替换成元数据区.

3. OOM

OOM有memory leak和memory overflow两种情况。
-Xoos:设置本地方法栈大小
-Xms:堆起始容量
-Xmx:堆最大容量
-Xmn: 新生代大小
-XX:PermSize:永久代(方法区)的初始大小
-XX:MaxPermSize:永久代(方法区)的最大值
-Xss:每个线程堆栈容量,JDK1.5+使用,直接决定函数调用的最大深度。

发生OOM的运行时数据区域
程序计数器是唯一没有规定任何OOM情况的运行时数据区域。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
如果虚拟机栈或本地方法栈可以动态扩展且扩展时无法申请到足够的内存将抛出OutOfMemoryError异常。
如果堆或方法区中没有内存完成实例分配且堆也无法再扩展时将抛出OutOfMemoryError异常。

4. GC(Garbage Collection)

对象创建与回收
判断是否需要类加载-分配内存-内存空间初始化-对象信息设置-对象赋值
HotSpot VM内存管理系统要求对象起始地址必须是8字节的整数倍。
任何一个对象的Finalize()方法都只会被系统自动调用一次,建议不用。

确定对象是死是活的算法

  • 引用计数法
    使用引用计数器对对对象的引用数进行记录,为0时对象不再使用。缺点是难以解决对象之间相互引用的关系,这个时候互相引用的对象实际上应该被回收,但是因为引用数不为0所以判断失误。

  • 可达性分析算法
    从一系列“GC Roots”对象向下遍历,此路径称为引用链。当一个对象到GC Roots没有任何引用链相连(对象不可达)时,则此对象不可用。为Java/C#/Lisp的主流实现。

新生代与老生代
Java堆分为新生代和老年代,新生代被分为三个区域:Eden、From Survivor、To Survivor。

Java堆内存大小划分

新生代可用区域是Eden + From Survivor,一般是Java对象申请内存及存放的地方。老年代一般存放在新生代中已存活许久的Java对象。


Java堆内存模型

垃圾收集器

HotSpot虚拟机的垃圾收集器

新生代大部分要回收,GC一般采用复制算法,快。老年代大部分不需要回收,GC一般采用标记-清除或者标记-整理算法。

  1. Serial收集器
    运行在Client模式下的虚拟机,对新生代进行单线程地暂停收集。采用复制算法。

  2. ParNew收集器
    是Serial收集器的多线程版本,关注点是尽可能地缩短垃圾收集时用户线程停顿的时间。采用复制算法。

  3. Parallele Scavenge收集器
    是多线程收集器,采用复制算法。目的是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行垃圾收集器时间+运行代码时间))。

  4. Serail Old收集器
    是Serial收集器的老年版本。采用标记-整理算法。

  5. Parallele Old收集器
    是Parallele Scavenge收集器的老年版本。采用标记-整理算法。

  6. CMS(Concurrent Mark Sweep)收集器
    是一种以获取最短回收停顿时间为目标的收集器。采用标记-清除算法。

  7. G1(Garbage-First)收集器
    是一款面向服务端应用的垃圾收集器。Java堆布局中的新生代和老生代不再物理隔离。具备如下特点:并行与并发;分代收集;空间整合;可预测的停顿。G1分代/分区.
    G1比CMS有更短的反应停顿时间,但是CMS有更高的吞吐量。但是G1在持续优化。

虚拟机之所以提供多种不同的收集器及调节参数,是因为只有根据实际应用需求和实现方式选择最优的收集方式才能获取最高的性能。

JVM参数
UseSerialGC 新生代和老年代都串行收集
UseParallelGC 新生代ParallelGC,老年代串行
UseParNewGC 新生代ParNew,老年代串行
UseParallelOldGC 新生代parallelGC,老年代ParallelOld
UseConcMarkSweepGC 新生代ParNew,老年代CMS+串行收集器
UserG1GC 使用G1回收器

GC模式

  1. Partial GC:并不收集整个GC堆的模式
    • Young GC:只收集young gen的GC。通常与与Minor GC等价。
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这
      个模式
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个
      模式
  2. Full GC:收集整个堆,包括young gen、old gen、perm gen(永久代,如果存在的话)等所有部分的模式。Major GC通常是跟full GC是等价的。经常会伴随至少一次的Minor GC,但不绝对。
    System.gc()默认触发Full GC。FullGC会造成Stop-The-World(Java用户执行线程停顿)。

对于GC分类并无完全准确的定义,所以不必纠结。参考

内存分配及回收策略

  • 对象优先在新生代Eden区中分配,当Eden区没有足够空间分配时将发起一次Minor GC。
  • 大对象直接进入老年代。
  • 长期存活的对象将进入老年代。
    根据对象的对象年龄(Age)计数器,从Eden出生并经过一次Minor GC仍存活,且被Survivor空间容纳,则将被移动到Survivor空间中,Age设为1。以后在Survivor区每熬过一次Minor GC,则Age加1,达到限制则晋升老年代。
  • 动态年龄对象判断
    如果Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象可直接进入老年代。
  • 空间分配担保
    新生代使用复制收集算法时,当Survivor空间无法容纳Minor GC存活对象时,需要老年代进行分配担保,将这些对象直接进入到老年代。
    但是老年代剩余空间要足够,为应对此风险,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

注:主要内容摘录自书籍 深入理解Java虚拟机,周志明 著