美文网首页
内存分析(一)

内存分析(一)

作者: 许宏川 | 来源:发表于2015-06-20 13:28 被阅读130次

这篇文章很重要,分析一下JVM的内存问题。涉及到三个内容:

  • 基本数据类型与引用数据类型的区别
  • 内存的分区
  • 垃圾回收机制。

在我们写Java程序的时候,我们从来不去考虑内存的问题。因为JVM有自动管理内存的机制,这大大降低了Java程序员的工作量。不需要自己为程序写分配内存和释放内存的代码。
C++就是要程序员自己管理内存的,关于C++和Java的垃圾回收机制,网上流传下面这两张有趣的动态图。


C++垃圾回收机制 Java垃圾回收机制

就是在吐槽C++累死,而Java虽然自动化但是乱扔。当然情况没这么夸张,看看开心一下就行。
虽然我们把内存管理的重任交给了JVM,但是还是得了解一些JVM对内存的管理方法,否则一旦出现内存问题我们将手足无措。

基本数据类型与引用数据类型的区别##

八种基本数据类型你应该已经很熟悉了,在【精致Java教程】10:常量、变量与运算符(一)这篇文章中说过Java引用数据类型分为类、接口和数组三种。其中的接口以后再讲,而类和数组你已经有所了解。但是至今还未解释过什么是基本数据类型和引用数据类型,那现在对照着下图来说说类型的问题。首先来说下什么是基础数据类型,我说过当你看到“变量”这两个字时,你首先要想到的是内存中的一块区域,操作变量名就相当于对应内存区域存的东西,基本数据类型的变量名所指向的内存区域存的就是一个值。例如<code>int i = 5;</code>的i指向的内存区域存的内容就是整数5,这就是基本数据类型。但是引用数据类型不一样,引用类型的变量名代表的内存区域存的是一个引用,也就是一个内存地址。这个内存地址指向内容的真正存在的另一块内存区域。例如<code>String s = "hi"</code>的s存的不是字符串"hi"而是"hi"所在的内存区域的地址,变量s就相当于一个引用,所以称之为引用数据类型。

内存区域划分##

JVM会把系统分给它的内存划分为五个区域,分别是程序计数器、虚拟机栈、本地方法栈、堆和方法区。另外方法区里又划出一块很重要的分区叫常量池。如下图所示,然后我一一来说明一下每一个区域的作用。


程序计数器##

程序计数器(Program Counter Register)也称为寄存器。寄存器具有线程唯一性,也就是每个线程都有一个属于自己的寄存器。寄存器是最小的一块分区,为cpu服务,作用是存当前线程所执行的字节码的地址。因为Java是支持多线程的,而多线程的原理是线程们去抢cpu时间,因为cpu以极快的速度在线程之间来回切换而形成同时在跑多个线程的假象。如果正在执行的线程被抢走cpu时间或者自己暂停然后再恢复时就会去看该线程的寄存器存的字节码地址,也就是看当前线程执行到哪了,这样就能恢复线程。
另外如果当前线程执行的是本地方法,那就没有所谓的字节码地址了,程序计数器就会存一个空值(Undefined)。

所谓本地方法(Native Method)就是由其它语言编写的并编译好供Java调用的方法库。

虚拟机栈##

虚拟机栈(Virtual Machine Stack)通常简称为栈,栈也具有线程唯一性,即每创建一个线程就会为该线程创建一个栈。栈是后进先出的,用于存储方法的局部变量(再次提醒下方法的参数也是局部变量),这些变量包括基础数据类型和引用数据类型的引用(reference)和returnAddress类型。前两者你已经知道是什么,而returnAddress存的是字节码指令的地址,在老的虚拟机里jsr、jsr_w和ret这三条实现finally代码块的字节码指令会用到这个类型,但现在的虚拟机已经不用这几条指令了(JDK7以后不允许使用),所以只需要记住栈里面存储了局部变量的基础数据类型和引用数据类型的引用即可。

本地方法栈##

本地方法栈(Native Method Stack)也是线程唯一的,它和栈类似,只不过栈为Java方法服务而本地方法栈为本地方法服务。

堆##

堆(Heap)是最大的一块内存区域,存放类的实例。例如某方法里的<code>Person p = new Person()</code>中p是局部变量存在栈,而new出来的Person是对象存在堆。另外堆是线程共享的,即一个Java程序的所有线程共享一个堆。

方法区##

方法区(Method Area)也是线程共享的,存放类信息(包括成员变量)、类的静态成员、常量、即时编译器编译后的代码。

即时编译(Just In Time,JIT)是JVM的一种运行模式。JVM有三种运行模式,分别是解释执行、编译执行和混合执行。Java源码被编译器编译成字节码后由JVM解释执行,这是最常见的模式。但是如果JVM检测到有频繁使用的热点代码可以编译成本地的二进制指令就是编译执行。而通常是这两种方式并存的也就是混合执行。

常量池##

常量池是方法区的一部分,存放常量和被final修饰的变量。final关键字等讲完继承再详细介绍。

栈帧##

栈帧(Stack Frame)是栈的一种数据结构,上文说栈存储了局部变量的基本数据类型和引用数据类型的引用其实就是存储在栈帧里的。栈的后进先出就是指这些栈帧在栈里是后进先出的。栈帧是方法私有的,即每执行一个方法都会为该方法创建一个栈帧。每条Java线程只能同时执行一个方法,那么这个方法对应的栈帧就位于栈顶,当方法执行结束后就会把对应的栈帧弹出栈,栈帧弹出栈后那么栈帧里存的局部变量就被销毁了,这也是为什么局部变量的作用域都是仅限于方法的大括号内。
我来举个例子说得明白点,注意刚说了栈帧在栈中是后进先出的,Java一个线程只能同时执行一个方法,如果执行方法A,那么就会创建一个A栈帧压入栈,这时候A栈帧在栈顶。如果方法A调用了方法B,那么就会创建一个B栈帧压入栈,这时候B栈帧在栈顶。如果B方法执行完了就会弹出栈并返回结果给调用它的方法A然后继续执行A,此时A栈帧又处于栈顶。

栈帧又分为局部变量表、操作数栈和帧数据区三部分。

  • 局部变量表自然是存储局部变量。
  • 操作数栈是指方法里计算时一些临时的数据。栈帧既然是栈自然也是后进先出的结构。例如计算方法有下面的操作:
int a = 10;
int b = 20;
int c = a + b;

那么就会先把10压入操作数栈,然后再把20压入操作数栈,然后再把b和a都弹出栈做加法计算再把结果30压入操作数栈。接着栈把30返回给局部变量c。

相关文章

网友评论

      本文标题:内存分析(一)

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