Java高阶之路系列文章之第4讲:初学者的内存分析模型

这里写图片描述

通过本篇文章的阅读,笔者希望你能知道以下问题:
1.什么是JVM?
2.JVM的内存区域有哪些?
3.学会画简单的内存分析图分析程序。

0.JVM是Java进阶的必经之路

  我们都知道程序是需要内存的,Java程序的内存这块主要是JVM,当程序猿达到某个阶段的时候,深掘内存是必经之路,那么JVM是什么呢?

  官方解释:【JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE(当然也可以单独安装JRE)。】

  笔者是一名Android程序猿,学JVM相关知识就是为了深入内存,知道内存优化相关知识,同时也能分析分析一个对象如何在JVM中从出生到死亡的过程,对于某些开源框架而言,在分析源码的时候,会提供大的帮助,不知道你面试的公司当中有没有一个这样的条件,就是”JVM内存调优”,这里笔者也表达的不够清晰,对于JVM的看法,每个人学了都有自己的理解,毕竟JVM的学习是有些难度的,接下来笔者就来一一介绍JVM里内存的区域,不过笔者没有打算在这里细细的介绍,因为这还不是介绍的时机,以免太过于复杂对你而言如天书般,我们只需要抽象出一种简单的内存模型去分析分析某些简单demo,在熟悉这种分析模式之后,我们将会逐渐深入学习JVM的点点滴滴。

1.JVM的每个内存区域

这里写图片描述

  • 堆:对于大多数应用来说,Java堆(Java Heap)是Java虚拟机管理的内存中最大的一块。Java堆是被所有线程共享的一块数据区域,在虚拟机启动时创建,这一内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也逐渐变得不是那么“绝对”。
    堆中可细分为新生代和老年代,在细分可以分为Eden空间、Form Survivor空间、to Survivor空间。
    Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
    根据Java虚拟机规范规定,Java堆可以处于物理上不连续的内存中,即只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,可以固定大小也是可扩展的。主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms来控制)。如果在堆中没有内存可分配,并且堆也无法继续扩展时,将会抛出OutOfMemortError异常。
    Java的普通对象存活在堆中,与栈不同,堆的空间不会随着方法调用结束而清空。因此,在某个方法中创建的对象,可以在方法调用结束之后,继续存在堆中。这带来的一个问题是,如果我们不断的创建新的对象,内存控件将会最终消耗殆尽。
  • 方法区:方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译之后的代码等数据。虽然Java虚拟机将其描述为堆的一个逻辑部分,但它却有一个别名叫做Non-Heap(非堆)。目的是与Java堆区分开来。(以前很多人把方法区称为永久代,现在JDK1.8中已经用元数据区域取代了永久代)。
  • JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。
  • 本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的。他们之间的区别就是Java虚拟机栈是位虚拟机执行Java方法(也就是字节码)服务,而本地方法栈为位虚拟机使用到的Native方法服务。
    其实虚拟机规范中对本地方发栈中方法所使用的语言、使用方式以及数据结构都没有强制规定,因此具体的虚拟机可以自由地实现它。甚至在有的虚拟机(如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemory异常。
  • 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入到方法区的运行时常量池中存放。并非预置入Class文件中常量池的内容才进入方法运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

以上是笔者从大神博客上copy过来的,不需要搞得很懂,毕竟对于刚刚接触JVM的程序猿而言,初始表象即可。我们接下来看看如何通过简单的抽象内存模型去分析分析程序某些结果的原因。

2.一个简单的例子教你学会使用简单的内存模型分析程序

  因为JVM内存的每一个区域都有其自己的自责,如果我们在分析程序时,把JVM的每一个内存区域都分析到,这样未免太过于复杂了,所以笔者这里给出一种简单的初学者的内存分析模式,对于理解和分析一般的代码没有任何难度的,一支笔,一个堆,一个栈,一个常量池,你就可以分析任何你想分析的程序,请看以下代码,并说出为什么输出结果是这样的呢?

    int a = 3;

    int b = a;

    a = 4;

    System.out.println("b = "+ b);//运行结果b = 3

我们来画一画这段代码的内存分析图,在分析之前我们先要学会话内存模型图,对于初学者而言,我们常常接触有方法栈,堆,常量池即可,我们来分析分析以上的代码吧。首先我们看第一句代码“int a = 3;”,由于这句代码是发生于main方法中,因此我们先来画一个mian方法栈,然后再画一个堆,最后再画常量池。画好的内存模型图如下:

这里写图片描述

接下来我们就来画一画没执行一句代码后内存模型图的变化如何:

执行第一句:”int a = 3”

这里写图片描述

你可能会问我,为什么这个”3”是属于main方法栈的啊,关于这些常量存储在哪里,有这一个总结:

在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。

     (1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈中

     (2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆类存中的。

二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。

   同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量

   (1)当声明的是基本类型的变量其变量名及其值放在堆内存中的

   (2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中

我们在画变量的时候,一定要遵循上面的总结。

执行第二句:”int b = a;”

这里写图片描述

“b = a”,实际上是把a指向的”3”的地址赋值给了int类型变量b,因此此时b的值也同样是3,接下来我们再来看看第三句代码的执行:

执行第三句:”a = 4”
这里写图片描述

你可以从图中看到,尽管a变量指向的地址发生了变化,但是b是没有任何变化的,因此最后的结果就是输出”b = 3”,对于为什么常量3的地址为什么是”0x11111111”,其实不必太在意这个地址,因为那是笔者随便定的,因为当程序运行到右边的3时,JVM就会在main方法栈存储运行时变量的地方开辟一个空间,当然这是一种随机的行为,并把开辟后的地址交给变量a的指向地址区域,也就是图中a的右边区域,我就不细致分析了,希望读者自己好好研究,这只是一个简单的int数据的例子,在往后学习类与对象之后,那就会变得复杂很多,把基础打牢固吧!

下篇:Java高阶之路系列文章之第5讲:使用内存分析图分析内存中的数组
请耐心等待笔者总结…

猜你喜欢

转载自blog.csdn.net/ClAndEllen/article/details/81629722