由于java虚拟机内部是自动分配和回收内存,因此,大部分同学的直观感受是内存是系统自动处理的,程序员无需关注内存问题,其实这是一种错误的观点。
虽然JVM有垃圾回收机制,但并不表示不会出现内存泄露等问题。一旦遇到这些问题,不了解java内存区域及垃圾回收机制,是很难解决的,因此,本文将分析java内存分配和回收机制。
运行时数据区
Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域,主要分为方法区,堆,栈,程序计数器,下图为Java虚拟机中的内存分类。
(1)方法区:主要存放类信息,常量,静态变量,编译器编译后的代码等数据
– 常量池:存放编译器生成的各种字面量和符号的引用,是方法区的一部分
(2)Java堆:对象分配内存的区域
(3)虚拟机栈(Heap): 方法执行的内存区,每个方法执行时都会在虚拟机栈中创建栈帧
(4)本地方法栈:虚拟机Native方法执行的内存区
(5)程序计数器:记录正在执行的虚拟机字节码的地址
其中,1,2是线程共享区,3,4,5是线程私有区。
栈内存
生命周期与线程相同,是Java方法执行的内存模型,每个方法(不包括native方法)执行的时候都会创建一个栈帧结构,方法执行过程,对应着虚拟机栈的入栈到出栈的过程。在函数中定义的一些基本类型的变量和对象的引用都是在函数的栈内存中分配。当在一段代码库中定义一个变量是,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java就会自动释放为该变量分配的内存空间。
堆内存
堆内存用于存放所有由new创建的对象(包括该对象其中的所有的成员变量)和数组。再对中分配的内存,由java虚拟机自动垃圾回收器来管理,在堆中产生一个数组或者对象后,还可以在占中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的手地址,在栈中的这个特殊的变量就叫做引用(有点类似指针),引用相当于为数组或者对象起的一个别名,或者代号。堆是不连续的内存区域(系统用链表来存储空闲内存地址,因此不是连续的),堆大小受限于计算机系统中的有效的虚拟内存。栈是一块连续的内存区域,大小是由操作系统预定好的。
垃圾回收机制
堆内存是由java虚拟机垃圾回收器(GC)来管理,垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。 java语言并没有明确的说明JVM使用哪种垃圾回收算法,但是垃圾回收算法一般主要完成两个基本的事情:发现无用信息对象;回收被无用对象占用的内存空间,使该空间可被程序再次使用。
1. 对象已死?
1.1 引用计算法
算法原理: 给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象是不可能再被使用,需要回收该对象。
缺点:难以解决对象之间相互循环引用的问题。
1.2 可达性分析算法
算法原理: 通过一系列的成为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连,证明该对象是不可用的。
左图中ObjD,ObjE,ObjF三个对象无法到达GC Root,因此这三个对象判定为可回收的对象。
GC Roots:在java中,可作为GC Roots的对象包括下面几种:
A: 虚拟机栈(栈帧中的本地变量表)中引用的对象。
B: 方法区中的静态属性引用的对象。
C: 方法区中的常量引用的对象。
D: 本地方法栈中的JNI(一般说的Native方法)引用的对象。
2. 基本回收策略算法
2.1 标记-清除算法
算法思路: 算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有的被标记的对象,这是最基础的收集算法。
缺点: 一个是效率问题,标记和清楚两个过程的效率都不搞;另外一个是空间问题,标记清除后会产生大量的不连续的碎片空间,因此回收后的空间是不连续的。在对象堆空间 分配过程中,尤其是大对象的内存分配,不连续的内存空间效率要低于连续的空间。
2.2 复制算法
算法思路:将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
缺点: 系统内存折半
2.3 标记-整理算法
算法思路: 此算法结合了"标记-清除"和复制算法的有点,分两个阶段,第一个阶段从根节点开始标记所有的被引用的对象,第二个阶段遍历整个堆,把清除未标记对象并且把存放对象压缩到堆的其中一块,按顺序排放。此算法避免了标记-清除的碎片问题,也避免了复制的算法空间问题。
3 . 分区对待算法
3.1 分代收集
分代的垃圾回收策略,是基于这样一个事实,不同的对象生命周期是不一样的,因此可以采用不同的收集算法,以便提高回收效率。
年轻代:
所有新生成的对象都是首先放到年轻代的。年轻代的目标就是尽可能快速回收掉那些生命周期短的对象。
新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将Eden区存活的对象复制到一个survivor0区, 然后清空Eden区,当这个survivor0也存放满了时,则将Eden区和survivor0区存活的对象复制到另外一个survivor1区,然后清楚Eden和survivor0,此时survivor0是空的,然后将 survivor0和survivor1区交换,即保持survivor1为空。
若是survivor1不足以存放Eden和survivor0的存活对象,那么将存放对象直接放到老年代。若是老年代也满了,那么就会触发一次Full GC,也就是新生代,老年代都进行回收。
新生代发声的GC叫做Minor GC,Minor GC发生频率比较高(不一定Eden满了才触发)。
年老代:
在年轻代经历了N次垃圾回收后仍然存活的对象,就会放到年老代中。因此,可以认为年老代中存放的是一些生命周期较长的对象。
年老代内存比新生代内存要大很多(大概比例是1:2),当老年代内存满时出发Major GC,即Full GC,Full GC发生频率较低,老年代对象存活时间比较长,存活率较高。
持久代:
用于存放静态文件,如Java类,方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,这个时候需要设置一个比较大的持久代空间来存放 这些运行过程中新增的类。
4. 按系统线程分
4.1 串行收集
串行垃圾收集器一次只使用一个线程进行垃圾回收
4.2 并行收集
一次开启多个线程同时进行垃圾回收,使用并行垃圾回收器可以缩短GC的停顿时间。
5. 按照工作模式分
氛围并发式垃圾回收器和独占式垃圾回收器。并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。独占式一旦运行,就停止应用程序中其他所有线程,直到垃圾回收过程结束。
引用类型
Java对象引用分为四种类型,分别是强引用,软引用,弱引用,虚引用。异同点如下表:
只要强引用对象还被使用,垃圾回收器绝不会回收它们!
堆内存中的长生命周期对象持有短生命周期对象的强/软引用,尽管短生命周期的对象已经不再需要了,但是长生命周期对象持有它的引用而导致不能被回收,这就是Java内存泄露的根本原因。