java虚拟机的基本结构
类加载子系统
类加载子系统负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
java虚拟机栈
线程私有,它的生命周期与线程相同。线程执行的基本行为是函数调用,每次函数调用的数据都是通过java栈传递的。当前正在执行的函数锁对应的帧就是当前的帧(位于栈顶),它保存着当前函数的局部变量、中间运算结果等数据。当函数返回时,栈帧从栈中被弹出。Java方法有两种返回函数的方式,一种是正常的函数返回,使用return命令;另一种是逃出异常,不管使用哪种方式,都会导致栈帧被弹出。在一个栈帧中,至少要包含局部变量表、操作数栈和帧数据区几个部分。
局部变量表用于存放方法参数和方法内部定义的局部变量。局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。在java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
在Class文件的局部变量表中,显示了每个局部变量的作用域范围、所在槽位的索引,变量名和数据类型。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非static方法),那么局部变量表中第0位索引默认是用于传递方法所述对象实例的应用,即this关键字。
局部变量表中的Slot是可以重用的,当PC计数器的值超过了某个变量的作用域,则改变量的Slot可以被其他变量覆盖。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都是不会被回收的。
操作数栈,操作数栈的最大深度也是在编译时写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的java数据类型。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容。
栈数据区,Java栈帧还需要一些数据来支持常量池解析、正常方法返回和异常处理等。
在帧数据区中保存着访问常量池的指针,方便程序访问常量池。每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用持有这个引用是为了支持方法调用过程中的动态连接。虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用。如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法。如果符号引用是在类加载阶段或者第一次使用的时候转化为直接引用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。
方法的返回分为两种情况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法。不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,如果是因为异常退出的,则是需要通过异常处理表来确定。方法的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操作,因此方法退出时可能做的事情包括:恢复上层方法的局部变量表以及操作数栈,如果有返回值的话,就把返回值压入到调用者栈帧的操作数栈中,同时还会把PC计数器的值调整为方法调用入口的下一条指令。
异常,在Java 虚拟机规范中,对虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
栈上分配,其基本思想是对于那些线程私有的对象(指不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾收集器的接入,从而提高系统的性能。栈上分配依赖于逃逸分析(-XX:+DoEscapeAnalysis)和标量替换(-XX:+EliminateAllocations,默认打开,允许将对象打散分配在栈上)的实现。
本地方法栈
本地方法栈(Native MethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
java堆
堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。
堆的大小可以通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap大小,通常-Xms与-Xmx的值设成一样。
如果从内存回收的角度看,由于现代收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;
新生代:程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及SurvivorSpace的大小。
老年代:用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:1、大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。2、大的数组对象,且数组中无引用外部对象。
老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
方法区
在JDK 1.6、JDK 1.7中,方法区可以理解为永久区(Perm,可以使用参数-XX:PermSize和-XX:MaxPermSize指定)。在JDK 1.8中,永久区已经被移除,取而代之的是元数据区(-XX:MaxMetaspaceSize指定)。在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在加载类时从.class文件中提取出来的。类(静态)变量也存储在方法区中。
简单说方法区用来存储类型的元数据信息,一个.class文件是类被java虚拟机使用之前的表现形式,一旦这个类要被使用,java虚拟机就会对其进行装载、连接(验证、准备、解析)和初始化。而装载后的结果就是由.class文件转变为方法区中的一段特定的数据结构。这个数据结构会存储如下信息:
类型信息 类型的全限定名,类型的直接超类的全限定名,类型是类类型还是接口类型,类型的访问修饰符,任何直接超接口的全限定名的有序列表
字段信息 字段名,字段类型,字段的修饰符
方法信息 方法名,方法返回类型,方法参数的数量和类型(按照顺序),方法的修饰符
其他信息 除了常量以外的所有类(静态)变量,一个指向ClassLoader的指针,一个指向Class对象的指针,常量池(常量数据以及对其他类型的符号引用)
JVM为每个已加载的类型都维护一个常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(string,integer,和floating point常量)和对类型,域和方法的符号引用。池中的数据项像数组项一样,是通过索引访问的。每个类的这些元数据,无论是在构建这个类的实例还是调用这个类某个对象的方法,都会访问方法区的这些元数据。
构建一个对象时,JVM会在堆中给对象分配空间,这些空间用来存储当前对象实例属性以及其父类的实例属性(而这些属性信息都是从方法区获得),注意,这里并不是仅仅为当前对象的实例属性分配空间,还需要给父类的实例属性分配,即实例化父类的某个子类时,JVM也会同时构建父类的一个对象。从另外一个角度也可以印证这个问题:调用当前类的构造方法时,首先会调用其父类的构造方法直到Object,而构造方法的调用意味着实例的创建,所以子类实例化时,父类肯定也会被实例化。
类变量被类的所有实例共享,即使没有类实例时你也可以访问它。这些变量只与类相关,所以在方法区中,它们成为类数据在逻辑上的一部分。在JVM使用一个类之前,它必须在方法区中为每个non-final类变量分配空间。
方法区主要有以下几个特点:
1、方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待。
2、方法区的大小不必是固定的,JVM可根据应用需要动态调整。同时,方法区也不一定是连续的,方法区可以在一个堆(甚至是JVM自己的堆)中自由分配。
3、方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集。
可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(PermanentGeneration),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常。
在JDK 1.4中新加入的NIO类,引入了一种基于通道和缓冲区的IO方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在java堆和native堆中来回复制数据。
Java的NIO库允许java程序使用直接内存。直接内存是在java堆外,直接向系统申请的内存区间。通常,访问直接内存的速度会优于java堆,但是分配内存的效率低于后者。
直接内存适合申请次数较少、访问较频繁的场合。如何内存空间本身需要频繁申请,则并不适合使用直接内存。
总结
名称 |
特征 |
作用 |
配置参数 |
异常 |
程序 计数器 |
占用内存小,线程私有, 生命周期与线程相同 |
大致为字节码行号指示器 |
无 |
无 |
虚拟机栈 |
线程私有,生命周期与线程 相同,使用连续的内存空间 |
Java 方法执行的内存模型,存储局部变量 表、操作栈、动态链接、方法出口等信息 |
-Xss |
StackOverflowError OutOfMemoryError |
java堆 |
线程共享,生命周期与虚拟机相 同,可以不使用连续的内存地址 |
保存对象实例,所有对象实例(包括 数组)都要在堆上分配 |
-Xms 最小 -Xmx 最大 -Xmn 新生代 |
OutOfMemoryError |
方法区 |
线程共享,生命周期与虚拟机相 同,可以不使用连续的内存地址 |
存储已被虚拟机加载的类信息、常量、静 态变量、即时编译器编译后的代码等数据 |
-XX:PermSize:16M -XX:MaxPermSize:64M |
OutOfMemoryError |
运行时 常量池 |
JDK1.6之前是方法区的一部分, JDK1.7被移到堆中管理,具有动态性 |
存放字面量及符号引用 |
|
|
HotSpot虚拟机对象
对象的创建
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。Java堆是否规整是由采用的垃圾收集器是否带有压缩整理功能决定。即使是指针的移动类分配内存,也需要同步,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲。
内存分配过程
1、JVM 会试图为相关Java对象在Eden Space中初始化一块内存区域。
2、当Eden空间足够时,内存申请结束;否则到下一步。
3、JVM 试图释放Eden中所有不活跃的对象(这属于1或更高级的垃圾回收)。释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。
4、Survivor区被用来作为Eden及Old的中间交换区域,当Old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区。
5、当Old区空间不够时,JVM 会在Old区进行完全的垃圾收集(0级)。
6、完全垃圾收集后,若Survivor及Old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现“outofmemory”错误。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。保证对象的实例字段在java代码中可以不赋初始值就直接使用。
接下来,虚拟机要对对象进行必要的设置。例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
最后,执行new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化。
内存分配策略
1. 优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次minor GC。Minor GC后存活的对象会被移到第一个Survivor区,并清除Eden区;当下一次Eden区又没有足够空间进行分配时,再次发起Minor GC,对Eden区和第一个Survivor区进行垃圾回收,并将存活的对象移到第二个Survivor区,同时清除Eden区和第一个Survivor区。
新生代GC (Minor GC):指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC (Major GC / Full GC):指发生在老年代的GC,出现了Major GC, 经常会伴随至少一次的Minor GC(但也非绝对的,在Parallel Scavenge收集器的手机策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般比Minor GC慢10倍以上。
2. 大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的java对象,最典型的大对象就是那种很长大的字符串及其数组。
3. 长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器。如果在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。
4. 动态对象年龄判断
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一般,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
5. 空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这是也要改为进行一次Full GC。
对象的内存布局
HotSpot虚拟机中,对象的内存布局可以分为3块区域:对象头,实例数据和对齐填充。
对象头包括两部分:第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据是对象真正存储的有效信息,也就是程序中所定义的各种类型的字段内容。
对齐填充是由于HotSpot的自动内存管理系统要求对象起始地址必须是8字节的整数倍。如果对象实例数据部分没有对齐的,就需要通过对齐填充来补全。
对象的访问定位
对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区域之间的关联关系,如下面的这句代码:
Object obj = new Object();
假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的局部变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。
由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。
如果使用句柄访问方式,Java 堆中将会划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
如果是指针访问方式,java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
OOM异常
java堆溢出
设置java堆大小为比较小的值(-Xms和-Xmx),不断创建对象,保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会产生内存溢出异常。要解决这个区域的异常,一般通过dump来分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄露还是内存溢出。
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收他们。
如果不存在泄露,就是内存中的对象却是还必须存活着,那就应该检查虚拟机的堆参数,与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
虚拟机栈和本地方法栈溢出
对于HotSpot来说,栈容量只由-Xss参数设定。如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
设置参数-Xss比较小,通过递归调用可以产生StackOverflowError异常。在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOutOfMemoryError异常。
通过不断地建立线程的方式倒是可以产生内存溢出异常。如果为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。因为系统分配给每个进程的内存是有限制的,而虚拟机提供了参数来控制java堆和方法区的这两部分内存的最大值,剩余内存为进程最大内存-Xmx(最大堆容量)-MaxPermSize(最大方法区容量),程序计数器消耗内存可以忽略。
一般来说StackOverflowError异常不太会发生(默认的栈深度在大多数情况下够用),即使发生,也可以通过发生时的错误堆栈查找问题所在。
方法区和运行时常量池溢出
JDK 1.6及之前的版本中,常量池分配在永久代(在方法区内)内,我们可以通过设置-XX:PermSize和-XX:MaxPermSize限制方法去大小,从而间接限制其中常量池的容量。再通过String.valueof(i++).intern()方法就可以产生OOM异常。而使用JDK 1.7运行同样代码就不会得到相同结果。因为从1.7开始逐步“去永久代”。
JDK 1.6中intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用。而JDK1.7中intern()方法不会再复制实例,只是在常量池中记录首次出现的实例的引用。
// JDK 1.6输出false,而JDK 1.7输出true;
String str1 = new StringBuilder(“abc”).append(“def”).toString();
System.out.println(str1.intern() == str1);
方法区,存放的是Class相关信息,如类名,访问修饰符、常量池、字段描述、方法描述等。对于方法区的测试,基本思路是运行时产生大量的类去填满方法区,直到溢出。
本机直接内存溢出
DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定,默认与java堆最大值(-Xmx)一样。可以通过反射获取Unsafe实例进行内存分配来抛出OOM异常。由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常。如果发现OOM之后Dump文件很小,而程序中又直接或者间接使用了NIO,那可以考虑检查一下是不是直接内存溢出。
内存溢出和内存泄漏
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out ofmemory。
Java 堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heapspace”。
要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。