揭开Java内存管理的面纱

前言

相对于C、C++这些高性能语言,Java有着让此类程序员羡慕的功能:内存自动管理。似乎这样,Java程序员不用再关心内存,也不用去了解相关知识。但结果真的是这样吗?特别对于我们这种Android程序员来说,对内存可是吃得死死的,一旦出现较为复杂的内存泄露和溢出方面的问题,简直就是噩梦。因此,对Java内存管理有个大体的了解似乎已经成为一个合格的Android程序员必备的技能,就算是新进的Kotlin同样是基于JVM的。不如趁此机会,大家一起来揭开它的面纱。

对象

Java是一门面向对象的编程语言,江湖一直流传着这么一句话:万物皆对象。因此,Java的内存管理也可以理解成为对象的创建与释放。那么,对象到底是什么?男朋友?女朋友?还是?对象和内存到底是什么关系?这里的问题太多,我们一步一步来。

Tips1:全文以常用的虚拟机HotSpot、常用的内存区域Java堆和普通Java对象为例。
Tips2:如果深读过《深入理解Java虚拟机》的同学可以不用看了,请右上角,如果忘了,请继续!

概念

男朋友或者说女朋友你都可以理解成对象,对象是实实在在存在的,比如老爸,老妈,同时伴随着一个抽象的概念,类:它是对对象的抽象,不管是男朋友和女朋友都是人,属于人类。概念差不多就介绍到这,感觉自己在大学上课一样。。。我的天(捂脸)。

对象与内存

创建

程序员没媳妇怎么办?new一个。老简单了,高的,矮的,瘦的,胖的,想要啥就有啥,此生最不后悔的就是当程序员了,虽然头有点冷。

new一个就是一个对象的创建,那么究竟是怎样的一个过程呢?JVM遇到一条new指令的时候,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,类加载检查通过后,可以说一个对象的模型已经出来了,但Java毕竟只是编程语言,还是得分配内存不是?不然怎么操作?

分配

对象内存的分配和现实很多场景都是一样的,比如停车,有些地方可能只有100个车位,先到的停在最前的空位上,就这样按顺序一辆一辆的停下来。这样的分配称为“指针碰撞”。还有一种你想停哪就停哪,只要你插得进去。这样的分配称为“空闲列表”。不管是前者还是后者,停车我们是靠眼睛看的,哪里有空位才停,那么JVM如何“看”的呢?前者是靠一个指针作为指示器,分配多大内存的对象就往后移多大距离,后者会维护一个列表来记录可用内存(可插车位)。

对于并发敏感的同学肯定会提出疑问,在并发的时候如何能正确分配到相应位置? 一般也有两种解决方案,一种是一辆一辆停,保证前一辆停完,下一辆才开始停;另一种是大家说好要停哪一片区域,比如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呢?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

