相对于C、C++这些高性能语言,Java有着让此类程序员羡慕的功能:内存自动管理。彷佛这样,Java程序员不用再关心内存,也不用去了解相关知识。但结果然的是这样吗?特别对于咱们这种Android程序员来讲,对内存但是吃得死死的,一旦出现较为复杂的内存泄露和溢出方面的问题,简直就是噩梦。所以,对Java内存管理有个大致的了解彷佛已经成为一个合格的Android程序员必备的技能,就算是新进的Kotlin一样是基于JVM的。不如趁此机会,你们一块儿来揭开它的面纱。git
Java是一门面向对象的编程语言,江湖一直流传着这么一句话:万物皆对象。所以,Java的内存管理也能够理解成为对象的建立与释放。那么,对象究竟是什么?男友?女友?仍是?对象和内存究竟是什么关系?这里的问题太多,咱们一步一步来。程序员
Tips1:全文以经常使用的虚拟机HotSpot、经常使用的内存区域Java堆和普通Java对象为例。
Tips2:若是深读过《深刻理解Java虚拟机》的同窗能够不用看了,请右上角,若是忘了,请继续!
复制代码
男友或者说女友你均可以理解成对象,对象是实实在在存在的,好比老爸,老妈,同时伴随着一个抽象的概念,类:它是对对象的抽象,无论是男友和女友都是人,属于人类。概念差很少就介绍到这,感受本身在大学上课同样。。。个人天(捂脸)。github
程序员没媳妇怎么办?new一个。老简单了,高的,矮的,瘦的,胖的,想要啥就有啥,今生最不后悔的就是当程序员了,虽然头有点冷。算法
new一个就是一个对象的建立,那么到底是怎样的一个过程呢?JVM遇到一条new指令的时候,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,而且检查这个符号引用表明的类是否已被加载、解析和初始化过。若是没有,那必须先执行相应的类加载过程,类加载检查经过后,能够说一个对象的模型已经出来了,但Java毕竟只是编程语言,仍是得分配内存不是?否则怎么操做?编程
对象内存的分配和现实不少场景都是同样的,好比停车,有些地方可能只有100个车位,先到的停在最前的空位上,就这样按顺序一辆一辆的停下来。这样的分配称为“指针碰撞”。还有一种你想停哪就停哪,只要你插得进去。这样的分配称为“空闲列表”。无论是前者仍是后者,停车咱们是靠眼睛看的,哪里有空位才停,那么JVM如何“看”的呢?前者是靠一个指针做为指示器,分配多大内存的对象就日后移多大距离,后者会维护一个列表来记录可用内存(可插车位)。bash
对于并发敏感的同窗确定会提出疑问,在并发的时候如何能正确分配到相应位置? 通常也有两种解决方案,一种是一辆一辆停,保证前一辆停完,下一辆才开始停;另外一种是你们说好要停哪一片区域,好比A,B,C停在A区域,那么A,B,C每次去停A区域就好了,跟其它区域不要紧(区域指的是线程),若是他们邀了朋友D,那对不起,只能等其余区域人停完,你再停。所以,对象的建立并非原子操做,切记,切记。并发
车停哪里,咱们已经知道了,那么怎么停?有人喜欢正着停,有人喜欢横着停,有人喜欢倒着停。一样的,对象在内存中是怎么摆放的呢?大致分为3个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。编程语言
简单地来介绍这3位,毕竟这概念性太强。布局
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据、如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的示例。对上面部分名词不理解的,我在后续文章可能会解释,毕竟本身也在学习当中,若是想急于知道的同窗能够查阅相关资料,姑且当它是概念记住便可。性能
实例数据就比较好理解了,它是对象真正存储的有效信息,也是在程序代码中所定义的各类类型的字段内容。
对齐填充并非必然存在的,因为内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。对象头大小是8字节的整数倍,因此实例数据大小不是8字节的整数倍时,就须要对齐填充来补齐。
你停完车,干完事,总得开车回家吧,那总得找到本身的车吧?怎么找?本身停在哪一个车位总记得吧?本身的牌照总记得吧?那么咱们如何在内存中访问咱们的对象呢?你们来看一组图:
前者称为句柄访问,优势很明显,对象移动了只要修改句柄中的指针就好了,不会牵涉到reference;后者称为直接指针访问,优势也很明显,就是快,直接少了句柄这一层。而本文中讨论的HotSpot采用后者。
车炸了怎么办?固然是买辆新的(手动坏笑)。那么咱们如何断定一个对象屎没屎呢?在此以前介绍两种引用算法:第一种是引用计数算法,很好理解,给对象一个计数器,初始值为0,有地方引用就加1,失效就减1,计数器为0的说明都是屎了的;第二种是可达性分析算法,也很好理解,从GC Roots开始,向下引用对象,若是一个对象存在一条从GC Roots到自己的路径,那么说明这个对象还活着,不然就屎了。以下图object567就是屎的:
那么哪些能够做为GC Roots呢?
咱们的HotSpot是采用后者,那么为啥没采用前者呢?由于它很难解决对象之间相互循环引用的问题。例如:
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
复制代码
那么问题来了,不可达的对象真的屎了吗?固然不会,至少通过2次标记才会宣告一个对象的屎亡。第一次标记是发现对象不可达,同时筛选出没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,那么这些能够认为屎了,能够回收(那么这时候不是只标记一次吗?有没有大佬解答);剩下的对象会被放置F-Quenue的队列中而且GC会对这些对象进行第二次标记,在执行finalize()方法的时候也是拯救本身的时候(只要在方法中合从新创建与引用链上其它对象的关联便可)。你们最好忘记这个方法的存在。它的运行代价高昂,不肯定性大,没法保证各个对象的调用顺序等。《Effective Java》中也有提到避免此方法。
对象的简单分析差很少就到这里结束了,你觉得到这里所有结束了?太天真。
像上面碰到的名词,诸如虚拟机栈、方法区、Java堆等究竟是什么玩意?
国际惯例,No picture,say a J8!
看到这张图,你们确定知道我要干什么了。。。我也不肯意啊,写到这感受是篇说明文了,个人天,贼尴尬。
程序计数器是一块较小的内存空间,它能够看做是当前线程所执行的字节码的行号指针。例如平时的分支、循环、跳转、异常处理、线程恢复等基础功能要依赖这个计数器完成。从图上咱们可知,它是线程私有的,也就说每一个线程都会有一个独立的程序计数器且互不影响。并且它是惟一一个在Java虚拟机规范中没有规定任何OutOfMemoryError状况的区域。
虚拟机栈描述的是Java方法执行的内存模型:每一个方法在执行的同时会建立一个栈帧用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。细心的朋友,会发现局部变量表在对象的访问章节图中出现过,重要的是当进入一个方法时,这个方法须要在帧中分配多大的局部变量空间是彻底肯定的,换句话说,局部变量表所需的内存空间是在编译期间就完成分配的。
在Java虚拟机规范中,对这个区域规定了两种异常情况:若是线程请求的栈深度大于虚拟机所容许的深度,将抛出StackOverflowError异常;若是虚拟机栈能够动态扩展(当前大部分的Java虚拟机均可动态扩展,只不过Java虚拟机规范中也容许固定长度的虚拟机栈),若是没法扩展时没法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈与虚拟机栈所发挥的做用是很是类似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java(也就说字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。所以与Java虚拟机栈抛出的异常情况也是同样的。
你能够认为几乎全部的对象实例都在堆上分配的。难道不是全部的?这是一个优化技术,试想一下,若是一个对象没法被别的方法或者线程经过任何途径访问到,为什么不直接分配在栈上呢?
根据Java虚拟机规范的规定,Java堆能够处于物理上不连续的内存空间中,只要逻辑上连续便可,这也意味着,若是逻辑上没有足够的内存完成分配且堆也没法扩展,那么将会抛出OutOfMemoryError异常。
方法区与Java堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。但它除了和Java堆同样不须要连续的内存和没法选择固定大小或者可扩展内存将抛出OutOfMemoryError异常外,还能够选择不实现垃圾收集。
运行时数据区介绍的差很少了,这里补充一个概念叫直接内存,在jdk1.4加入的NIO有用到,感兴趣的能够看看。你们确定注意到每一个区域(除程序计数器)都有抛内存溢出的情况,之后有人问到, 什么时候会产生OOM,就不要再说内存不够的时候了,很伤感情。
上面提到的Java堆能够说是虚拟机管理的内存中最大的一块了,是GC光顾的常客,所以也叫“GC堆”。GC顾名思义就是垃圾回收,这也是Java一大优点,不用的内存能够自动回收。既然是垃圾回收能够有垃圾回收装置啊,扫地的还用扫帚呢。
见名知义啊,先标记须要回收的对象,而后一次性清除标记的对象。它能够说是最基础的收集算法,就算是后面介绍的算法都是在它基础上加以改进的。既然改进,那么确定有没法忍受的缺点,它除了效率不高外,还有个严重的问题,就算会产生大量不连续的内存碎片,从刚刚咱们提到的Java对OOM的缘由可知,很是容易没法分配而第二次执行垃圾回收,或者直接OOM。执行过程如图所示:
这个算法很好理解,将可用内存化为两块,每次只用其中一块,当要回收的时候,把可用的对象复制到另一块,而后把原先那块一次性清理掉,可用说在效率上大大的提升,但有个致命的弱点就是内存减半。
复制算法执行过程如图所示:
复制算法理论上效率很高,可是你想一想若是存在100个对象,其中98个均可用的,那么你得复制98个对象,极端状况100个都存活,你还得复制所有一遍,这是没法接受的。该算法针对标记-清楚算法产生大量内存碎片作了改进,先把可用对象移到一端,而后直接清理掉端边界之外的内存。执行过程如图所示:
从咱们刚刚分析来看,复制算法貌似更适合朝生夕屎的对象,而剩余的两个算法更适合“百岁”对象。前者那些对象所在区域咱们就叫作新生代,后者对象所在区域就叫作老年代。咱们的分代算法就是根据新生代和老年代采用不一样的算法而已。
那么,这里有个问题,老年代的对象到底怎么来?换句话问,怎样才能进入老年代?首先,分析一个特例:大对象直接进入老年代;而后是正常步骤:对象A在分配的时候优先分配在新生代的Eden空间,当Eden空间不够分配内存的时候,将进行一次Minor GC,此后对象A仍然存活且能被Survivor空间容纳,那么将移至Survivor空间,并将其年龄计数器置为1,此后,对象A每度过一次Minor GC且存活,年龄就加1,当达到最大年龄(MaxTenuringThreshold)时,将被荣升到老年代(鼓掌鼓掌)。固然这也不是绝对的,若是Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接晋级。
关于本文主要内容差很少就到这了,最后留下一个很关键的问题,垃圾回收器到底何时进行垃圾回收,又是如何进行的?这里有个很牛逼的名词叫“Stop The World”。
首先,我想说深刻理解Java虚拟机(第2版)真的是一本不错的书,我这种小菜鸡根本没机会认识这种大神,也谈不上打广告,看过的同窗应该都知道。其次,本文全部的内容均来自于该书,甚至有一字不差的一段话。本文能够说是我读完该书第二部分:自动内存管理机制的笔记。本文不少都属于概念性知识,就好比地球为何叫地球?这种属于约定俗成的东西,但对于咱们Android程序员来讲,最好是可以对其有个大概的了解,但不是全部同窗都看过该书(买了,也不必定看),所以我分享了该文章,其中有部分是本身的理解,若是有问题我及时改正,最好你们仍是买原著仔细阅读,我这里抛砖引玉一下- -!
天天都学习一点点也是极好的。既然是学习,对象确定是有前辈已经总结了的,你应该作的是将其理解,并转为本身的东西(用本身的思想把它翻译出来,本质不变),否则就叫作探索。还有一句话就是好记性不如烂笔头,老师确定说过这句话,当时一句都没进我法耳。
最后,感谢一直支持个人人!
在这里,提早祝你们新年快乐!
Github:github.com/crazysunj/