android开发需要了解的堆和栈

引言

Android开发中经常会遇到各种内存问题,比如内存溢出,内存泄露,栈溢出等常见的问题,也会经常听到关于内存中的堆的概念和栈的概念,要想更好的解决这些问题,还是得站在一个整体的高度对这些东西有一个全面的认识

jvm虚拟机

每一个Android的app进程都是运行在一个独立的jvm虚拟机上面,通过adb shell ps可以简单的看出用户进程的父进程都是zygote进程,往上追溯就能发现zygote进程里面初始化了一个独立的jvm虚拟机,所以简单的理解就是每个用户进程都是运行在独立的jvm环境中,这样也保证了一个app的崩溃不会影响其他app进程的运行。

不管是java文件还是kotlin文件,通过编译后都是.class的java字节码文件,jvm的类加载机制加载.class文件,规定了一套自己的内存模型,所以Android的应用程序中的内存分配也就跟jvm的内存模型严格对应起来。

抛开jvm中的类加载器和执行引擎,单纯的分析和内存相关的运行时数据区,主要分为线程共享的内存区域和线程独立的内存区域,而在线程共享的区域里面就有我们最常见的堆区,还有一个方法区,而线程独占的区域里面就是常见的概念栈


jvm运行时数据区.png
方法区

每一个jvm有一个方法区,生命周期跟jvm一致,里面放的数据有

  • 类信息(.class类文件解析的信息,用于后续对象的实例)

  • 常量/静态变量(static/static final修饰的变量和常量也分配在方法区)

  • 即时编译期编译的代码(编译就是将.java文件编译成.class文件过程后的内容)

堆区
  • 数组

  • 对象实例(基本上所有通过new关键字实例的对象都分配在对象,当然也存在对象逃逸在栈上分配)

在堆上的对象也是所谓的gc垃圾回收主要回收的目标,而堆里面又有不同的细分区域,在后面会分析到

这一部分是线程共享的,不同的线程都可以进行实例化对象,实例化对象就需要找到对应的.class文件,所以方法区中的是线程共享的,而对象的实例化分配在堆上,不同的线程中持有的是对象的引用,这部分基本上就是一个内存地址,所以这部分也是线程可以共享的

虚拟机栈

这部分是线程私有的,意思就是不同的线程就会有自己的虚拟机栈,而这个栈里面又是存放的一个一个的栈帧

  • 栈帧:就是线程中运行的方法,当调用其他方法的时候,就会产生一个新的栈帧压入虚拟机栈,当运行完对应的方法,方法出栈,接着运行之前压栈的方法,保证了方法的执行顺序
//举个栗子
public void main() {
       a();
       b();
  }

private void a() {}

private void b() {}

比如线程执行到main()方法,就会在对应线程的虚拟机栈中产生一个main()方法的栈帧,然后执行到对应的a()/b()方法,又会产生两个栈帧压栈

线程方法栈.png

栈帧的示意结构,不同的方法就会产生不同的栈帧,然后栈帧的结构里面有包含了【局部变量表/操作数栈/动态链接/正常错误返回】

局部变量表

局部变量表就是保存方法中局部变量引用,局部变量的作用域就是在对应的方法体中,属于栈帧私有的变量

操作数栈

虽然更虚拟机栈都是栈,但是一般说的对象分配分配中的栈是指虚拟机栈,这个操作数栈的理解,就跟Java的字节码指令有关系了,会将计算数和符合不断的压栈出栈进行计算,这部分就不细讲了

本地方法栈

本地方法栈就是一些native方法的调用栈,也是线程私有的

程序计数器

这个跟cpu的调度有关,时间片轮转会让不同的线程都会有执行的机会,然后线程在运行的时候,当时间片运行结束,就会挂起,这会就需要记录当前执行的程序字节码位置,以便后续得到时间片后继续运行

小结

上面就分析了jvm运行时数据区的不同分区,jvm就是一套内存模型规范,所以程序运行时就会把不同的数据存储在不同的内存地址区域


jvm运行时数据区.png
堆和栈

上面知道了堆和栈性质,那么对比分析一下

  • 用途

堆:用来存储java中实例化的对象,不管是成员变量,局部变量还是类变量,他们引用的实例化对象都是存储在堆中

栈:用来存储方法的调用过程,以栈帧的形式在栈上存储,栈上可以存储方法调用过程中的局部变量,但是仅限于基本数据类型的变量和对象的引用,这部分数据是分配在栈上的,随着方法执行完,栈帧出栈,对应的变量就会被自动释放

  • 跟线程的关系

堆:堆中的对象是所以线程共享的,分配在堆上的对象可以被所有线程访问

栈:栈是归属于单个线程,所以栈内存中的变量也不用考虑线程同步的问题,属于线程的私有内存

  • 大小

堆:堆的大小跟jvm设计相关,可以通过-Xmx设置堆区内存可被分配的上限,使用-Xms设置堆区的初始大小

栈:栈帧的局部变量表和操作数栈的大小也是编译时确定的,取决于jvm虚拟机的实现,栈的深度也是有限的,如果栈帧超出了限制,就会抛出常见的StackOverFlow的错误

深入堆

一般在开发中,打交道最多的还是堆上的内存对象,内存溢出,内存泄露等问题更多的是出现在堆上的对象没法回收,堆上的内存满了,无法继续分配导致,当然由于机器的内存是固定的大小,也会出现栈上的内存溢出,或者方法区的内存溢出

new对象

当遇到new关键字去实例化一个对象,就会进行类加载检测,如果类加载器中检测出对应的.class文件,就会去堆上分配内存,分配了内存会会对内存空间进行初始化,比如对基本类型设置初始值,然后才是设置对应对象中的赋值和引用

对象的大小

