Java的内存管理
Java的内存管理就是对象的分配和释放。其中内存的分配是由程序完成的,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。对象的释放是由垃圾回收机制决定和执行的,这样做确实简化了程序员的工作。但同时,它也加重了JVM的工作。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。
要了解Java内存是如何分配的,首先我们来了解一下JVM的内存区域组成。Java把内存分为两种:栈内存和堆内存。
1、在函数中定义的基本类型变量和对象的引用变量都在栈内存中分配。
2、堆内存用来存放由new创建的对象和数组以及对象的实例变量。
在函数(代码块)中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间;在堆中分配的内存由java虚拟机的自动垃圾回收器来管理。
java中的各种数据在内存中是如何存储的呢,我们来分别看一下:
(1)基本数据类型
Java的基本数据类型共有8种,即int、short、long、byte、float、double、boolean以及char。这种类型的定义是通过诸如int a = 3; long b = 255L; 的形式来定义的。如int a=3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。
另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。比如,我们同时定义:
int a = 3;
int b = 3;
编译器先处理int a = 3; 首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3; 在创建完b这个引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。定义完a与b的值后,再令a=4; 那么,b不会等于4,还是等于3。在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
(2)对象
在Java中,创建一个对象包括对象的声明和实例化两步,下面用一个例题来说明对象的内存模型。
假设有类Rectangle定义如下:
public class Rectangle {
double width;
double height;
public Rectangle(double w,double h){
w = width;
h = height;
}
}
1、声明对象时的内存模型。用Rectangle rect; 声明一个对象rect时,将在栈内存为对象的引用变量rect分配内存空间,但Rectangle的值为空,称rect是一个空对象。空对象不能使用,因为它还没有引用任何"实体"。
2、对象实例化时的内存模型。当执行rect = new Rectangle(3,5); 时,在堆内存中为类的成员变量width,height分配内存,并将其初始化为各数据类型的默认值(类加载一篇中说过 从Java到android:类的加载机制);接着进行显式初始化(类定义时的初始化值);最后调用构造方法,为成员变量赋值。 返回堆内存中对象的引用(相当于首地址)给引用变量rect,以后就可以通过rect来引用堆内存中的对象了。
(3)创建多个不同的对象实例
一个类通过使用new运算符可以创建多个不同的对象实例,这些对象实例将在堆中被分配不同的内存空间,改变其中一个对象的状态不会影响其他对象的状态。例如:
Rectangle r1 = new Rectangle(3,5);
Rectangle r2 = new Rectangle(4,6);
此时,将在堆内存中分别为两个对象的成员变量width、height分配内存空间,两个对象在堆内存中占据的空间是互不相同的。如果有:
Rectangle r1 = new Rectangle(3,5);
Rectangle r2 = r1;
则在堆内存中只创建了一个对象实例,在栈内存中创建了两个对象引用,两个对象引用同时指向一个对象实例。
(4)包装类
基本型别都有对应的包装类:如int对应Integer类,double对应Double类等。基本类型的定义都是直接在栈中,如果用包装类来创建对象,就和普通对象一样了。例如:int i = 0; i直接存储在栈中。Integer i = new Integer(5); 这样,i对象数据存储在堆中,i的引用存储在栈中,通过栈中的引用来操作对象。
(5)字符串String
String是一个特殊的包装类数据。可以用用以下两种方式创建:
String str = new String("abc");
String str = "abc";
第一种创建方式,和普通对象的的创建过程一样;
第二种创建方式,Java内部将此语句转化为以下几个步骤:
1、先定义一个名为str的对String类的对象引用变量:String str;
2、在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o的字符串值指向这个地址,而且在栈这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。
3、将str指向对象o的地址。
值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种情况下,其字符串值却是保存了一个指向存在栈中数据的引用。
为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
String str1="abc";
String str2="abc";
System.out.println(s1==s2);//true
注意,这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。我们再接着看以下的代码。
String str1= new String("abc");
String str2="abc";
System.out.println(str1==str2);//false
创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。
(6)数组
当定义一个数组,int x[];或int []x;时,在栈内存中创建一个数组引用,通过该引用(即数组名)来引用数组。x = new int[3];将在堆内存中分配3个保存int型数据的空间,堆内存的首地址放到栈内存中,每个数组元素被初始化为0。
(7)静态变量
用static的修饰的变量和方法,实际上是指定了这些变量和方法在内存中的"固定位置",可以理解为所有实例对象共有的内存空间。静态表示的是内存的共享,就是它的每一个实例都指向同一个内存地址。把static拿来,就是告诉JVM它是静态的,它的引用(含间接引用)都是指向同一个位置,在那个地方,你把它改了,它就不会变成原样,你把它清理了,它就不会回来了。 那静态变量与方法是在什么时候初始化的呢?对于两种不同的类属性,static属性与实例属性,初始化的时机是不同的。instance属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。
我们常可看到类似以下的例子来说明这个问题:
class Student{
static int numberOfStudents=0;
Student()
{
numberOfStudents++;
}
}
每一次创建一个新的Student实例时,成员numberOfStudents都会不断的递增,并且所有的Student实例都访问同一个numberOfStudents变量,实际上int numberOfStudents变量在内存中只存储在一个位置上。
Java的内存回收机制
看完Java内存分配,我们来看看内存的释放:垃圾回收机制。垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用,以免造成内存泄露。
Java对象的四种引用
1)强引用:创建一个对象并把这个对象直接赋给一个变量。
2)软引用:通过SoftReference类实现。
3)弱引用:通过WeakReference类实现。
4)虚引用:不能单独使用,主要是用于追踪对象被垃圾回收的状态。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现。
Android的内存管理
其实我们在用安卓手机的时候不用太在意剩余内存,Android的上层应用是Java,当然需要虚拟机,而android上的应用是自带独立虚拟机的,也就是没开一个应用就会打开一个独立虚拟机。其实和java的垃圾回收机制类似,系统有一个规则来回收内存。进行内存调度都有一个阀值,只有低于这个值,系统才会按一个列表来关闭用户不需要的东西。当然这个值默认设置得很小,所以你会看到内存老在很少的数值徘徊。但事实上他并不影响速度,相反加快了下次启动应用的速度(按home键,应用处于stop状态在后台,下次打开不需要再次加载,所以速度会快很多)。这本来就是android标榜的优势之一,如果人为去关闭进程,没有太大必要。特别是使用自动关进程的软件。为什么内存少的时候运行大型程序会慢呢,原因是:在内存剩余不多时打开大型程序时会触发系统自身的进程调度策略,这是十分消耗系统资源的操作,特别是在一个程序频繁向系统申请内存的时候。这种情况下系统并不会关闭所有打开的进程,而是选择性关闭,频繁的调度自然会拖慢系统。
Android之所以采用特殊的内存管理机制,原因在于其设计之初就是面向移动终端,所有可用的内存仅限于系统RAM,必须针对这种限制设计相应的优化方案。当Android应用程序退出时,并不清理其所占有的内存,Linux内核进程也相应的继续存在,所谓“退出但不关闭”。从而使得用户调用程序时能够在第一时间得到响应。在系统内存不足的情况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给需要的app,这套杀进程回收内存的机制叫Low Memory Killer,它是基于Linux内核的OOM_Killer机制诞生的。
Android基于进程中运行的组件及其状态规定了五个默认的回收优先级:
1)前台进程(Foreground process)
2)可见进程(Visible process)
3)服务进程(Service process)
4)后台进程(Background process)
5)空进程(Empty process)
前台进程的重要性最高,一次递减,空间进程重要性最低,更容易被回收。参考文档如下:
https://developer.android.com/guide/components/processes-and-threads.html?hl=zh-cn
前台进程 —— Foreground process
用户当前操作所必需的进程。通常在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。
A. 拥有用户正在交互的Activity(已调用onResume())
B. 拥有某个 Service,后者绑定到用户正在交互的 Activity
C. 拥有正在“前台”运行的 Service(服务已调用 startForeground())
D. 拥有正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
E. 拥有正执行其 onReceive() 方法的BroadcastReceiver
可见进程 —— Visible process
没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。
A. 拥有不在前台、但仍对用户可见的 Activity(已调用 onPause())
B. 拥有绑定到可见(或前台)Activity的Service
服务进程 —— Service process
尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。
A. 正在运行 startService() 方法启动的服务,且不属于上述两个更高类别进程的进程
后台进程 —— Background process
后台进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU 列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个
Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。
A. 对用户不可见的 Activity 的进程(已调用 Activity的onStop() 方法)
空进程 —— Empty process
保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
A. 不含任何活动应用组件的进程
再科普一下oom_adj。什么是oom_adj?它是linux内核分配给每个系统进程的一个值,代表进程的优先级,进程回收机制就是根据这个优先级来决定是否进行回收。对于oom_adj的作用,你只需要记住以下几点即可:
1)进程的oom_adj越大,表示此进程优先级越低,越容易被杀回收;越小,表示进程优先级越高,越不容易被杀回收
2)普通app进程的oom_adj>=0,系统进程的oom_adj才可能<0