Java 内存从分配到泄露
2017-02-26 19:03
183 查看
引言
如果你被问及Android内存泄露,而你只能背出几种常见的内存泄露的场景以及解决方案,却不能从更深层次的原理上去解释它,那么是时候补一补Java虚拟机的基础知识了。文章较长,请耐心阅读,相信会有所收获。Java内存区域划分
许多程序员将Java内存简单粗暴地分为栈内存和堆内存,事实上,它至少可以分为堆、栈、方法区、本地方法栈等部分。借用《深入理解Java虚拟机》一书中的一张图,看图说话。栈(Stack):
图中称作虚拟机栈(VM Stack),这里简称栈。栈内存用作在方法执行时创建栈帧(Stack Frame)存放局部变量表、操作数栈、动态链接、方法出口等。而局部变量表存放基本数据类型、对象引用、returnAddress类型的数据。方法调用时栈帧进栈,执行结束时栈帧出栈,局部变量自动销毁,不需要GC。想一想递归调用的“栈溢出”就很好理解了,以及Debug时查看的调用栈。栈内存是线程私有的,即每条线程都有独立的存储,各条线程之间互不影响。栈内存的特点是分配效率高但是容量有限。
堆(Heap):
主要存放对象实例和数组,是线程共享的。堆内存中的对象不会随着方法执行结束而销毁,需要GC。它的特点是容量大,是Java内存管理中最大的一块,是GC主要区域,也是内存泄露的主要区域。
方法区(Mehtod Area):
很多人习惯称之为静态区,它存放的是类信息、常量、静态变量、JIT编译后的代码数据等。方法区是线程共享的,需要GC。
本地方法栈(Native Mehtod Stack):
类似栈,线程私有,不需要GC,区别是它服务于JNI。
程序计数器(Computer Counter Register):
当前线程所执行的字节码的行号指示器,线程私有,不需要GC。
由以上划分可知,当我们编写的一个类被加载后,类信息、静态变量、常量等放在方法区;代码中的A a=new A( ),A a部分放在栈中,new A( )部分放在堆中;int b这样的变量也放在栈中。本地方法栈和程序计数器我们不做过多讨论,重点关注栈、堆、和方法区。
对象的创建过程
稍微了解一下对象的创建过程。它大致分为4个步骤:检查类是否被加载,如果未加载则先加载
在堆上为新生对象分配内存
设置对象Header,如对象HashCode、GC分代年龄、类型指针等
执行《init》方法初始化
Java内存回收机制
内存区域划分好了,对象和变量创建好了,程序运行着,部分方法已经执行完了,那么哪些内存需要回收?何时回收?如何回收?已知所有的对象实例和数组都要在堆上分配,那我们重点关注这块最大的内存是如何回收的。堆的回收
C++中,程序员在一个对象使用完毕时手动free/delete并赋值为null来回收对象占用的内存。Java使用众所周知的垃圾回收器来回收对象,那么它如何确定一个对象已经完成使命,可以被回收了呢?引用计数算法
最简单的方法是给每个对象维护一个引用计数器,每当有一个地方引用它时,对应的计数器加1;引用失效时,计数器减1;当计数器小于等于0时,对象就可以被回收了。引用计数算法简单高效,但它最大的问题是无法解决对象之间的循环引用问题。比如以下代码:
A a =new A(); B b =new B(); a.x=b; b.x=a; a=null; b=null;
如上图右半部分,“A的实例”持有“B的实例”的引用,“B的实例”持有“A的实例”的引用。但是没有其他任何地方引用这两个实例。如果采用引用计数法,它们的引用计数器都不为0,垃圾回收器一直都不会回收它们;但实际上系统中并不需要再次使用这两个对象,这势必造成内存泄露——占着茅坑不拉屎。假如系统中有大量的这种循环引用的对象存在,并且每个这样的对象占用的内存都较大,那么系统很快就会因为内存溢出而崩溃。
可达性分析算法
Java虚拟机采用可达性分析算法来判定对象的存活与否。它应用图论的思想,堆中的每个对象实例都是图的一个顶点,一个对象对另一个对象的引用是一条有向边。在图的所有顶点中,以一系列称为“GC Roots”的顶点作为起始点,沿着有向边向下搜索,走过的路径成为引用链(Reference Chain)。当起始点集合到某个顶点不可达,即GC Roots到某个对象不存任何一条引用链时,则证明此对象是不可用的,可以回收。如下图右半部分,可达性分析算法可以解决对象间循环引用的问题。Java中可作为GC Roots的对象包括以下几种:
栈中引用的对象
方法区中静态熟悉引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
到这里,我们已经可以简单解释内存泄露了。原本应该正常回收的对象,如上图右半部分,如果因为某种原因,变成上图左半部分一样,变成GC Roots可达了,那么这些对象就不会被及时回收,造成资源浪费,是为内存泄露。好比是小明用过的厕所,本来应该把门打开,其他人可以接着使用,但是小明不再继续使用,却把门锁了,别人用不了。小明是GC Roots或者和GC Roots连通的对象,厕所是应该回收的对象所占的内存资源。
实际的可达性分析算法比以上分析复杂地多,需要结合Java的强、软、弱、虚四种引用来解释。内存泄露的防治也离不开软引用和弱引用的使用。这个话题将在其他博文中详细阐述。
方法区的回收
方法区的回收极少被人提及,更不会像堆内存的回收一样被大众讨论,但它确实是客观存在。回看方法区存放的内容:类信息、常量、静态变量、JIT编译后的代码数据等,后两者的生命周期贯穿整个应用程序,因此方法区内存的回收内容主要是废弃常量和无用的类。废弃常量
回收废弃常量与回收堆中的对象非常类似,不展开论述。无用的类
回收无用的类即类的卸载,对应于类的加载。满足以下条件的类才可算作无用的类:该类所有实例都已被回收,即Java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收算法
以上只谈到需要回收的内存区域(堆和方法区)中如何判定一个对象可以回收,即内存中哪些是垃圾(Garbage),并未涉及如何进行垃圾回收(Garbage Collection,GC)。这部分内容非常类似《操作系统》或《计算机组成原理》中的存储单元的分配、寻址、清除、替换等操作。不外乎是几种常见的算法,由易到难,最终的实现因厂家而异。一般是难易搭配,集各种算法的有点于一身,但又有所折衷,以实现某几大因素(如时间、空间)的平衡。标记 -
b1ec
清除法
先标记所有需要回收的对象,然后统一清除。优点:算法简单
缺点:
时间上:标记和清除的效率都不高
空间上:标记清除后会产生大量不连续的内存碎片
复制算法
将内存分为大小相等的两块,每次只使用其中的一块,用完后将还存活的对象复制到另外一块,然后把使用过的那块一次清理掉。优点:每次回收整个半区,算法简单;时间上快速高效,空间上不会产生碎片。
缺点:
空间上,内存容量减半,利用率低。
对象存活率高时复制次数增加,效率降低。
标记 - 整理法
标记 - 整理 - 清除:先标记所有需要回收的对象,将所有存活对象移到一端,然后清除端边界以外的内存。优缺点都类似标记 - 清除法,外加移动对象的开销。
分代回收算法
根据对象存活周期讲内存划分新生代、老年代等几块,根据不同年代的特点采用适当的算法。新生代对象存活率低,采用复制算法;老年代对象存活率高,采用标记 - 清理算法或者标记 - 整理算法。
垃圾回收器种类
计算机中常把完成某类功能的程序叫做某某器,英语中就是XXX-er。比如完成编译功能的程序叫编译器Compiler,完成调试功能的程序叫调试器Debugger,以及此处完成垃圾回收功能的Garbage Collector。Serial
最基本、历史最悠久单线程收集器
使用一个CPU或一条线程进行垃圾回收
它进行垃圾回收时,必须暂停其他所有工作线程,Stop The World,直到本次回收结束。
Stop The World,保洁阿姨在帮你打扫办公室时,你需要暂停一切工作离开房间,并且不继续乱扔垃圾,直到打扫结束。
ParNew
Serial回收器的多线程版Parallel Scavenge
新生代收集器,多线程高吞吐量,高CPU利用率
Serial Old
Serial回收器的老年代版本,单线程。Parallel Old
Parallel Scavenge的老年代版本,多线程。CMS
回收停顿时间最短,并发G1
较新较牛逼总结
Java内存划分为栈、堆、方法区等区域,其中栈保存的是方法的局部变量,随方法起随方法灭,不需要GC;堆保存所有对象的实例和数组,是GC和泄露的重点区;方法区保存的是类信息、常量、静态变量等静态信息,也需要GC。堆内存的回收中,判断对象存活的算法有引用计数算法和可达性分析算法,引用计数算法无法解决对象间循环引用的问题,虚拟机通常采用可达性分析算法。
常见的垃圾回收算法有:标记 - 清除法、复制算法、标记 - 整理法、分代回收算法。常见的垃圾回收器种类有:Serial、ParNew、Parallel Scavenge等。
以下内容请听下回分解:
Java强、软、弱、虚四种引用详解
内存泄露的定义、与内存溢出的区别和联系、内存泄露产生的原因
Java和Android常见的内存泄露的情景和解决方案
Android内存泄露检测
防止内存泄露和内存优化的最佳实践等等
相关文章推荐
- 【Java面试整理之JVM】深入理解JVM结构、类加载机制、垃圾回收GC原理、JVM内存分配策略、JVM内存泄露和溢出
- Java内存泄露问题分析
- 讨论:Java内存泄露问题
- java中的内存分配机制(转帖)
- Java 内存分配及String类型详解
- 再谈java的内存泄露
- Java内存泄露问题
- JAVA中的内存分配精讲
- java的内存泄露
- java内存分配初探
- Java的内存泄露 Memory leak of Java
- Java内存泄露问题分析
- JAVA垃圾回收机制与内存泄露问题
- Java内存泄露问题
- 关于java内存泄露
- Java内存泄露问题分析
- JAVA中的内存分配
- 内存动态分配的首先适应、最优适应、最坏适应算法的窗口演示(java版)
- 转一篇有关Java的内存泄露的文章(受益哦)
- Linux下用JMap对Java程序进行性能测试检查内存泄露问题