一.Java内存分配结构复习
1.Java内存分配策略
上一篇Android内存管理分析总结中我们提到了Java内存分配策略,这里我们再复习一下:
Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是方法区(静态储存区)、栈区和堆区。
- 方法区(静态储存区):既然称为静态储存区,我们在代码中分配的静态域都存在于这个区域中;图中我们还可以看到该区域中存在一个“常量池”的区域,这个区域主要存放常量,包括我们在代码中通过final声明的字段(不包括局部的final)和各种编译器生成的常量。这里说一下,我们知道静态实例是在类加载时初始化,而这里的常量池是在编译期就分配好的。
- Java堆:又称动态分配内存区域,通场是指程序运行时直接new出来的内存,包括各种实例和数组。该区域是垃圾回收的重点区域。
- Java栈:当方法执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动释放。因为栈内存分配运算置于处理器的指令集中,效率很高,但分配的内存容量有限。
2.Java栈与Java堆
在方法体中定义的局部变量,包括基本类型的变量和对象的引用变量都是在Java栈内存中分配的。当在一段方法中定义一个变量时,Java就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给他的内存空间就会被释放掉了。
堆内存中用来存放所有new创建的对象(包括实例对象和数组)。在堆中产生了一个数组或者对象之后,可以在栈中定义一个特殊的变量,该变量的取值等于数组或者对象在堆中的首地址,这个特殊的变量就是我们上文所说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。
3.几个容易混淆的问题
(1).全局变量存放在哪里?
链接:https://www.nowcoder.com/questionTerminal/51123ddacab84a158e121bc5fe3917eb?toCommentId=23586
来源:牛客网
下列Java代码中的变量a、b、c分别在内存的____存储区存放。
class A {
private String a = “aa”;
public boolean methodB() {
String b = “bb”;
final String c = “cc”;
}
}
A.堆区、堆区、堆区
B.堆区、栈区、堆区
C.堆区、栈区、栈区
D.堆区、堆区、栈区
E.静态区、栈区、堆区
F.静态区、栈区、栈区
这道题的答案是C。
可以看到private String a = “aa”;
这里的a是一个全局变量的应用,或者叫成员变量/实例变量的引用,这个引用是储存在堆区中的;“aa”
是未经 new 的常量,在方法区的常量区中。
String b = “bb”;
b 为局部变量的引用,在栈区;“bb”
为未经 new 的常量,在常量区。
final String c = “cc”;
这句虽然有final作修饰符,但是由于是在方法中定义的,所以c仍然存在于栈中。
(2).在方法中new一个对象,这个对象在堆中还是在栈中?
class A {
private ObjectA obj1;
public boolean methodB() {
obj1 = new ObjectA();
ObjectB obj2 = new ObjectB();
}
}
这段代码写的更加明确一点了,new ObjectA()与new ObjectB()这两个对象,只要是通过“new”来创建的,就一定是存在于堆内存中的;obj1
和obj2
这两个对象的引用,obj2由于存在于方法块中,那么一定就是存在于栈中的,而obj1是A这个类的成员变量,笔者的理解也是:既然obj1是A的一个成员,那么当A的对象在堆上开辟内存的并实例化时候,也会一并将obj1这个变量放到堆中——因此它是存在于堆中的,但是并不能确定,这点有待于验证。
总之,记住一句话:局部变量位于栈区,静态变量位于方法区,实例变量及new出来的对象位于堆区!
另:局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束;成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。
二.Java内部类
Java中引入内部类的最大理由在于“间接实现多重继承”——我们知道,Java中禁止了混乱的多重继承,通过接口来实现这一功能。但是,通过接口实现有很多缺点,比如说实现一个接口就得实现他其中的所有方法。而假如在外部类已经继承了一个类的基础之上,我们还有继承另一个类中某一个特定的方法或者属性,这个时候就可以通过内部类来继承——因为内部类类继承了另一个类中的属性之后,外部类就可以通过调用内部类中继承来的属性,从而间接的实现多继承。
1.成员内部类
也就是普通的内部类,相当于外部类的一个成员的位置,可以使用任意得到访问修饰符,如 public, protected, private 或是不加访问修饰符(包可见)。成员内部类有以下几个特点:
①.成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类
内部类中存在一个外部类的引用,内部类实例需要使用 OuterClass.InnerClass inner = outer.new InnerClass();
的形式创建。这里直接拿一下网上博客中的代码来说明问题吧,笔者自己也没有反编译过,大家知道有这回事就行了:
public class Outer {
private int id;
private int state;
public class Inner {
private int field;
public void displayOuterState() {
System.out.println("Outer Id: " + id);
System.out.println("Outer State: " + Outer.this.state);
}
public void setField(int field) {
this.field = field;
}
public int getField() {
return this.field;
}
}
public static void main(String[] args) {
Outer.Inner inner = new Outer().new Inner();
inner.displayOuterState();
}
}
反编译一下 Outer$Inner.class:
F:\Demos\InnerClass>javap -p Outer$Inner.class
Compiled from "Outer.java"
public class Outer$Inner {
private int field;
final Outer this$0;
public Outer$Inner(Outer);
public void displayOuterState();
public void setField(int);
public int getField();
}
其中 this$0 就是外部类的引用。也就是说,创建一个成员内部类之后,内部类就会隐式的持有外部类的引用。
②.外部类是不能直接使用内部类的成员和方法的,但是内部类可以直接访问外部类的成员,包括私有成员
内部类可以访问外部类的成员,这个很容易理解,通过上面的分析我们知道,内部类隐式的持有外部类的引用:
反编译一下 Outer.class, 看到编译器在外部类添加了静态方法 access$000(Outer) 和 access$100(Outer), 内部类正是通过这两个方法来读取到私有的外部成员变量的。
F:\Demos\InnerClass>javap -p Outer.class
Compiled from "Outer.java"
public class Outer {
private int id;
private int state;
public Outer();
public static void main(java.lang.String[]);
static int access$000(Outer);
static int access$100(Outer);
}
外部类访问内部类的时候,必须先通过OuterClass.InnerClass inner = outer.new InnerClass();
的形式创建内部类的实例,进而获取内部类的成员变量和方法。
③.成员内部类中不能存在任何 static 的变量和方法;
成员内部类中成员内部类中不能存在任何 static 的变量和方法,但 static final 的常量是可以的。
因为内部类是要依赖外部类得实例的,而静态变量和静态方法是不依赖对象的,他在外部类初始化的时候就已经加载;而在加载静态域的时候外部类的实例尚未创建,自然会出错。
但是static final是常量型,常量是在编译期就确定的,放在方法区的常量池中,因此可以。但是还是要注意一点,方法块中,即便是final类型也是存放在栈中的。
2.静态内部类
在定义内部类的时候,可以在其前面加上一个权限修饰符static。此时这个内部类就变为了静态内部类。笔者的理解是这样的,既然是静态的类,那么也是属于静态域的,也就是说该类是属于JVM的方法区的,而普通类或者普通类的对象,是属于堆内存的——因此我们可以知道,静态内部类和外部类对象没有任何关联,最好将静态内部类看作一个普通类,只是碰巧被声明在另一个类的内部而已。
①.静态内部类和外部类没有关联,也不能访问外部类中的非静态成员变量
静态内部类在编译期会单独生成一个.class文件,不像普通内部类那样会生成一个Outer$Inner.class。从这里我们可以看出,静态内部类和外部类基本是独立的两个类,没有什么关联,只是恰好声明在了外部类中而已。
基于这个认识,静态内部类不能访问外部类的非静态成员变量方法就很好理解了——毕竟这是两个类,静态内部类又不持有外部类的引用,自然不能访问;但是外部类中的静态成员变量例外,因为静态成员变量是属于静态域的,或者说方法区的,静态内部类也是属于静态域的,两个在一个区域自然可以互相访问。
②.创建静态内部类的对象时不需要通过外部类的对象来进行;静态内部类可以直接访问外部类的静态成员,包括私有的。
这个,感觉上面已经说的很清楚了,没什么好说的。
三.Android中的内存泄漏
前面两个大点都是在为内存泄漏做准备,好了现在我们来说说内存泄漏的事情。在Java中,内存泄漏指的是存在一些分配后的对象,在他变的无用(即程序以后不会再使用这些对象)之后,仍然通过GC Roots可达,因此GC不会回收这些已经无用的对象,这样的对象就被判定为内存泄漏。
下面我们来说说Android中常见的内存泄漏:
(1).集合类造成内存泄漏
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。如网上给出的这个例子:
Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。
这个例子是网上直接拷贝过来的,主要是很清晰的展示了集合类内存泄漏的泄漏。但是毕竟拿人家的手断嘛,下面我就举一个我在实际开发中的例子,笔者在做应用开发的时候,还真遇到集合类造成内存泄漏的情况:
我们可能有看过“Android中优雅的退出所有的Activity”这类文章,在郭霖的《第一行代码(第二版)》中也有讲到过这种方式,具体的实现就是:
- ①.创建一个自定义的myApplication类,该类继承自Application。在这个类中创建一个List集合类;
public class myApplication extends Application {
......
private List<Activity> list = new ArrayList<>();
- ②.在myApplication中创建两个方法,一个用于添加我们启动的各个Activity,另一个用于退出时清楚List中的Activity;
public void addActivity(Activity activity) {
list.add(activity);
}
public void exit() {
try {
for (Activity activity : list) {
if (activity != null)
activity.finish();
}
} catch (Exception e) {
e.printStackTrace();
}
}
- ③.在我们启动的Activity中,调用addActivity()方法添加启动的Activty;
myApplication.getInstance().addActivity(this);
- ④.在我们退出程序的地方,调用exit()方法,删除添加的Actiivty;
myApplication.getInstance().exit();
这样做看似优雅,但是是使用的过程中,我的leakcanary检测工具一直内存泄漏,打开一看,发现调用过myApplication.getInstance().addActivity(this);
方法的Activity全都内存泄漏了!!
为什么呢?这个List是在myApplication中的,这个myApplication的生命周期是整个App的生命周期,因此它自然要比单个Activity的生命周期要长。假如我们从一个Ativity A跳到了另一个Activity B,那么A就到了后台,假设这个时候系统内存不足了,想要回收他,却发现有一个和APP生命周期一样长的List还持有他的引用,完了,明明没有用的Activity实例却回收不了,自然就造成了内存泄漏。
所以这种看似优雅的方式,实际上使用不好就极为不优雅。其实解决上述问题的方法也很简单,回收不了是因为List持有的是Activity的强引用,我们只要想办法给他搞成弱引用即可,这里只是提供一种思路,具体怎么实践可以自行考虑。
(2).单例/Context使用不当造成的内存泄漏
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。我们想想单例有什么特点?在笔者的Android设计模式之——单例模式这篇文章中提到过,除了枚举单例以外,所有的单例都有这么一个特点:内部有一个公有的静态方法,用于返回该类内部创建的实例,既然在一个静态方法中返回单例引用,那么这个引用必然也是静态的。
OK,我们来看看这个经典的例子,这个例子上网一搜随手粘来,我这里就直接贴了:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
①.传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长
②.传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
所以正确的单例应该修改为下面这种方式:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。
OK,上面是别人的代码,我贴完了;对于上面的例子,我们需要注意的是,这个问题的本质不是单例引起的内存泄漏,而是想一个app全局变量中传入了Activity的Context引起的,和上面我们第一个例子中问题的本质如出一辙,这点我们要意识到。另外,还有最重要的一点,“单例的静态特性使得其生命周期跟应用的生命周期一样长”这句话应该怎么理解?或者说,为什么单例的静态特性与应用的生命周期一样长?
这里强推一篇文章:单例模式讨论篇:单例模式与垃圾回收,这篇文章讨论了这个问题,这里笔者再结合自己的理解谈一谈:
当一个单例的对象长久不用时,会不会被jvm的垃圾收集机制回收?
我们观察上述单例的代码,尤其是这段:
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
这段中,instance是该类的静态实例:private static AppManager instance;
,我们在获取的时候就是获取的这个静态的instance。首先明确一点,instance是存在于JVM的方法区内(静态域)的,这点我们多次强调过。JVM在垃圾回收的时候,会从GC Roots开始进行可达性分析,那么GC Roots自然不会被回收,可以作为GC Roots的对象有:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中的常量引用的对象。
- 本地方法栈中JNI的引用的对象。
可以看到,instance = new AppManager(context);
这句中,我们创建的单例对象new AppManager(context)是被静态引用instance
所引用,符合第二条,也就是说我们的静态单例是作为一个GC Root的,因此不会被JVM回收。这一点我们在Android内存管理分析总结这篇文章中有说过。
虽然jvm堆中的单例对象不会被垃圾收集,但是单例类本身如果长时间不用会不会被收集呢?因为jvm对方法区也是有垃圾收集机制的。如果单例类被收集,那么堆中的对象就会失去到根的路径,必然会被垃圾收集掉。对此,笔者查阅了hotspot虚拟机对方法区的垃圾收集方法,JVM卸载类的判定条件如下:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
只有三个条件都满足,JVM才会在垃圾收集的时候卸载类。显然,单例的类不满足条件一,因此单例类也不会被卸载。也就是说,只要单例类中的静态引用指向jvm堆中的单例对象,那么单例类和单例对象都不会被垃圾收集。
综上所述,只有在应用退出的时候,静态单例才会走完它的一生,因此静态单例的生命周期和app的生命周期是一致的。当然,还有一种方法让单例对象回收,那就是人为的断开静态引用instance和new AppManager(context),即instance = null;
,当然,这样做没有什么意义。
还有一点需要注意,上面我们谈论的是JVM中单例对象的回收情况。在5.0之后的Android系统中,已经默认用ART虚拟机了,这种虚拟机与JVM有些区别,据说在ART虚拟机的Android版本上,静态单例对象是可以被回收的,当然,这个只是据说,笔者没有自己验证过。
(3).Handler/非静态匿名内部类 造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//...
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadData();
}
private void loadData(){
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例(同第二大点中的成员内部类),所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。所以另外一种做法为:
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private TextView mTextView ;
private static class MyHandler extends Handler {
private WeakReference<Context> reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context); //获得MainActivity的一个弱引用
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
activity.mTextView.setText("");
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private void loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
reference:
Android 内存泄漏总结
Android性能优化之常见的内存泄漏
单例模式讨论篇:单例模式与垃圾回收