android 内存分哪些区
Posted 韩亚飞_yue31313_韩梦飞沙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android 内存分哪些区相关的知识,希望对你有一定的参考价值。
韩梦飞沙 yue31313 韩亚飞 han_meng_fei_sha 313134555@qq.com
android 内存分哪些区
内存分哪些区
============
Android内存机制,了解Android堆和栈
1、dalvik的Heap和Stack
这里说的只是dalvik java部分的内存,实际上除了dalvik部分,还有native。
2、对象实例数据
实际上是保存对象实例的属性,属性的类型和对象本身的类型标记等,但是不保存实例的方法。实例的方法是属于数据指令,是保存在Stack里面,也就是上面表格里面的类方法。
对象实例在Heap中分配好以后,会在stack中保存一个4字节的Heap内存地址,用来查找对象的实例。因为在Stack里面会用到Heap的实例,特别是调用实例的时候需要传入一个this指针。
3、方法内部变量
类方法的内部变量分为两种情况:简单类型保存在Stack中;对象类型在Stack中保存地址,在Heap 中保存值。
4、非静态方法和静态方法
非静态方法有一个隐含的传入参数,这个参数是dalvik虚拟机传进去的,这个隐含参数就是对象实例在Stack中的地址指针。因此非静态方法(在Stack中的指令代码)总是可以找到自己的专用数据(在Heap 中的对象属性值)。
当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得Stack中的地址指针,否则dalvik虚拟机将无法将隐含参数传给非静态方法。
静态方法没有隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法即可被调用。所以我们可以直接使用类名调用类的方法。当然此时静态方法是存取不到Heap 中的对象属性的。
5、静态属性和动态属性
静态属性是保存在Stack中的,而不同于动态属性保存在Heap 中。正因为都是在Stack中,而Stack中指令和数据都是定长的,因此很容易算出偏移量,所以类方法(静态和非静态)都可以访问到类的静态属性。也正因为静态属性被保存在Stack中,所以具有了全局属性。
6、小结
Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。
堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double, boolean, char)和对象句柄。
对比上面的解析可以看出,其实Java处理Heap和Stack的大致原理跟C++是一样的。只是多了一个内存回收机制,让程序员不用主动调用delete释放内存。就像在C++里面,一般使用new申请的内存才会放到堆里面,而一般的临时变量都是放到栈里面去。
7、APP默认分配内存大小
在Android里,程序内存被分为2部分:native和dalvik,dalvik就是我们普通的java使用内存,也就是刚分析堆栈的时候使用的内存。
我们创建的对象是在这里面分配的,对于内存的限制是 native+dalvik 不能超过最大限制。
android程序内存一般限制在16M,也有的是24M(早期的Android系统G1,就是只有16M)。具体看定制系统的设置,在Linux初始化代码里面Init.c,可以查到到默认的内存大小。有兴趣的朋友,可以分析一下虚拟机启动相关代码。
-
gDvm.heapSizeStart =2*1024*1024;// heap初始化大小为2M gDvm.heapSizeMax =16*1024*1024;// 最大的heap为16M
8、Android的GC如何回收内存
Android的一个应用程序的内存泄露对别的应用程序影响不大。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。
Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启)。
做应用开发的时候,你需要了解系统的GC(垃圾回收)机制是如何运行的,Android里面使用有向图作为遍历回收内存的机制。
Java将引用关系考虑为图的有向边,有向边从引用者指向引用对象。线程对象可以作为有向图的起始顶点,就是从起始顶点开始的一棵树,根顶点可以到达的对象都是有效对象,GC不会回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。
因此对于我们已经不需要使用的对象,我们可以把它设置为null,这样当GC运行的时候,就好遍历到你这个对象已经没有引用,会自动把该对象占用的内存回收。我们没法像C++那样马上释放不需要的内存,但是我们可以主动告诉系统,哪些内存可以回收了。
9、查看应用内存使用情况
下面我们看看如何在开发过程中查看我们程序运行时内存使用情况。我们可以通过ADB的一个命令查看:
-
//$package_name:应用包名 //$pid:应用进程ID,可以用PS命令查看 adb shell dumpsys meminfo $package_name or $pid
- dalvik:是指dalvik所使用的内存。
- native:是被native堆使用的内存。应该指使用C\\C++在堆上分配的内存。
- other:是指除dalvik和native使用的内存。但是具体是指什么呢?至少包括在C\\C++分配的非堆内存,比如分配在栈上的内存。puzlle!
- Pss:它是把共享内存根据一定比例分摊到共享它的各个进程来计算所得到进程使用内存。网上又说是比例分配共享库占用的内存,也就是上面所说的进程共享问题。
- PrivateDirty:它是指非共享的,又不能换页出去(can not be paged to disk )的内存的大小。比如Linux为了提高分配内存速度而缓冲的小对象,即使你的进程结束,该内存也不会释放掉,它只是又重新回到缓冲中而已。
- SharedDirty:参照PrivateDirty我认为它应该是指共享的,又不能换页出去(can not be paged to disk )的内存的大小。比如Linux为了提高分配内存速度而缓冲的小对象,即使所有共享它的进程结束,该内存也不会释放掉,它只是又重新回到缓冲中而已。
10、程序中获取内存信息
通过ActivityManager获取相关信息,下面是一个例子代码:
-
1 privatevoid displayBriefMemory() 2 { 3 finalActivityManager activityManager =(ActivityManager) getSystemService(ACTIVITY_SERVICE); 4 ActivityManager.MemoryInfo info =newActivityManager.MemoryInfo(); 5 activityManager.getMemoryInfo(info); 6 Log.i(tag,"系统剩余内存:"+(info.availMem >>10)+"k"); 7 Log.i(tag,"系统是否处于低内存运行:"+info.lowMemory); 8 Log.i(tag,"当系统剩余内存低于"+info.threshold+"时就看成低内存运行"); 9 }
1 privatevoid displayBriefMemory() 2 { 3 finalActivityManager activityManager =(ActivityManager) getSystemService(ACTIVITY_SERVICE); 4 ActivityManager.MemoryInfo info =newActivityManager.MemoryInfo(); 5 activityManager.getMemoryInfo(info); 6 Log.i(tag,"系统剩余内存:"+(info.availMem >>10)+"k"); 7 Log.i(tag,"系统是否处于低内存运行:"+info.lowMemory); 8 Log.i(tag,"当系统剩余内存低于"+info.threshold+"时就看成低内存运行"); 9 }
内存优化方式
Exception里面要释放资源
界面不可见时,停止所有动画和相关线程
少用帧间动画,真要用的时候,用SurfaceView做
异步任务队列一定要用有界队列
读SqlLite的时候请尽量不要在UI线程,虽然你这样做也不会Exception
善用LeakCanary
=======
在数据结构中,堆栈是:堆 和栈两种数据结构,堆是完全二叉树,堆中各元素是有序的。在这个二叉树中所有的双亲节点和孩子节点存在着大小关系,如所有的双亲节点都大于孩子节点则 为大头堆,如果所有的双亲节点都小于其孩子节点说明这是一个小头堆,建堆的过程就是一个排序的过程,堆得查询效率也很高。栈是一种先进后出的线性表。
在jvm虚拟机中得堆栈对应内存的不同区域,和数据结构中所说的堆栈是两码事。
类装载子系统 ,负责把类从文件系统中装入内存
GC子系统 ,垃圾收集器的主要工作室自动回收不再运行的程序引用对象所占用的内存,此外,它还可能负责那些还在使用的对象,以减少的堆碎片。
内存区 ,用于存储字节码,程序运行时创建的对象,传递给方法的参数,返回值,局部变量和中间计算结果。
2. 栈的优势是,存取速度比堆要快 ,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
Java程序 运行时数据区域
Java虚拟机在执行java程序的过程中,会把它所管理的内存划分成若干个不同的数据区域(每当运行一个java程序都会启动一个虚拟机)
其中方法区和堆是由所有线程共享的,例如使用ThreadPoolExecutor
创建多个线程时,堆与方法区都可以被多个线程读取。
程序计数器
学过计算机组成原理的人都会知道在CPU的寄存器中有一个PC寄存器,存放下一条指令地址,这里,虚拟机不使用CPU的程序计数器,自己在内存中设立一片区域来模拟CPU的程序计数器。只有一个程序计数器是不够的,当多个线程切换执行时,那就单个程序计数器就没办法了,虚拟机规范中指出,每一条线程都有一个独立的程序计数器。注意,Java虚拟机中的程序计数器指向正在执行的字节码地址,而不是下一条。
虚拟机栈
是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的时候都会创建一个栈帧(我觉得可以把它看作是一个快照,记录下进入方法前的一些参数,实际上是方法运行时的基础数据结构),用于存放局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直到执行完成的过程都对应着一个栈帧在虚拟机中的入栈到出栈的过程。我们平时把内存分为堆内存和栈内存,其中的栈内存就指的是虚拟机栈的局部变量表部分。局部变量表存放了编译期可以知道的基本数据类型,对象引用,和返回后所指向的字节码的地址。
本地方法区
与 虚拟机栈
所发挥的作用很类似,但是要注意一下,虚拟机规范中没有对本地方法区中的方法作强制规定,虚拟机可以自由实现,即可以不是字节码。但是也可以是字节码,这样虚拟机栈和本地方法区就可以合二为一,事实上,OpenJDK
和SunJDK
所自带的HotSpot虚拟机
就直接将虚拟机栈和本地方法区合二为一。
堆
这个概念应该很多人都很熟悉,例如初学C语言的时候,老师就会讲malloc方法会在堆中分配空间,这里也一样。这个区域是用来存放对象实例的,几乎所有对象实例都会在这里分配内存,虚拟机规范中讲:所有对象的实例以及数组都要在堆上分配。但是随着JIT(Just-in-time) 编译期的发展,有些时候也有可能在栈上分配(这里我也不是很明白其中的道理)。堆是java垃圾收集器管理的主要区域(很多时候会称为GC堆,不叫垃圾堆),垃圾收集器实现了对象的自动销毁。
方法区
也是各个线程共享的区域,它用于存储已经被虚拟机加载过的类信息,常量,静态变量,及时编译期编译后的代码(类方法)等数据。这里要讲一下运行时常量池,它是方法区的一部分,用于存放编译期生成的各种字面量和符号引用(其实就是八大基本类型的包装类型和String类型数据)。
对象的创建
对于面向对象的一门语言,我们无时不在通过new关键字创建对象,那么这个过程又是怎样的呢?
当虚拟机遇到一条new指令的时候,首先会去检查所new的类是否已经被加载,在哪里检查?当然在方法区,方法区存放了加载过的类信息。如果没有加载,那么先执行类的加载。
通过类加载检查后,虚拟机开始为新生对象分配内存,对象所需要的内存大小在类加载完成后已经可以确定,这时候只要在堆中分配空间即可。分配内存有两种方式,第一种,我们假设内存绝对规整,那么只要在用过的内存和没用过的内存间放置一个指针即可,每次分配空间的时候只要把指针向空闲空间移动相应距离即可。第二种,我们假设空闲内存和非空闲内存夹杂在一起,实际上就是这种情况,那么就需要一个列表,去记录堆内存的使用情况,操作系统对内存的管理就是这样的。
那么,我们还要考虑一个问题,即在多线程的情况下,只有一个指针怎么能确保一个线程分配了内存指针没修改的时候另一个线程又分配内存不会覆盖之前的内存呢?这里有一种方法,让每一个线程在堆中先预分配一小块内存(TLAB
本地线程分配缓冲),每个线程只在自己的内存中分配内存。
最后,对象被成功分配内存。我们知道通过一个对象,我们可以通过getClass()方法获取类,默认比较两个对象实际比较的是对象内存的哈希值,这又是怎么实现的呢?其实在分配完内存后,虚拟机会对对象进行必要的设置,对象的类,对象的哈希码等信息都存放在对象的对象头中,所以分配的内存大小绝不止属性的总和。
对象的内存布局
对象在堆中的布局分为三个区域:对象头,实例数据,对齐填充。
-
对象头 包括两个部分,第一部分用于存储自身运行时的数据例如GC标志位,MonirGC次数,哈希码,锁状态,哪个线程可以拥有等被称为
MarkWord
(标记字)。第二部分存放指向方法区类数据的指针。在32位系统中,class指针大小为4字节,标记字大小为4字节。在64位系统中标记字大小为8字节。 -
实例数据 存放类的属性信息,包括父类的属性信息。数组的实例部分还包括数组的长度。实例信息按类分别4字节对齐。
-
对齐填充 这是虚拟机要求对象起始地址必须是8字节的整数倍,可以说对齐填充没有什么特别的含义。
对象的访问定位
我们知道,引用是引用,对象实例是对象实例。引用存放在虚拟机栈中,数据类型为reference,对象实例存放在堆中。那么引用是如何指向对象实例的呢?
主流的访问方式有两种,第一种是通过句柄池
,如果使用句柄池,那么java堆
中将会划分出一部分内存作为句柄池,句柄包含对象类型指针指向方法区的类型信息,还有对象实例指针,指向堆中的实例地址。
第二种是reference引用直接指向堆中的对象实例,对象实例的对象头存放对象类型指针。
两种方法各有优势,第一种可以在对象实例在GC
时移动的时候只改变句柄池中的对象实例指针,而不用改变reference引用本身。第二种方法就是访问速度快,减少了一次指针定位的时间开销。目前HotSpot虚拟机
就采用的第二种方式。
=====
-
java中的内存被分成以下四部分:
①、代码区 ②、栈区 ③、堆区 ④、静态区域
- 堆区:一般由程序员分配释放,存放new分配的对象和数组,JVM不定时查看这个对象,如果没有引用指向这个对象就回收
- 静态区:存放全局变量、静态变量和字符串常量,不释放
- 代码区:存放程序中方法的二进制代码,而且是多个对象共享一个代码空间区域。
==========
以上是关于android 内存分哪些区的主要内容,如果未能解决你的问题,请参考以下文章
36.JVM内存分哪几个区,每个区的作用是什么如和判断一个对象是否存活java垃圾回收机制垃圾收集的方法有哪些java类加载过程类加载机制双亲委派Minor GC和Major GC