-、运行时数据区域
程序计数器:记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
Java虚拟机栈:每个Java方法在执行的同时会创建一个栈帧用户存储局部变量表、操作数栈、常量池引用等信息等信息。从方法调用直至执行完成的过程,对应着一个栈帧在Java虚拟机栈中入栈和出栈的过程。
可以通过-Xss这个虚拟机参数来指定每个线程的Java虚拟机栈内存大小,在JDK1.4中默认为256k,而JDK1.5+默认为1M:
java -Xss2M
该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出StackOverflowError异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出OutOfMemoryError异常;
本地方法栈:本地方法栈与Java虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其他语言(C、C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
堆:所有对象都在这里分配内存,是垃圾收集的主要区域(GC堆)
现在的垃圾回收器基本都是采用粉黛收集算法,其主要的思想是针对不同类型的对象采用不同的垃圾回收算法。可以将堆分成两块:
- 新生代(Young Generation)
- 老年代(Old Generation)
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出OutOfMemoryError异常。
可以通过-Xms和-Xmx这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xmx1M -Xmx2M
方法区:用于存放已被加载的类信息、常量、静态变量、及编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,等待扩展失败一样会抛出OutOfMemoryError异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的 卸载,但是一般比较难实现。
HotSpot虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它收到很多因素影响,并且每次Full GC之后永久代的大小都会改变,所以经常会抛出OutOfMemoryError异常。为了更容易方便管理方法区,从JDK1.8后,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
方法区是一个JVM规范,永久带与元空间都是其一种实现方式。在JDK1.8后,原来永久代的数据被分到了堆和元空间中。元空间存储类的原信息,静态变量和常量池等放入堆中。
运行时常量池:运行时常量池是方法区的一部分。
Class文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译器生成的常量,还允许动态生成,例如String类的intern().
直接内存:在JDK1.4中新引入了NIO类,它可以使用Native函数库直接分配堆外内存,然后通过Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
二、垃圾回收
垃圾回收主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
判断一个对象是否可被回收
1.引用计数算法
为对象添加一个引用计数器,当对象增加一个引用计数器加1,引用失效时计数器减1。应用计数为0的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为0,导致无法对它们进行回收。正式因为循环引用的存在,因此Java虚拟机不使用引用计数算法。
2.可达性分析算法
以GC Roots为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java虚拟机使用该算法来判断对象是否可被回收,GC Roots一般包含一下内容:
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中JNI中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
3.因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的Class对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
4.finalize()
类似C++的析构函数,用户关闭外部资源。但是try-finally等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺数,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的finalize()方法,那么就有可能在该方法中让对象重新被引用,从而实现自救,自救只能进行一次,如果回收的对象之前调用了finalize()方法自救,后面回收时不会再调用该方法。
引用类型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java提供了四种强度不同的引用类型。
1.强引用。
被强引用关联的对象不会被回收。
使用new一个新对象的方式来创建引用。
Object obj =new Object();
2.软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
使用SoftReference类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
3.弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
使用WeakReference类来创建弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
4.虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
为一个对象设置虚引用的唯一目的是恩给你在这个对象被回收时收到一个系统通知。
使用PhantomReference来创建虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
垃圾收集算法
1.标记-清除
在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空间分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为“空闲链表”的单向链表。
在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小size的快block。如果它找到的块等于size,会直接返回这个分块;如果找到的块大于size,会将块分割成大小为size与(block-size)的两部分,返回大小为size的分块,并把大小为(block-size)的块返回给空闲链表。
不足:
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
2.标记-整理
让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
优点:不会产生内存碎片
不足:需要移动大量对象,处理效率比较低
3.复制
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。在回收时,将Eden和Survivor中还存活着的对象全部复制到另一块Survivor上,最后清理Eden和使用过的那一块。
HotSpot虚拟机的Eden和Survivor大小比例默认为8:1,保证了内存的利用率达到90%。如果每次回收有多余10%的对象存活,那么一块Survivor就不够用了,此时需要依赖老年代进行空阿金分配担保,也就是借用老年代的空间存储放不小的对象。
4.分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
新生代使用:复制算法
老年代使用:标记-清除 或者 标记-整理 算法
垃圾收集器
1.Serial收集器
Serial翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的有点是简单高效,在单个CPU环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是Client场景下的默认新生代收集器,因此在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
2.ParNew收集器
它是Serial收集器的多线程版本。
它是Server场景下默认的新生代收集器,除了性能原因外,主要是因为除了Serial收集器,只有它能与CMS收集器配个使用。
3.Parallel Scavenge 收集器
与ParNew一样是多线程收集器。
其他收集器目标是尽可能缩短垃圾收集用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里吞吐量指CPU用于运行用户程序的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后天运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变的频繁,导致吞吐量下降。
可以通过一个开关参数打开GC自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn),Eden和Survivor区的比例,晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。
4.Serial Old收集器
是Serial 收集器的老年代版本,也是给Client场景下的虚拟机使用。
如果用在Server场景下,它有两大用途:
- 在JDK1.5以及之前版本(Parallel Old诞生之前)中与Parallel Scavenge收集器搭配使用。
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
5.Parallel Old收集器
是Parallerl Scavenge收集器的老年代版本。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
6.CMS收集器
CMS(Concurrent Mark Sweep),Mark Sweep指的是标记-清除算法。
分为以下四个流程:
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行GC Roots Tracing的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分的标记记录,需要停顿。
- 并发标记:不需要停顿。
具有以下缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高。
- 无法处理浮动垃圾,可能出现Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次GC时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着CMS收集不能像其他收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现Concurrent Mode Failure,这时虚拟机将临时启用Serial Old来代替CMS。
- 标记-清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次Full GC。
7.G1收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多CPU和大内存的场景下有很好的性能。HotSpot开发团队赋予它的使命是未来可以替换掉CMS收集器。
堆被分为新生代和老年代,其他收集器进行收集的范围都是整个新生代或者老年代,而G1可以直接对新生代和老年代一起回收。
G1把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入Region的概念,从而将原本的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使的可预测的停顿时间模型成为可能。通过记录每个Region垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
每个Region都有一个Remembered Set,用来记录该Region对象的引用对象所在的Region。通过使用Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:为了修正正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提升收集效率。
具备如下特点:
- 空间整合:整体来看是基于"标记-整理"算法实现的收集器,从局部(两个Region之间)上来看是基于"复制"算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个成都为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒。