声明:本文基于HotSpot JVM 1.7版本
本文垃圾回收器部分不具体介绍G1
文中部分图片来源于网络,权侵删
1.JVM规范规定的运行时数据区域
Java虚拟机规范将Java内存划分为程序计数器、Java虚拟机栈、本地方法栈、方法区、运行时常量池和直接内存,下面我们就简单介绍一下这些区域:
1.1 程序计数器
程序计数器(Program Counter Register)可以看作是当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个程序计数器来完成。如果线程运行的是一个Java方法,它记录的是正在执行的虚拟机字节码指令的地址;如果运行的是一个Native方法,这个计数器的值则为空(Undefined)。
1.2 Java虚拟机栈
Java虚拟机栈(Java Vitual Machine Stacks)也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。虚拟机栈中的局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(可以是指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAdress类型(指向了一条字节码指令的地址)。这部分区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;如果虚拟机栈可以动态扩展,如果无法申请到足够的内存,就会抛出OutOfMemoeyError异常。
1.3 本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,他们之间的区别是虚拟机栈执行Java方法(也就是字节码)服务,而本地方法栈则为Native方法服务。与虚拟机栈一样本地方法栈可能抛出StackOverFlowError和OutOfMemoryError异常。
1.4 Java堆
Java堆是所有线程共享的一块区域,也是Java虚拟机所管理最大的一块内存区域。此内存区域唯一的目的就是存放对象实例,几乎所有的对象、数组都在这里分配内存,但随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有对象都分配在堆上也渐渐变得不是那么绝对。如果创建对象时无法申请到足够的内存就会抛出OutOfMemoryError异常。
1.5 方法区
方法区(Method Area)也是各个线程共享区域,它用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
1.6 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容会在类加载后进入方法区的运行时常量池中存放。作为方法区的一部分,当常量池无法申请到内存时会抛出OutOfMemoryError异常。
1.7 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区域的一部分,也不是java虚拟机规范中定义的内存区域。在JDK1.4新加入的NIO(New Input/Output)类,引入了一种基于通道(Chnnel)与缓冲区(Buffer)的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存区域 的引用进行操作。这样就避免了在Java堆和Native堆中来回复制数据。虽然直接内存不受Java堆大小的限制,但既然是内存就会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。
从线程的角度来说,Java内存可以分为两类:
所有线程共享区域
方法区、运行时常量池、Java堆;这些区域是所有线程共享的;在Java虚拟机启动时创建的,只有当Java虚拟机退出时才会销毁。
线程间隔离的数据区
程序计数器、Java虚拟机栈、本地方法栈;这些数据区域是每个线程私有的,不与其他线程共享;每个线程的数据区随线程创建而创建,并在线程退出时销毁。
2.HotSpot 1.7对JVM规范中运行时数据区域的实现
2.1 Java虚拟机栈和本地方法栈
HotSpot将Java虚拟机栈和本地方法栈合二为一,统称为栈内存。
JVM栈内存设置参数:
- -Xss 设置栈内存大小
2.2 堆内存和方法区
HotSpot1.7使用分代回收的思想,将堆内存划分为新生代和老年代,而方法区通过永久代实现。
JVM堆内存相关的参数如下:
- -Xms 控制Java堆的初始化值
- -Xmx 堆的最大值
- -Xmn 控制年轻代的值
- -Xss 设置java线程栈的大小
- -XX:NewRatio 年轻代与老年代的比值
- -XX:SurvivorRatio Eden区与两个Survivor区域的比值, 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
JVM永久代(方法区)内存参数:
- -XX:PermSize 永久代的初始大小
- -XX:MaxPermSize 永久代的最大值
3.判断对象是否有可以被回收
3.1 引用计数器算法
思路就是给对象添加一个引用计数器:当对象被引用一次时,计数器值就增加1;当引用失效时,引用器的值就减1;任何引用计数器值为零的对象就是没有被使用的对象。这种判断对象是否被使用的方式实现简单,但是存在一个问题:当A、B两个对象只是循环引用,没有其他任何引用,这时候其实这两个对象已经不能被访问到了,但是他们还是被对方所引用。所以Java没有采用这种方式,而是采用可达性分析算法。
3.2 可达性分析算法
这个算法以一系列的“GC Roots”对象作为起点,从这些起点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象从引用链不可达时,证明此对象是不可用的。
实际上Java1.2以后就对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),具体GC对几种引用的回收机制可以参考这篇文章:JVM 引用计数、强引用、弱引用、软引用、虚引用
4.垃圾回收算法
4.1 标记-清理算法(Mark-Sweep)
标记清理算法分为两步--“标记”和“清理”:首先标记出所有需要回收的对象,在标记完成后统一回收所有标记的对象。这种算法有两个明显的不足:一个是效率问题,标记和清理两个过程的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,内存碎片太多可能会导致后续分配大对象时,无法找到足够的连续内存,而不得提前触发另一次垃圾回收动作。
4.2 复制算法
复制算法的思路就是,将内存划分为两块,每次只是用其中的一块。当其中的一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样每次清理只需要移动堆顶指针,按顺序分配即可,运行效率高,且不会产生内存碎片。但是他的代价是将内存缩小为原来的一半。。。
实际上在真正垃圾回收器实现时,没有必要将内存划分为等量的两块。例如,HotSpot就是将新生代划分为Eden区和两个Survivor区(From 和To区),默认比例为8:1:1,创建新对象是在Eden区域分配内存,每次垃圾回收时,将Eden区和From区中存活的对象复制到To区,然后将Eden和From区清空,下一次垃圾回收时同上操作。这样只浪费了1/10的新生代区域。
4.3 标记-整理算法
复制算法在对象存活率高时就要进行较多的复制操作,效率就会大打折扣,对于对象存活的老年区就不太适用了,所以标记整理算法就应运而生了。标记整理算法的“标记过程”和“标记清除”的一样,但后续的整理阶段是将存活的对象移动到一端,然后清理掉端边界以外的内存。
5.垃圾回收器
5.1 Serial收集器
Serial是一个单线程新生代的收集器,它在进行垃圾回收时会暂停其他所有的工作线程(Stop The World),直到它收集结束。这是一个古老的垃圾收集器,一般不会用在企业级应用中,但对于一些内存区域很小的桌面程序,倒是可以采用这种垃圾回收器。可以使用参数-XX:+UseSerialGC
使用Serial收集器。
5.2 ParNew收集器
ParNew收集器其实就是Serial的多线程版,采用多线程的方式回收垃圾,同样回收过程中,也需要暂停其他所有用户线程。可以使用JVM参数-XX:UsePartNewGC
指定新生代使用ParNew收集器。
5.3 Parallel Scavenge收集器
新生代垃圾回收器,采用复制算法,是一个吞吐量(运行用户代码的时间、(运行用户代码的时间+垃圾回收的时间))优先的并行垃圾回收器。他提供了控制最大垃圾收集时间的参数-XX:MaxGCOauseMillis
和直接设置吞吐量的参数-XX:GCTimeRatio
,其中-XX:MaxGCOauseMillis
是以毫秒为单位的正数,-XX:GCTimeRatio
是大于0小于100的整数。
Parallel Scavenge
收集器还有一个参数-XX:+UseAdaptiveSizePolicy
,它是一个开关,当它打开后就不需要手动设置新生代的大小(-Xmn)、Eden与Survicor区的比例(-XX:Survivor)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等详细参数了,虚拟机会根据当前系统的运行情况、收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或者最大吞吐量,这种调节方式成为GC自适应策略(GC Ergonomics)。
可以使用参数-XX:+UseParallelGC
指定新生代使用Parallel Scavenge收集器。
5.4 Serial Old收集器
Serial Old收集器是Serial的老年代版本,通同样使用一个线程进行垃圾回收,采用“标记整理”算法。这种垃圾收集器主要给Client模式下的虚拟机使用。但是Server模式下也有两种使用场景:JDK1.5及以前的版本中与Parallel Scavenge收集器搭配使用,另一个场景是作为CMS收集器后备方案,在并发收集发生Concurrent Mode Failure时使用。可以使用参数-XX:+UseSerialGC
指定老年代使用Serial old收集器。
5.5 Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年版本,使用多线程和“标记-整理”算法。这个收集器的出现很大程度上解决了新生代的Parallel Scavenge收集器的尴尬地位。原因是在JDK1.6以前,新生代采用Parallel Scavenge收集器,老年代的收集器只能采用Serial Old收集器,这样的组合很大程度上还不如ParNew加CMS给力。但是1.6出现的Parallel Old收集器和新生代的Parallel Scavenge收集器是一个吞吐量优先的最佳搭档。使用参数-XX:+UseParallelGC
指定老年代使用Parallel Old收集器。
5.6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的老年代垃圾收集器。从名字(包括“Mark Sweep”)上就可以看出,CMS收集器是基于“标记-清除”算法实现的,它的运作过程分为4个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍需要“Stop The World”,可以使用参数-XX:+UseConcMarkSweepGC
指定老年代使用CMS回收器。CMS还有一个重要的参数-XX:CMSInitiatingOccupancyFraction
,它是0-100的数,是触发CMS回收老年代的阈值,即:当老年代使用率大于改参数指定值时候,就会使用CMS回收器对老年代的垃圾进行回收。
6.JVM参数
HotSpot JVM
选项分为三类:
6.1 标准选项
这类选项的功能很稳定,在后续的版本中也不太会发生变化。运行java -help
可以看到所有的标准选项。标准选项都是以-
开头的,比如-version
,-server
等。
6.2 X选项
比如-Xms
。这类选项都是以-X
开头的,也被称为X
选项。运行java -X
命令可以查看所有的X
选项。
6.3 XX选项
这类选项属于实验性的,主要是给JVM
开发者用于开发和调试JVM
的,在后续的行为中可能会发生改变。XX选项的语法
- 如果是布尔类型的选项,它的格式为-XX:+flag或者-XX:-flag,分别表示开启和关闭该选项。
- 针对非布尔类型选项,它的格式为-XX:flag=value。
对于HotSpot建议将最大堆和最小堆设置为相同值
- -Xms 控制Java堆的初始化值
- -Xmx 堆的最大值
- -Xmn 控制年轻代的值
- -Xss 设置java线程栈的大小
- -XX:PermSize 永久代的初始大小
- -XX:MaxPermSize 永久代的最大值
- -XX:NewRatio 年轻代与老年代的比值
- -XX:SurvivorRatio Eden区与两个Survivor区域的比值, 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
- -XX:+PrintGCDetails 打印GC详情
更多JVM参数请参阅:JVM参数设置与分析
7.参考文献
Java中的垃圾回收机制
内存区域 JVM运行时数据区
杂谈GC
JVM系列三:JVM参数设置、分析
JVM 垃圾回收器工作原理及使用实例介绍