我们的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方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。细心的朋友,会发现局部变量表在对象的访问章节图中出现过,重要的是当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,换句话说,局部变量表所需的内存空间是在编译期间就完成分配的。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果无法扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java(也就说字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。因此与Java虚拟机栈抛出的异常状况也是一样的。

Java堆

你可以认为几乎所有的对象实例都在堆上分配的。难道不是所有的?这是一个优化技术,试想一下,如果一个对象无法被别的方法或者线程通过任何途径访问到,为何不直接分配在栈上呢?

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,这也意味着,如果逻辑上没有足够的内存完成分配且堆也无法扩展,那么将会抛出OutOfMemoryError异常。

方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。但它除了和Java堆一样不需要连续的内存和无法选择固定大小或者可扩展内存将抛出OutOfMemoryError异常外,还可以选择不实现垃圾收集。

运行时数据区介绍的差不多了,这里补充一个概念叫直接内存,在jdk1.4加入的NIO有用到,感兴趣的可以看看。大家肯定注意到每个区域(除程序计数器)都有抛内存溢出的状况,以后有人问到, 何时会产生OOM,就不要再说内存不够的时候了,很伤感情。

垃圾收集算法

上面提到的Java堆可以说是虚拟机管理的内存中最大的一块了,是GC光顾的常客,因此也叫“GC堆”。GC顾名思义就是垃圾回收,这也是Java一大优势,不用的内存可以自动回收。既然是垃圾回收可以有垃圾回收装置啊,扫地的还用扫帚呢。

图中是我们HotSpot的垃圾收集器,上边是新生代,下边是老年代,具体的垃圾收集器来历作用我就是不介绍,没有必要,本文希望读者有个大概的了解。那么,有垃圾器,总得有方法吧,喝饮料还用吸管呢,吸管什么原理大家没点13数吗?那么在这里大体介绍几种算法的思想。

标记-清除算法

见名知义啊,先标记需要回收的对象,然后一次性清除标记的对象。它可以说是最基础的收集算法,就算是后面介绍的算法都是在它基础上加以改进的。既然改进,那么肯定有无法忍受的缺点,它除了效率不高外,还有个严重的问题,就算会产生大量不连续的内存碎片,从刚刚我们提到的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程序员来说,最好是能够对其有个大概的了解,但不是所有同学都看过该书(买了,也不一定看),因此我分享了该文章,其中有部分是自己的理解,如果有问题我及时改正,最好大家还是买原著仔细阅读,我这里抛砖引玉一下- -!

每天都学习一点点也是极好的。既然是学习,对象肯定是有前辈已经总结了的,你应该做的是将其理解,并转为自己的东西(用自己的思想把它翻译出来,本质不变),不然就叫做探索。还有一句话就是好记性不如烂笔头,老师肯定说过这句话,当时一句都没进我法耳。

如果你在学习Java的过程中遇见什么问题或者想获取一些学习资源的话欢迎加入我的Java学习交流QQ群:495273252

原文地址:https://www.cnblogs.com/chenshengjava/p/8417118.html

时间: 2024-10-11 07:00:05

揭开Java内存管理的面纱的相关文章

java内存管理

一.jvm内存结构 程序计数器(Program Counter Register).JVM虚拟机栈(JVM Stacks).本地方法栈(Native Method Stacks).堆(Heap).方法区(Method Area) (1)PCR 跟随线程生命周期,记录当前执行到的.class字节码行数,用于多线程操作 (2)JVM Stacks 跟随线程生命周期,在方法执行中存储数据 (3)Native Method Stacks 处理native方法,如object中的hashCodes()等

简单的例子 关于Java内存管理的讲解

我想做的是,逐行读取文件,然后用该行的电影名去获取电影信息.因为源文件较大,readlines()不能完全读取所有电影名,所以我们逐行读取. 就这段代码,我想要在位置二处使用base64,然后结果呢? 两处位置都打印了,位置一得到base64,ok,没问题.当我在位置二想使用base64时,问题来了?onload队列的问题,位置二总是无法正确的获取到想要的base64,这个时候就可以考虑异步问题了. 在还没有接触到angular的时候,还真的不知道它到底有什么作用,直到我开始学习它,并且运用到它

java内存管理机制

JAVA 内存管理总结 1. java是如何管理内存的 Java的内存管理就是对象的分配和释放问题.(两部分) 分配 :内存的分配是由程序完成的,程序员需要通过关键字new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间.释放 :对象的释放是由垃圾回收机制决定和执行的,这样做确实简化了程序员的工作.但同时,它也加重了JVM的工作.因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请.引用.被引用.赋值等,GC都需要进行监控. 2. 

【Java】Java内存管理

Java内存管理是面试中经常会问到的问题.Java的内存管理其实是指对象 的分配和释放问题.曾经看过这样一句话:"C++程序员觉得内存管理太重要了,所以一定要自己进行管理,而Java程序员觉得内存管理太重要了,一定不能自己管理".我觉得这句话说得太精辟了. C++程序员需要显式分配内存,释放内存,而这样常常会引起"内存泄露".而Java程序员不需要显式分配和释放内存,Java在创建对象的时候会自动分配内存,在对象不再使用的时候释放内存.Java的对象是通过new关键

java内存管理的分析

java 中的内存分为四个部分: stack(栈):存放基本类型的数据和对象的引用,即存放局部变量. Note: 如果存放的是基本类型数据(非静态变量),则直接将变量名和值存入stack中. 如果存放的是引用类型,则将变量名存入栈,然后指向它new出的对象(存放在堆中). heap(堆)存放 new 出来的东西. data segment(数据区):分为静态区和常量区(常量池) 静态区(static segment): 存放在对象中用 static 定义的静态成员(即静态变量,如果该静态变量是基

Java内存管理的9个小技巧

Java内存管理的9个小技巧很多人都说“Java完了,只等着衰亡吧!”,为什么呢?最简单的的例子就是Java做的系统时非常占内存!一听到这样的话,一定会有不少人站出来为Java辩护,并举出一堆的性能测试报告来证明这一点.其实从理论上来讲Java做的系统并不比其他语言开发出来的系统更占用内存,那么为什么却有这么多理由来证明它确实占内存呢?两个字,陋习. 1.别用new Boolean(). 在很多场景中Boolean类型是必须的,比如JDBC中boolean类型的set与get都是通过Boolea

Java内存管理第二篇 - 内存的分配

Java内存管理无非就是对内存进行分配和释放.对于分配来说,基本类型和对象的引用存储到栈中,常量存储到常量池中,对象存储到堆上,这是一般的分配.而对于回收来说要复杂的多,如果回收不好,还可能造成分配出去的内存得不到回收而造成内存泄漏. 这一篇将简单介绍一下Java内存的分配,下一篇将介绍内存的回收及内存泄漏等知识. 1.JVM内存模型 1.程序计数器(Program Counter Register): 程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是

java内存管理浅析

首先感谢强大的网络资源,本博文是根据网络上的各种资源进行整合,然后加入自己的理解而成,可能会与其它网络资源有重复,望其他作者多多包涵.由于初学java,如有不准确的描述还请读者指正.下面正式切入正题: 众所周知,java和C++都是面向对象的编程语言,但是与C++相比,java上手比较容易,而且使用方便.小弟对c++了解不是很多,但是有一点是C++初学者最为头痛的问题,那就是内存管理,这也正是C++和java之间很大的一个区别.在C++中,内存是依靠程序员自己来管理的,编写程序过程中稍有不慎就会

JAVA内存管理再解

首先我们要明白一点,我们所使用的变量就是一块一块的内存空间!! 一.内存管理原理:   在java中,有java程序.虚拟机.操作系统三个层次,其中java程序与虚拟机交互,而虚拟机与操作系统间交互!这就保证了java程序的平台无关性!下面我们从程序运行前,程序运行中.程序运行内存溢出三个阶段来说一下内存管理原理! 1.程序运行前:JVM向操作系统请求一定的内存空间,称为初始内存空间!程序执行过程中所需的内存都是由java虚拟机从这片内存空间中划分的. 2.程序运行中:java程序一直向java