总是在说对象会在堆上占内存,那么一个对象到底占多数内存,对象是由三部分组成的,对象头+实例数据+对齐

对象头

对象头大小占8个字节,里面会包含有对象的哈希值,GC的分代年龄,锁状态,线程持有的锁,偏向线程id等信息,还会包含类型指针,如果对象是数组,对象头中还会记录数组的长度

实例数据

实例数据中不同的基本类型所占的空间大小不一样

对齐

由于计算机的位数,为了更方便的存取对象,会对对象大小进行一个8字节整数倍的对齐


java对象内存分布.png
对象的分配

实例化了对象后,就会将对象进行一个堆上的分配,这会就得提到堆上对应的分区,都是一些非常常见的概念

堆内存区域划分了多个区域用来存储不同的对象,这样的目的主要是为了更效率的垃圾回收,主要有新生代和老年代,新生代里面又分了eden区,survive区

整体来说,新生代和老年代的大小是个1:2的关系,新生代中eden区和survive区是一个8:2的关系,survive区又分为了两个一样大小的S0和S1区域

堆上的这么划分,主要就是为了垃圾回收的效率,当一个分区内存分配满了后,都会触发gc,而gc回收又是一个占cpu的行为,也可能停止用户线程,所以更好的分配策略和回收算法,可以提供更好的用户体验

step1

对象的创建基本都是直接分配在新生代的eden区,除了一些大对象会直接进入老年区,很明显的eden区的内存空间有限,分配大对象可能很容易触发gc,所以大对象会直接进入老年代

step2

当eden区满了后,如果之前实例化的对象还存活,就会被复制到S0区域,由于对象的生命特性,eden区中绝大多数的对象都会被回收清理掉,所以8:1:1的设计也是为了更好的空间利用,当从eden去移动到S0区域的时候,对象头上面的分代年龄就会+1

step3

当继续触发gc进行垃圾回收,存活在S0中的对象,就会被标记整理到S1区域中,S0区域格式化清空,然后对象的分代年龄继续+1,随着gc的进行,一直重复在survive区中来回交换

step4

当对象的分代年龄达到15的时候(一般情况,可以设置),长时间的回收不掉,就会把对象移动到老年代,所以老年代中的对象生命周期都比较长,到后续对象直到生命周期结束前都在老年代中,直到被回收

持久代

堆上的内存分为新生代和老年代,对应的还有一个持久代的概念,这个就是最上面jvm内存模型里面的方法区,用来存放类文件和静态类型数据,放在一起对比概念叫法


堆内存和持久代.png
gc

最后需要提到的概念就是gc,对象实例化后会占据内存,如果内存不够分配,就会触发gc,去回收可以会回收的内存,怎么判断这个可以回收的内存,就是常见的根可达算法,GC Roots

GC Roots

可以作为GC Roots的变量就是一些长期存活或者短期内不会被回收的变量

  • 静态变量/常量池(方法区中,gc回收在堆区,方法区中能跟进程生命周期一致)

  • 线程栈变量(线程的局部变量在栈上存活,方法没有执行完,栈帧不会被回收释放)

  • JNI指针(也就是本地方法栈,跟线程栈理解类似)

如果一个对象的引用链最头部是一个GC Roots对象,那么对应引用链上的对象就不可被回收,反之,如果没有被GC Roots持有,那就是gc回收的对象

引用类型

上面提到的gc roots引用关系,就是一般的=赋值的强引用,Java中处理强引用,还有其他类型的引用,也一并记录

  • 强引用-> =赋值的操作,如果有gc roots的引用关系,就不可被回收,即使抛出OOM,也不会去回收

  • 软引用-> SoftReference,在内存不足的时候,触发gc,如果gc后还是不足,就会回收软引用对象

  • 弱引用->WeakReference,触发gc就会被回收

  • 虚引用->PhantomRefrence,这个用的比较少,直接通过虚引用get对象会是一个null值,这个的用途设计到一个ReferenceQueue,就不详细分析了

gc清理方式

在知道了哪些对象可以回收的情况下,最后需要了解的概念就是gc在不同的堆内存分代中的回收方式

可能最上面对于新生代的8:1:1的空间划分存在疑惑,这点也主要和清理方式有关

复制算法

复制算法就是把内存空间分成两等分,拿一份作为预留空间,在需要gc回收的时候,将其中回收的区域内存活的对象,复制到对应预留区联系的内存空间内,这样的回收方式后都是连续的内存空间,但是预留一半的,会造成空间的利用率降低,这就是典型的堆区新生代中的survive区1:1的原因,这部分就使用了复制算法进行清理

那么为什么是8:1:1,新生代还得划分一个eden区,而不是5:5直接使用复制算法

因为通常堆中的对象生命周期都比较短,一次gc后存活的对象很少,为了最大程度的提高空间利用率,就使用了8:1:1的比例

从eden取到S0的整理也是复制算法,将还存活的对象直接复制到survive中

标记清理

标记整理算法,就是遍历堆中的存活对象和可清理对象,对需要清除的对象进行标记,然后再统一对这些标记的对象进行回收,这种算法的优点就是不需要内存复制,内存也是100%利用,但是缺点就是会有内存碎片,整理后的内存不是连续的空间

标记整理

相比于标记清理产生内存碎片,标记整理算法,会把存活的对象复制移动到连续的内存空间,这样空间的利用率是100%,但是效率会低很多,整理的时候,比如两个对象中只要1m的空间,整理一个2m的对象过来,还得进行对象的移动

对象的整理会反向触发对象的引用地址的变更,所以对象的移动会有额外的开销

总结

从jvm的内存模型分区,到对象的分配,和对象的回收都进行了一遍梳理,具如果有错误和不足还望路过的大佬指点一二

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容