深入理解Java虚拟机:JVM高级特性与最佳实践-总结-1
- Java内存区域与内存溢出异常
- 运行时数据区域
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 方法区
- OutOfMemoryError异常
- Java堆溢出
- 垃圾收集器与内存分配策略
- 对象是否可以被回收
- 引用计数算法
- 可达性分析算法
Java内存区域与内存溢出异常
运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图所示:
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。 因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,这类内存区域为“线程私有”的内存。
Java虚拟机栈
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
Java堆
对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里"几乎”所有的对象实例都在这里分配内存。
Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
OutOfMemoryError异常
Java堆溢出
Java堆用于储存对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
代码示例中限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOf-MemoryError
可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析叫。
代码清单2-3 Java堆内存溢出异常测试
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOut OfMemoryError
* @author zh
*/
public class HeapooM {
static class oowobject {
}
publie statie woid main(String[] args) {
List<OOMobject> list = new ArrayList<OOMobject>();
while(true) {
list.add (new OONobject ());
}
}
}
运行结果:
java.lang.OutofMemoryError: Java heap space
Dumping heap to java_pid3404.hprof...
Heap dump file created [22045981 bytes in 0.663 secs]
Java堆内存的OutOfMemoryError
异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息"java.lang.OutOfMemoryError"
会跟随进一步提示"Java heap space"
。
要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具(如Eclipse MemoryAnalyzer)对Dump
出来的堆转储快照进行分析。第一步首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(MemoryOverflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots
的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots
相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots
引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx
与-Xms
)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运
行期的内存消耗。以上是处理Java堆内存问题的简略思路。
垃圾收集器与内存分配策略
对象是否可以被回收
引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
循环引用的代码示例如下所示:对象objA
和objB
都有字段instance
,赋值令objA.instance = objB
及objB.instance = objA
,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
/**
*testGc()方法执行后,objA和obB会不会被GC?
*eauthor zh
*/
public class Referencecountinggc {
public Object instance = nul1;
private statie final int _1MB = 1024 * 1024;
/**
*这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过
*/
private byte[] bigsize = new byte[2 * _1MB];
public statie void testGC() {
ReferenceCountingec objA = new ReferenceCountingGC();
ReferenceCountingec objB = new ReferenceCountingGC();
abjA.instance = objB:
objB.instance = objA;
objA = null;
ob}B = null;
//假设在这行发生GC,abjA和objB是否能被回收?
System.gc();
}
}
运行结果:
[Fu11 GC(System)[Tenured: OK->210K(10240K),0.01491428ecs] 4603K->210K(19456K),[Perm:2999K->2999K1
Heap
def new generation total 9216K, used 82K [0x00000000055e0000,0x0000000005fe0000,0x0000000005fe0
Eden space 8192K, 1% used (0x00000000055e0000,0x00000000055f4850,0x0000000005de0000)
from space 1024K, 0% used (0x0000000005de0000,0x0000000005de0000,0x0000000005ee0000)
to space 1024K, 0% used (0x0000000005ee0000,0x0000000005ee0000,0x0000000005fe0000)
tenured generation total 10240K,used 210K[0x0000000005fe0000,0x00000000069e0000,0x00000000069e
the space 10240K, 2% used [0x0000000005fe0000,0x0000000006014a18,0x0000000006014c00,0x00000000
compacting perm gen total 21248K, used 3016K [0x00000000069e0000,0x0000000007ea0000,0x000000000b
the space 21248K,14% used [0x00000000069e0000,0x0000000006cd2398,0x0000000006cd2400,0x00000000
No shared spaces configured.
从运行结果中可以清楚看到内存回收日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就放弃回收它们,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的。
可达性分析算法
当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。可达性分析算法的基本思路就是通过一系列“GC Roots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。
如下图所示,对象object 5、object 6、object 7
虽然互有关联,但是它们到GCRoots
是不可达的,因此它们将会被判定为可回收的对象。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(
String Table
)里的引用。 - 在本地方法栈中
JNI
(即通常所说的Native
方法)引用的对象。 - Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
NullPointExcepiton
、OutOfMemoryError
)等,还有系统类加载器。 - 所有被同步锁(
synchronized
关键字)持有的对象。 - 反映Java虚拟机内部情况的
JMXBean、JVMTI
中注册的回调、本地代码缓存等。
除了这些固定的GC Roots
集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots
集合。