1,JVM 的内存布局/内存模型
Java虚拟机(Virtual Machine)所管理的内存包括的运行时数据区域(如下图):
1)程序计数器(Program Counter Register)
程序计数器是一个比较小的内存区域,是线程隔离的。
①功能
指示当前线程所执行的字节码的行号。
②JVM的多线程的实现
线程轮流切换并分配处理器执行时间。在任何一个确定的时刻,一个CPU(内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,故这类内存区域为“线程私有”内存。
如果线程正在执行一个:
i> Java方法,PCR记录的是正在执行的VM字节码指令地址。
ii> Native方法,PCR值为Undefined。
③特点
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2)虚拟机栈(VM Stack)
①概念
线程私有,生命周期与线程相同。
Java的内存并非简单的区分为Heap和Stack,实际远比这复杂。Stack指的是VM Stack,或者说是虚拟机中局部变量表部分。
②功能
成为栈帧,存放方法运行时所需的数据。
VM Stack描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表(局部基本类型的变量、局部对象的引用变量,但引用指向的对象存在于堆中)、操作数栈(操作指令区)、动态链接、方法出口(执行环境上下文)等信息。每一个方法从调用直至执行完成的过程,对应一个Stack Frame在VM Stack中入栈出栈的过程。
局部变量表所需的内存空间在编译期间完成分配,进入方法时,方法所需在Stack Frame中分配的局部变量的空间是确定的,且在运行期间大小不变。
③栈帧(stack frame)
用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
i>存储局部变量表(局部基本类型的变量、局部对象的引用变量,但引用指向的对象存在于堆中)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。
局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。
Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。
系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
ii>操作数栈(操作指令区)
Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。
操作数栈也常被称为操作栈。
和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。
虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:
a.实际字节码运算:
int a = 1;
int b = 2;
int c = a + b; //局部变量表存储3个变量{a=1,b=2,c=?}
begin
iload_0 // 将存储在局部变量中索引为0的整数:1 ,压入操作数栈中
iload_1 // 将存储在局部变量中索引为1的整数:2 ,压入操作数栈中
iadd // iadd指令从操作数栈中弹出前面两个整数相加,再将结果压入操作数栈
istore_2 // 从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置
end
b.i++和++i的字节码运算:
java中,编译器会优化i++和++i,字节码是一样的。但是从原理上将,++i性能更高一点,显示声明能确保性能更加稳定。
++i:将数据加载到内存,自增、返回;
i++:将数据加载到内存,创建一个新对象+1,返回。
int j = 0;
int x = j++;
x = ++j;
//j = 0
iconst_0 //把常量0放到操作数栈顶
istore_1 //0弹出并存储到局部变量区索引为1的位置,此时j = 0;
//j++
iload_1 //先取出j的值0压栈
iinc 1, 1 //j++,把局部变量表中j的值加1,j=1,此时操作数栈顶的值还是0
//++j
iinc 1, 1 //++j,先把局部变量表中j的值加1(本来是1),此时j=2
iload_1 //取出j的值2
istore_2 //x=2
return //方法返回
c.i=i++的字节码运算:
int i = 0; i = i++;
iconst_0 //将常量0放到操作数栈顶 操作数栈:[0]
istore_1 //栈顶元素0弹出,赋值给局部变量表索引为1的变量 操作数栈:[] 局部变量表:i=0
iload_1 //取出局部变量表中索引为1的变量值 操作数栈:[0]
iinc 1, 1 //把局部变量表中索引1的值加1(第二个数) 操作数栈:[0]
istore_1 //栈顶元素0弹出,赋值给局部变量表索引为1的变量(本来i=1,赋值后i=0,相当于把局部变量表中之前的值1覆盖掉了)
iload_1 //把局部变量表中的0再存入栈中#
iii>动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。
这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。
另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
iv>方法出口(执行环境上下文)
即方法返回地址。
当一个方法被执行后,有两种方式退出这个方法:
- 正常完成出口:执行引擎遇到任意一个方法返回的字节码指令,返回给上层的方法调用者。
调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。 - 异常完成出口:在方法执行过程中遇到了没有处理的异常,导致方法退出。不会给它的调用都产生任何返回值的。
返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。
无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行。
一般来说,方法正常退出时,而方法异常退出时,
④异常情况
StackOverflowError异常:线程请求的栈深度大于VM所允许的深度。
OutOfMemoryError异常:JVM Stack动态扩展时,无法申请到足够的内存。
3)本地方法栈(Native Method Stack)
功能:为VM使用Native方法服务。
其他与VM Stack一致。
4)方法区(Method Area,静态存储区)
①概念
与Java Heap一样,是各个线程共享的内存区域。是线程共享的。
②功能
存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
③运行常量池(Runtime Constant Pool)
是方法区的一部分。
Class文件:包含类的版本、字段、方法、接口等描述信息,还包括常量池(Constant Pool Table)信息,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法的运行时常量池中存放。
④OutOfMemoryError异常
常量池无法再申请到内存时抛出。
5)堆(Heap,GC堆)
所有线程共享,存储已被VM加载的类信息、常量、静态变量,即时编译器编译后的代码等数据。这个区域的内存回收目标主要是针对常量池的对象的回收和堆类型的卸载。
①功能
存放对象实例(类对象、类成员变量)、数组。
②特点
由于JIT编译器的发展与逃逸分析技术的成熟,栈上分配、标量替换优化技术的发生,对象的分配也不一定就在堆上。
Java堆是GC管理的主要区域,故很多时候被称为“GC堆”。
Java堆可以处于物理上不连续的内存空间中。
Java Heap是JVM所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在VM启动时创建。
对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈(即引用变量:取值等于数组或者对象在堆内存中的首地址),如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。
③堆、栈、方法区 区别
栈 | 堆 | 方法区 |
---|---|---|
线程私有 | 线程共享 | 线程共享 |
存储局部变量表(局部基本类型的变量、局部对象的引用变量,但引用指向的对象存在于堆中)、操作数栈(操作指令区)、动态链接、方法出口(执行环境上下文) | 对象实例(类对象、类成员变量)、数组 | 类信息、常量、静态变量 |
超过作用域后就无效了,释放内存空间 | 由 GC 来自动管理 |
举例如下:
class A {
private String a = “aa”;//类中的成员变量,存放在堆区
public boolean methodB() {
String b = “bb”;//b、c都是方法中的局部变量,存放在栈区
final String c = “cc”;
}
}
public class Sample {
int s1 = 0;//存在于堆
Sample mSample1 = new Sample();//存在于堆
public void method() {
int s2 = 1;//s2存在于栈
Sample mSample2 = new Sample();//mSample2存在于栈,但mSample2指向的对象是存在于堆上
}
}
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。
成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。
好的编码习惯:尽可能使用局部变量。
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。
2,内存管理
1)概念
Java的内存管理就是对象的分配和释放问题。
2)内存分配
由程序完成,通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。
①类变量和实例变量
类变量
当类创建好后,类变量也随之创建好,无论创建多少个实例来引用访问类变量,底层都是对本类的引用。
实例变量
为java对象所有,每次创建Java对象都会为它分配内存,并初始化。
②基本类型与引用类型
基本类型
在栈中分配内存,值保存于栈中。
引用类型
在栈中保存变量指针,指向堆中的对象。
3)内存的释放
由GC完成。
3,GC(垃圾回收机制)
1)概念
①垃圾
在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。
有2种情况:
i>一个或一组对象为不可到达状态
即一个对象不被任何引用所指向,如:
一个是给对象赋予了空值null,以下再没有调用过。
另一个是给对象赋予了新值,这样重新分配了内存空间。
ii>一组对象中只包含互相的引用,而没有来自它们外部的引用
2)GC功能
①释放没用的对象
JVM的一个系统级线程会自动释放该垃圾内存块
②清除内存纪录碎片
3)GC优缺点
优点
①自动释放内存空间,减轻编程的负担
②保护程序的完整性, 防止OOM
缺点
①影响程序性能。
Java虚拟机必须追踪运行程序中有用的对象,而且最终释放没用的对象。这一个过程需要花费处理器的时间。
②并不能保证100%收集到所有的废弃内存。
在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象。而java的垃圾回收机制虽然解决了C++中令人头痛的垃圾回收问题,但同时由于它不是主动性的行为,完备性欠缺。另外,也有垃圾回收器不能处理的垃圾回收情况。
4)JVM堆内存分类
JVM堆内存分为2块:Permanent Space 和 Heap Space。
Permanent Space存放的主要是持久代,与垃圾收集器要收集的Java对象关系不大。
Heap = { Old + NEW ={Eden, from, to} },Old 即 年老代(Old Generation),New 即 年轻代(Young Generation)。年老代和年轻代的划分对垃圾收集影响比较大。
1>持久代(Permanent Generation)
主要存放的是Java类定义信息:静态文件,如Java类、方法。
持久代如果满了,将触发Full GC。
持久代溢出原因:动态加载了大量Java类而导致溢出。如使用CGLib技术直接操作字节码运行,生成大量的动态类,会导致持久区jvm堆内存溢出。
2>年轻代(Young Generation,新生代)
存放新分配的对象。
- Eden区
所有对象创建。
当Eden区内存满后触发Minor GC,标记复制算法进行GC(非存活对象标记删除,eden区、非空闲Survivor区的存活对象被移到另一个存活的Survivor区中) - Survivor区(from Survivor、to Survivor)
当Survivor区满了,通过Minor GC将对象复制到老年代。
相关配置
默认情况下,eden区和from Survivor\to Survivor 区空间大小比例:8:1:1,可以通过-XX:SurvivorRatio
参数调整。
3>老年代(Old Generation)
存放长期存在的对象。在年轻代中经历了N次(可配置)GC仍存活的对象,就会被复制到年老代中。
老年代满了后触发Full GC,使用【标记整理算法】针对整个堆(包括新生代、老年代、持久代)进行垃圾回收,在full GC之前会触发minor GC,eden区所有数据清空复制放入Survivor区;同时老年代会被清空。
代码里显式的调用System.gc()
,可能会触发Full GC也可能不会。
年老代溢出原因:循环上万次的字符串处理、创建上千万个对象、在一段代码内申请上百M甚至上G的内存。
a)进入老年代的条件
满足一个即可进入老年代。
- 躲过15次GC(jdk8默认)
通过-XX:MaxTenuringThreshold来设置这个参数。注意:这个参数范围:0~15次,如果设置为0或者负数对象会直接进入老年代,对象的GC年龄存储在对象头中,4bit位,最大支持15位。 - 动态对象年龄判断:某一批对象总是大于Survivor区的50%,那么比这些对象老的对象就会进入老年代。
- 大对象直接进入老年代。
避免在Eden区及两个Survivor 区之间发生大量的内存复制。
通过-XX:PretenureSizeThreshold
参数设置大对象临界值(默认3MB)。
5)GC算法
①引用计数法
概念
GC中早期策略。现在几乎不用该算法。
堆中每个对象实例都有一个引用计数,当计数为0时GC。
优缺点
优点:快速。
对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。
如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.
②tracing算法(标记清除算法 mark and sweep)
概念
把所有的引用关系看作图。
从一个节点GC Root开始,寻找对应的节点,找到这个节点以后,继续寻找这个节点的引用节点并标记,当所有节点被寻找完毕后,剩余的节点没有被引用到的节点,即无标记的节点,是无用的节点。
可做GC Root的对象有:
JVM栈中引用的对象(本地变量表)
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象(Native对象)
场景
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多且较稳定的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
③compacting(标记整理法,full GC使用)
概念
与标记清除法一样,不同的是,在清楚后,回收不存活对象的控件,并移动存活对象往左端空闲空间移动,并更新指针。
优缺点
进行了对象的移动,成本更高,但是却解决了内存碎片的问题。
④copying(标记复制法,minor使用)
将每个活对象赋值到空闲面,空闲面做对象面,对象面变成空闲面,程序在新的对象面分配内存。
当对象不稳定时,比标记法效率高。
5)主动GC
①System.gc()方法
调用System.gc()可以主动请求垃圾回收机制,但也仅仅是一个请求。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。
良好的编程习惯是:
GC调用不宜过多,保持代码健壮(记得将不用的变量置为null、资源使用完后释放),让虚拟机去管理内存。因为System.gc()方法会触发full GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。
②finalize()方法
一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法进行一些必要的清理工作。只有到下一次再进行垃圾回收动作的时候,才会真正释放这个对象所占用的内存空间(所以调用finalize()并不一点会GC)。
另外finalize()函数是在垃圾回收器准备释放对象占用的存储空间的时候被调用的,绝对不能直接调用finalize(),所以应尽量避免用它。
finalize生命周期流程:
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。在C++中所有的对象运用delete()一定会被销毁,而JAVA里的对象并非总会被垃圾回收器回收。
6)触发主GC(Garbage Collector)的条件
①当应用程序空闲时,即没有应用线程在运行
因为GC在优先级最低的守护线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
②Java堆内存不足
主GC的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主GC是反复进行的。且GC仅与内存有关,与其它命令或操作都无关。
7)其他减少GC开销的措施
①减少临时对象创建
临时对象在跳出函数调用后,会成为垃圾。
尽量不在for循环中创建对象
②使用StringBuffer代替String
③用基本类型代替包装类型
④尽量少用静态对象变量
静态变量属于全局变量,不会被GC回收,它们会一直占用内存。
⑤分散对象的创建或删除
集中在短时间内大量创建、删除对象,特别是大对象,会导致突然需要、GC大量内存,JVM在面临这种情况时,只能进行full GC,以回收内存或整合内存碎片,从而增加主GC的频率。
4,4种引用:强引用、软引用、弱引用、虚引用
1)强引用(StrongReference)
①概念
指程序代码中普遍存在的,类似 Object a = new Object() 这类的引用。
②GC
只要引用还存在,垃圾收集器永远不会回收掉该引用对象所占内存
③实现
对象的一般状态。
2)软引用(WeakReference)
①概念
用来描述一些还有用,但并非必需的对象。
②GC
在内存不足时GC。
③场景
缓存:软引用可用来实现内存敏感的高速缓存。
对于Bitmap的加载,非常耗费时间,我们希望把加载过的Bitmap做缓存来节省加载的时间。
可是Bitmap非常的吃内存,我们不希望OOM,所以使用软引用。
④实现
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
⑤举例——图片缓存
假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:
首先定义一个HashMap,保存软引用对象。
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
再来定义一个方法,保存Bitmap的软引用到HashMap。
使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。
如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
3)弱引用(SoftReference)
①概念
用来描述非必需对象。
②GC
第二次GC时回收。
GC工作时,无论当前内存时候足够,都将其回收。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
③场景
主要作用:可以防止内存泄漏。
主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
全局的Map对象用于保存某种映射的时候 一定一定要用弱引用来保存对象,因为全局变量一般是static的,他的生命周期一定长于单个对象,如果用弱引用来保存对象,当对象回收时,如果是强引用,就会发生内存泄漏。
④实现
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
4)虚引用(PhantomReference)
①概念
它是最弱的一种引用关系。不会对对象生存时间构成影响,也无法通过虚引用来取得对象实例。
②GC
每次GC时都回收,无法通过引用取到对象值,被GC时收到一个系统通知。
③场景
使用虚引用完成对象回收后的资源释放工作。
FileCleanTracker,将一个文件与一个对象关联起来,当这个对象被回收之后,把文件也删掉。
主要用于检测对象是否已经从内存中删除。
④实现
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
5,内存泄漏(OOM,OutOfMemoryError)
1)概念
内存泄漏是指无用对象持续占有内存导致内存一直得不到释放。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:
①这些对象是可达的,即在有向图中,存在通路可以与其相连;
②这些对象是无用的,即程序以后不会再使用这些对象。
如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
2)危害
虚拟机占用内存过高,导致OOM(内存溢出),程序出错。
多次内存泄漏之后,程序占用内存超过系统限制,FC(force close)。
在编程时,资源在使用完毕后应该主动关闭,或设成null,注意资源的释放,垃圾收集器就会自动收集。
3)原因
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。
具体主要有如下几大类:
①静态变量引起内存泄漏
主要原因是静态变量和所持有对象生命周期不一致而导致内存泄露。
为了防止OOM,尽量少地使用静态持有的变量,以避免发生内存泄露。在适当的时候将静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。
不要在类初始时初始化静态成员。可以考虑lazy初始化。 架构设计上要思考是否真的有必要这样做,尽量避免。、
i>静态集合类引起内存泄漏
像HashMap、Vector等的使用最容易出现内存泄漏,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
好的编程习惯是:如果对象加入到Vector 后,还必须从Vector 中删除,或者直接将Vector对象设置为null。
例如:
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。
为了防止OOM,必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
ii>集合中的对象未清理造成内存泄露
如果一个集合类是静态的(缓存HashMap),只有添加方法,没有对应的删除方法,会导致引用无法被释放,引发内存泄漏。
所以在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏。
iii>不正确使用单例模式
单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。
例如:单例持有Context(Activity),使得Activity对象无法被及时释放。
创建单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。
2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。
②资源未关闭或释放导致内存泄露
i> 在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。
这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。
即使设成null,只要未close,就可能造成OM。因为这些都使用了缓冲区。
好的编程习惯是:不使用流、cursor、File等带缓冲区的变量,需要close掉。
ii> 对于使用了BroadcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
iii>图片引用后未释放内存
图片引用后在不使用时调用recycle()释放内存。
无论是使用layout布局设置了背景还是使用了setBackgroundResource 设置背景,都需要释放内存,否则会造成OOM。
无论你是在xml中布局使用了:android:background
;
还是在java代码中调用了以下方法设置了背景图片:
setBackground(background);
setBackgroundDrawable(background)
setBackgroundResource(resid)
使用的时候,请调用一下对应的方法:
setBackgroundResource和android:background →setBackgroundResource(0);
setBackgroundDrawable(background)→
setBackgroundDrawable(null)
setBackground(background)
→ setBackground(null)
然后再onDestory中调用System.gc();
同理:对于imageView.setImageResource(0);
这种方式,来释放我们设置的android:src或者bitmap等等。
③匿名内部类/非静态内部类
非静态内部类(包括匿名内部类)默认就会持有外部类的引用(静态的内部类不会持有外部类的引用),当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。
通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式(见handle例子)。
好的编程习惯是:使用静态内部类/匿名类,不要使用非静态内部类/匿名类。
此静态实例会阻止垃圾回收。非静态内部类里有静态实例。
i> 监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
好的编程习惯是:不用监听器的时候,setListener(NULL)
好的编程习惯是:进行socket连接、数据库连接时,在try里面去连接,finally里去关闭连接。
ii> Handler
Handler作为内部类存在于Activity中,但是Handler生命周期与Activity生命周期往往并不是相同的,比如当Handler对象有Message在排队,则无法释放,进而导致本该释放的Acitivity也没有办法进行回收。
避免:
在 Activity 中避免使用非静态内部类,比如将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用(new WeakReference(activity))的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:
对 Handler 持有的对象使用弱引用,这样在回收时也可以回收 Handler 持有的对象,但是这样做虽然避免了 Activity 泄漏,不过 Looper 线程的消息队列中还是可能会有待处理的消息,所以我们在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列 MessageQueue 中的消息。
public class SampleActivity extends Activity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
iii>Thread、AsyncTask
举例:继承实现Activity/Fragment/View时使用了匿名类,并被异步线程持有了:
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();//无影响
Runnable ref2 = new Runnable() {//持有MainActivity.this。如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。
@Override
public void run() {
}
};
...
}
好的编程习惯:如果一个线程不依赖与Activity,我们还可以使用Service来执行后台任务,然后用BroadcastReceiver来向Activity报告结果。
iv>TimerTask
例如:BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap。
④override finalize()
i> 错误的覆写了finalize()方法,finalize()方法执行执行不确定,可能会导致引用无法被释放。
ii> finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize 方法,再次被 GC 时也不会再执行了,原因是:
含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该Object的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。
iii> 含有Finalize方法的object需要至少经过两轮GC才有可能被释放。
⑤属性动画导致的OOM
Android3.0开始,有了属性动画。
属性动画中有一类无限循环的动画,如果在Activity中播放了但未在onDestroy中停止动画,则Activity的View会被动画持有,最终Activity无法释放,泄漏当前Activity。
⑥构造Adapter时,没有使用缓存的 convertView
4)常见垃圾收集器(Garbage Collector)
1>串行垃圾回收器(Serial GC)
单线程串行GC,适合GC不频繁的情况。使用标记-复制法。
Serial Old是老年代的回收,用的标记-整理法。
ParNew是Serial的并行GC,使用标记-复制法。
2>并行垃圾回收器(Parallel GC)
java8:
- Parallel Scavenge GC(新生代)
多线程,标记-复制法。高吞吐量,适合后台执行。 - Parallel Old GC(老年代)
标记-整理法,高吞吐量。
使用可达性分析识别垃圾。
STW(Stop-The-World机制)
在执行GC算法时,Java应用程序的其他所有线程都被挂起(除了GC线程之外)。全局暂停现象。
如果不暂停,程序在运行过程中会不断产生对象,势必会导致两种情况:漏标、多标。STW无法避免,只能尽量缩短。
可达性分析算法进行垃圾标记:
缺点:
- 循环引用
- 误标记问题
并行情况下,一个线程遍历对象图一个线程修改对象图,会导致遍历结果不准确。- STW时间长,为了避免对象状态的改变整个过程都需要STW,导致GC停顿很长时间。
3>并发标记扫描GC(CMS GC)
老年代收集器。并发、标记-清除算法。
4>G1 GC
jdk7推出,jdk9设置为默认。
全堆进行GC,年轻代:标记-复制法;老年代:标记-整理法,堆内存基本要求:4G。
使用三色标记法识别垃圾。
适用于:大型内存环境,不会产生内存碎片。
三色标记法:
优化了可达性分析垃圾标记法。
对象分为3种状态:
- 白色
对象没有被标记过。- 灰色
对象标记过了,但是对象的引用对象还没有标记完。- 黑色
对象及其引用对象都标记完了。标记过程可以分为3个阶段:
- 初始标记
遍历所有根对象,将根和直接引用对象标记为灰色;
这个阶段不会扫描整个堆;- 并发标记
遍历整个对象图,被引用对象标记为灰色;已经遍历过的对象标记为黑色;
遍历过程中不需要STW,可能会修改对象图,通过写屏障(write barrier)技术保证并发标记的正确性。- 重新标记
标记在并发标记阶段修改了的对象、为遍历的对象。重复并发标记的步骤。
5>ZGC(The Z GC)
jdk11推出,默认还是G1。
保证高吞吐量的同时,保证低停顿。
6,android内存优化
1)节制的使用Service——用IntentService替代
如果应用程序需要使用Service来执行后台任务的话,只有当任务正在执行的时候才应该让Service运行起来。通过使用IntentService,当后台任务执行结束后会自动停止,避免了Service的内存泄漏。
当启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,系统可以在LRUcache当中缓存的进程数量也会减少,导致切换程序的时候耗费更多性能。
2)界面不可见时释放内存
当用户打开了另外一个程序,我们的程序界面已经不可见的时候,我们应当将所有和界面相关的资源进行释放。
重写Activity的onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发说明用户离开了程序,此时就可以进行资源释放操作了。
onTrimMemory:Application、Activity、Fragement、Service、ContentProvider都提供相关回调
Android 4.0 之后提供的一个API。
主要作用是提示开发者在系统内存不足的时候,通过处理部分资源来释放内存,从而避免被 Android 系统杀死。这样应用在下一次启动的时候,速度就会比较快。
TRIM_MEMORY_UI_HIDDEN 表示应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键导致应用的UI界面不可见.这时候应该释放一些资源.
TRIM_MEMORY_RUNNING_MODERATE 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。
TRIM_MEMORY_RUNNING_LOW 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能。
TRIM_MEMORY_RUNNING_CRITICAL 表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。
3)当内存紧张时释放内存
onTrimMemory()方法还有很多种其他类型的回调,可以在手机内存降低的时候及时通知我们,我们应该根据回调中传入的级别来去决定如何释放应用程序的资源。
4)避免在Bitmap上浪费内存
①使用压缩
读取一个Bitmap图片的时候,千万不要去加载不需要的分辨率。可以压缩图片等操作。
较大图片可通过BitmapFactory缩放后再使用。
②使用软引用
android系统给图片分配的内存只有8M, 图片尽量使用软引用。
③及时recycle
Bitmap 对象在不使用时,先调用 recycle() 释放内存,然后才它设置为 null。
因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。
5)优化数据集合
①使用优化后的数据集合
Android提供了一系列优化过后的数据集合工具类,如SparseArray、SparseBooleanArray、LongSparseArray,使用这些API可以让我们的程序更加高效。HashMap工具类会相对比较低效,因为它需要为每一个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。
②构造adapter时使用缓存contentview
构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用 ViewHolder。
③增强for循环的使用
ArrayList手写的循环比增强型for循环更快,其他的集合没有这种情况。因此默认情况下使用增强型for循环,而遍历ArrayList使用传统的循环方式。
6)知晓内存的开支情况
使用枚举通常会比使用静态常量消耗两倍以上的内存,尽可能不使用枚举;
任何一个Java类,包括匿名类、内部类,都要占用大概500字节的内存空间;
任何一个类的实例要消耗12-16字节的内存开支,因此频繁创建实例也是会在一定程序上影响内存的;
使用HashMap时,即使你只设置了一个基本数据类型的键,比如说int,但是也会按照对象的大小来分配内存,大概是32字节,而不是4字节,因此最好使用优化后的数据集合。
7)谨慎使用抽象编程
在Android使用抽象编程会带来额外的内存开支,因为抽象的编程方法需要编写额外的代码,虽然这些代码根本执行不到,但是也要映射到内存中,不仅占用了更多的内存,在执行效率上也会有所降低。所以需要合理的使用抽象编程。
8)尽量避免使用依赖注入框架
使用依赖注入框架貌似看上去把findViewById()这一类的繁琐操作去掉了,但是这些框架为了要搜寻代码中的注解,通常都需要经历较长的初始化过程,并且将一些你用不到的对象也一并加载到内存中。这些用不到的对象会一直站用着内存空间,可能很久之后才会得到释放,所以可能多敲几行代码是更好的选择。
9)谨慎使用多进程
多数应用程序不该在多个进程中运行的,一旦使用不当,它甚至会增加额外的内存而不是帮我们节省内存。
这个技巧比较适用于哪些需要在后台去完成一项独立的任务,和前台是完全可以区分开的场景。比如音乐播放,关闭软件,已经完全由Service来控制音乐播放了,系统仍然会将许多UI方面的内存进行保留。在这种场景下就非常适合使用两个进程,一个用于UI展示,另一个用于在后台持续的播放音乐。
关于实现多进程,只需要在Manifast文件的应用程序组件声明一个android:process属性就可以了。进程名可以自定义,但是之前要加个冒号,表示该进程是一个当前应用程序的私有进程。
10)避免创建不必要的对象
在循环内尽量不要使用局部变量;
如果有需要拼接的字符串,那么可以优先考虑使用StringBuffer或者StringBuilder来进行拼接;
尽量使用基本数据类型来代替封装数据类型,如int比Integer要更加有效;
两个平行的数组要比一个封装好的对象数组更加高效,举个例子,Foo[]和Bar[]这样的数组,使用起来要比Custom(Foo,Bar)[]这样的一个数组高效的多。
11)及时释放不使用的资源
不用的对象及时释放,即指向NULL;
数据库的cursor及时关闭;
及时关闭InputStream/OutputStream。
12)在onResume中注册的内容,要在onPause中解绑
调用registerReceiver()后在对应的生命周期方法中调用unregisterReceiver()
13)尽量避免static成员变量引用资源耗费过多的实例
①静态优于抽象,使用静态方法
如果只是想调用一个类中的某些方法,使用静态方法,调用速度提升15%-20%,同时也不用为了调用这个方法去专门创建对象了,也不用担心调用这个方法后是否会改变对象的状态(静态方法无法访问非静态字段)。
②对常量使用static final修饰符
所有的常量都会在dex文件的初始化器当中进行初始化。
static final int intVal = 42;
static final String strVal = "Hello, world!";
当我们调用intVal时可以直接指向42的值,而调用strVal会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。
这种优化方式只对基本数据类型以及String类型的常量有效,对于其他数据类型的常量是无效的。
14)多使用系统封装好的API
系统提供不了的Api完成不了我们需要的功能才应该自己去写,因为使用系统的Api很多时候比我们自己写的代码要快得多,它们的很多功能都是通过底层的汇编模式执行的。 举个例子,实现数组拷贝的功能,使用循环的方式来对数组中的每一个元素一一进行赋值当然可行,但是直接使用系统中提供的System.arraycopy()方法会让执行效率快9倍以上。
15)避免在内部调用Getters/Setters方法
面向对象中封装的思想是不要把类内部的字段暴露给外部,而是提供特定的方法来允许外部操作相应类的内部字段。但在Android中,字段搜寻比方法调用效率高得多,我们直接访问某个字段可能要比通过getters方法来去访问这个字段快3到7倍。但是编写代码还是要按照面向对象思维的,我们应该在能优化的地方进行优化,比如避免在内部调用getters/setters方法。
7,计算机可存储数据的地方
1)寄存器
最快的存储区,位于处理器内部。
数量有限,根据需求分配。
java不能控制寄存器存储,但C/C++可以建议寄存器的分配方式。
2)堆栈
位于RAM(随机访问存储器)中,速度仅次于寄存器。
通过堆栈指针操作:堆栈指针下移,分配新的内存;上移,释放内存。java系统必须明确存储在堆栈中的所有项的确切生命周期,以便上下移动指针。这般限制了程序的灵活性,故java对象不存储在堆栈中,仅保存对象引用。
3)堆
位于RAM中,用于存放Java对象。
相比堆栈,编译器不需要知道存储在堆中的所有项生命周期,比较灵活。
但代价是:用堆进行存储分配和清理可能比用堆栈需要更多的时间。
4)常量存储
java中的常量即final修饰的变量。
存储方式:
- static修饰,作为类信息,在类被加载时被存在静态的方法区。
- 非static的,作为对象属性,在对象创建的时候被初始化,存在堆里。
- 在方法里的。我们知道在方法被调用时会被加载到栈中进行执行,所以写在方法里的变量存在栈中。
有时在嵌入式系统中,常量本身会和其他部分隔离开,可以将其放在ROM(只读存储器)中。
5)非RAM存储
如果数据完全存活于程序之外,那么它不受程序控制。两个基本例子:
- 流对象:对象转化成字节流,通常被发送给另一台机器。
- 持久化对象:对象被存放于磁盘上,即使程序终止,它们依然可以保持自己的状态。例如:JDBC和Hibernate这样的机制提供了更加复杂的对在数据库中存储和读取对象的支持。
8,java服务端GC排查
排查思路:
- 通过日志确定问题:GC频繁并发生了异常;
- 通过gcutilo确定是GC问题导致的。
jstat -gcutil PID 1000
## 出现以下2个条件说明GC已经存在问题
## 1. fgc>ygc;
## 2. E、O = 100%
- GC定位:(分析dump)
top -Hp pid
## 1)确定tomcat线程中cpu占用过高的线程
## 2)如果有,printf '%x' pid 得到oxPid,
## 同时执行 jstack -l pid >output 查看栈信息,看output文件中nid=oxPid的线程是否正常,定位到具体线程。一般频繁GC情况下,cpu占用过高的线程都是GC线程。
## 3)如果没有找到高cpu线程,那么打出dump包来查看。一般tomcat的dump包比较大,没法看。可以使用以下命令进行统计和排序,一般查看dumpSort文件看见大的异常的业务代码就是有问题,试着把有问题的war包移出tomcat(注意文件夹也一并删除),运行一段时间看问题能否复现。
jmap -histo PID > dumpSort
## 打出来的包修改后缀名:.hprof,用JProfiler打开,查看堆信息看哪里有问题
1)GC log(tomcat gc日志: tomcat.gc.log)
1>Major GC
2019-10-12T18:20:08.224+0800: 2.287: [GC (Allocation Failure) [PSYoungGen: 524800K->29911K(611840K)] 524800K->30015K(2010112K), 0.0896057 secs] [Times: user=1.40 sys=0.23, real=0.09 secs]
方括号内部:[PSYoungGen: 524800K->29911K(611840K)]
表示:
221627K->0K(465920K) 表示:GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域的总容量)。
方括号外部:524800K->30015K(2010112K), 0.0896057 secs
表示:
GC前Java堆已使用容量->GC后Java堆已使用容量,后面圆括号里面的125952K为Java堆总容量。 PSYoungGen耗时。
最后面的 [Times: user=1.40 sys=0.23, real=0.09 secs]
表示:
用户消耗的CPU时间 内核态消耗的CPU时间 操作从开始到结束所经过的墙钟时间(Wall Clock Time)。
real是整个过程实际花费的时间。user+sys是CPU时间,每个CPU core单独计算,所以这个时间可能会是real的好几倍。
CPU时间和墙钟时间的差别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时。
2>Full GC
2019-10-12T19:07:45.475+0800: 2859.539: [Full GC (Ergonomics) [PSYoungGen: 221627K->0K(465920K)] [ParOldGen: 1146078K->1352631K(1398272K)] 1367705K->1352631K(1864192K), [Metaspace: 102828K->102351K(1142784K)], 5.0675678 secs] [Times: user=122.35 sys=2.36, real=5.06 secs]
PSYoungGen(新生代)
这个名称由收集器决定。PS是Parallel Scavenge收集器的缩写,它配套的新生代称为PSYoungGen。
ParOldGen
Parallel Scavenge收集器配套的老年代。
Metaspace
Parallel Scavenge收集器配套的永久代。
2)Heap
Heap
PSYoungGen total 465920K, used 121214K [0x00000000d5580000, 0x0000000100000000, 0x0000000100000000)
eden space 232960K, 52% used [0x00000000d5580000,0x00000000dcbdf9e0,0x00000000e3900000)
from space 232960K, 0% used [0x00000000e3900000,0x00000000e3900000,0x00000000f1c80000)
to space 232960K, 0% used [0x00000000f1c80000,0x00000000f1c80000,0x0000000100000000)
ParOldGen total 1398272K, used 357409K [0x0000000080000000, 0x00000000d5580000, 0x00000000d5580000)
object space 1398272K, 25% used [0x0000000080000000,0x0000000095d084d8,0x00000000d5580000)
Metaspace used 102915K, capacity 106894K, committed 107648K, reserved 1142784K
class space used 13164K, capacity 13819K, committed 13952K, reserved 1048576K
total
总的空间。
used
用掉的空间。
PSYoungGen
新生代又分化eden space、from space和to space这三部分。