美文网首页
从HelloWo从HelloWorld.class讲内存到底是如

从HelloWo从HelloWorld.class讲内存到底是如

作者: 问心2018 | 来源:发表于2020-12-12 17:41 被阅读0次

    很多人问,作为一个Android开发有必要了解Java内存分配机制吗?答案是肯定的。

    java的内存区域划分实际上远比这复杂:java虚拟机在执行Java
    程序的过程中会把所有的内存划分为不同的数据区域,下面这张图
    描述了一个HelloWorld.java文件被JVM加载到内存中的过程:


    1.HelloWorld.java 文件首先需要经过编译器编译,生成HelloWorld.class 字节码文件。

    2.Java程序中访问HelloWorld这个类时,需要通过ClassLoader将HelloWorld.class加载到JVM内存中。

    3.JVM中的内存可以划分为若干个不同的数据区域,包括:
    程序计数器,虚拟机栈,本地方法栈,堆,方法区

    1.1 程序计数器

    Java是多线程的,CPU可以在多个线程中分配执行时间片段。当一个线程被CPU挂起时,需要记录代码已经执行到的位置,方便CPU重新执行此线程时,知道从那行执行开始执行,这就是程序计数器的组作用。

    程序计数器是虚拟机中一块较小的内存空间,主要用于记录当前线程
    执行的位置。


    如上图所示:每个线程都会记录当前方法执行到的一个位置,当CPU切换到某一个线程上时,根据程序计数器记录的数字,继续向下执行指令。

    关于程序计数器还有几个需要格外注意:

    1.在Java虚拟机规范中,对程序计数器这一区域规定没有任何OutOfMemoryError情况。

    2.线程私有,每个线程内部都有一个私有的程序计数器,他的生命周期随着线程创建而创建,结束而死亡。

    3.当一个线程正在执行一个Java方法时,这个程序计数器记录正在执行虚拟机字节指令的地址,如果正在执行的是Native方法,这个程序计数器的数值为空

    1.2本地方法栈

    本地方法栈和下面即将要讲的虚拟机栈基本相同,只不过是针对本地方法,在Android开发涉及JNI可能接触本地方法多一些,有一些虚拟机的实现已经将本地方法栈和虚拟机栈合二为一了(HotSpot)。

    1.3虚拟机栈

    虚拟机栈也是线程私有的,与线程的生命周期同步,在Java虚拟机中,规定了两种异常:

    1.StackOverflowError :当前线程请求栈深度超出虚拟机栈所允许的深度时抛出。

    2.OutOfMemoryError:当JVM动态扩展到无法申请足够内存时抛出。

    在我们看一些博客和书的时候,经常看到一句话:JVM是基于栈解释器执行的,DVM是基于寄存器解释器执行的。

    上面那句话基于栈值得就是虚拟机栈,虚拟机栈的初衷是用来描述Java方法执行的内存模型,每个方法执行的时候,JVM都会在虚拟机栈中创建一个栈帧,让我们看一下栈帧是什么?

    1.3.1栈帧

    每一个线程在执行某一个方法时,都会为这个方法创建一个栈帧,
    我们可以这样理解:一个线程包含多个栈帧,每个栈帧内部包含:局部变量表,操作数栈,动态链接,返回地址等。


    1.3.1.1 局部变量表

    局部变量表是变量值的存储空间,我们调用方法传递的参数,以及在方法内部创建的变量都会保存在局部变量表中。

    1.3.1.2 操作数栈

    操作数栈也常称为操作栈,他是一个后入先出栈。
    当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会把各种指令压入和弹出操作数栈。

    1.3.1.3 动态链接

    动态链接的主要目的是为了支持方法在调用过程中的动态链接。

    在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。

    1.3.1.4 返回地址

    当一个方法开始执行后,只有两种方式可以退出这个方法:

    1.正常退出:方法中的代码正常完成,或者遇到任意一个返回的指令。
    2.异常退出:方法执行过程中遇到异常,并且内部没有处理。

    正常退出时,栈帧中可能保存此数值作为返回地址。方法异常退出时,栈帧中一般不会保存部分信息。

    1.4 堆

    Java堆是JVM所管理的内存中最大的一块,该区的唯一目的就是存放对象实例,几乎所有的对象的实例都在堆里分配,因此他是GC管理的主要区域,有时候也叫GC堆同时他是所有线程的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全。

    按照对象存储时间的不同,可以分为新生代,老年代,其中新生代又被分为Eden和Survivor区。

    图中不同的区域具有不同的生命周期,可以根据不同区域使用不同的垃圾回收算法,进而提高垃圾回收率。

    1.5 方法区

    方法区也是JVM规范里规定的一块运行时数据区。主要存储已经被JVM加载的类信息(版本,字段,方法,接口),常量,静态变量,即时编译器后的代码和数据,该区域也是被各个线程共享。

    1.6 异常再现

    StackOverflowError 栈溢出:

    在method方法中,递归调用了自身,并且没有设置递归结束条件,所以出现了StackOverflowError异常。

    OutOfMemoryError 内存溢出:
    理论上,虚拟机栈,方法去,堆都有可能发生OutOfMemoryError,但在实际过程中,大多数发生于堆中。

    在一个无线循环中,动态向List添加HeapError对象,这会不断的占用堆中的内存,当堆内存不够时,必然会产生OutOfMemoryError,也就是内存溢出异常。

    1.7 总结

    对于 JVM 运行时内存布局,我们需要始终记住一点:上面介绍的这 5 块内容都是在 Java 虚拟机规范中定义的规则,这些规则只是描述了各个区域是负责做什么事情、存储什么样的数据、如何处理异常、是否允许线程间共享等。千万不要将它们理解为虚拟机的“具体实现”,虚拟机的具体实现有很多,比如 Sun 公司的 HotSpot、JRocket、IBM J9、以及我们非常熟悉的 Android Dalvik 和 ART 等。这些具体实现在符合上面 5 种运行时数据区的前提下,又各自有不同的实现方式。

    最后我们借助一张图来概括一下本课时所介绍的内容:


    总结来说,JVM 的运行时内存结构中一共有两个“栈”和一个“堆”,分别是:Java 虚拟机栈和本地方法栈,以及“GC堆”和方法区。除此之外还有一个程序计数器,但是我们开发者几乎不会用到这一部分,所以并不是重点学习内容。 JVM 内存中只有堆和方法区是线程共享的数据区域,其它区域都是线程私有的。并且程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

    相关文章

      网友评论

          本文标题:从HelloWo从HelloWorld.class讲内存到底是如

          本文链接:https://www.haomeiwen.com/subject/uvcygktx.html