Java内存区域理解

来源:互联网 发布:ug多轴编程视频 编辑:程序博客网 时间:2024/05/01 20:30

前言

最近的经验告诉我,大家在挑选技术书籍时候一定要慎重,若是买到不适合自己情况,真的是既浪费金钱,更重要的是浪费了时间。宁愿花费几天甚至一周的时间来好好选择自己所需要的书籍,这样做是非常值得的,原因有二:

  • 好书值得一读再读,每读一遍都有新的体验,买来总是不会错的
  • 不会浪费金钱,也不会浪费时间

作为搞技术的,书籍的花费是绝不能吝啬的,网上找的大部分文章包括我这篇,都大概只能解决当前问题,因为我也是为了总结学习内容,对你的技术提高应该是没有什么帮助的。只有阅读优秀书籍,站在巨人肩膀上,才能系统地学习,并提高自己的能力与技术。

以上是我的浅见,不认同的朋友不妨当我是胡说,因为本质上来说我是说给自己听的(笑)。

Java 内存概述

Java 虚拟机在执行Java 程序的时候,将它所管理的内存给划分成了若干个不同的数据区域,这些区域,可以使用下图来简单概括:

这里写图片描述

下面来一一解析这些内存区域:

程序计数器

程序计数器是很小的一块内存空间,它的作用主要是起到一个线程中字节码执行行号的作用,换句话说,它标识了下一句应该直接的字节码,举个例子,常用的分支,循环,跳转,异常处理等基础功能都是由这个程序计数器实现的。

在Java虚拟机中,多线程是通过线程轮流切换并分配处理器执行时间来实现的,使用这一种cpu调度方式意味着任何一个确定的时候,一个处理器都只会处理一条线程中的指令。所以为了线程切换后,能恢复到正确的位置,就用到了程序计数器,由于是多线程,每个线程间还需要独立的程序计数器,存储这个东西的程序计数器的内存区域,叫做“线程私有”内存。这很好理解,因为每个线程的实际执行位置几乎是不一样的。

需要说明的是,在执行Java方法的时候,程序计数器就如同它的功能一样,标识字节码的下一行执行代码,然而,若是执行的Native方法,那么,这个计数器就为空。而且此区域是唯一一个JVM没有规定任何OutOfMemoryError的区域。

Java虚拟机栈

Java虚拟机栈可以类比C语言中的栈,每个方法执行都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口信息等跟方法执行相关的信息,一个方法的执行到结束就对应着一个栈帧的入栈到出栈的过程。同样地,很容易得,栈也是线程私有的,它的生命周期与线程相同。

这里有个需要注意的地方:

当线程请求的栈深度大于虚拟机所允许的最大值时,那么,其会抛出 StackOverflowError,若是虚拟机栈时允许拓展的,那么,在这种情况下,深度还是不够,则会抛出OutOfMemoryError。

本地方法栈

这块区域很好理解,与虚拟机栈所有的功能非常类似,但是是执行的本地方法。与虚拟机栈一样,也会抛出一样的异常。

Java堆

这一块是比较重点的内容,也是大部分程序员比较关心的一块。Java堆是被所有线程所共享的一块区域,在虚拟机启动时就创建完成。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都是在该区域进行分配内存的,注意是几乎,随着技术的发展,以及一些比较激进的优化策略,这都不是绝对的。

关于Java堆的内容,我不准备在这里展开,之后会另外写Blog来看看。

方法区

和Java堆一样,方法区也是所有线程所共享的内存区域,它主要是用来存储已被虚拟机加载的类信息,常量,静态变量等数据,注意,这里说到里一些静态变量等数据,这可以说明GC在这个区域是比较少发生的,或者说在这里GC很难有所收获,在这里GC主要是对常量池以及类型卸载。

运行时常量

运行时常量属于方法区的一部分,它用于存放编译期生成的各种字面量和符号引用。运行时常量区具有动态性,换句话说,Java并不要求,常量一定只有编译期才能够产生,也就是并非预置入class文件中常量池的内容才能进入,运行期间也可以将新的常量放入池里。

直接内存

就如同它的名字一样,这部分内存并不属于虚拟机所管辖的内存区域,Java提供了NIO API来让用户操作这部分内存,以此来获取更高的效率,因为减少了复制次数。

Java 对象

Java 对象的创建

虚拟机在遇到一条New 指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的引用,并且检查这个符号引用代表的class,是否以i纪念馆被加载,解析,初始化。这时候,若是没有以上流程的话,则去执行相应的类加载流程。这段话其实不用想太多,这样去理解;

在执行New之前,虚拟机会去检查代表这个对象的模版class是否已经加载到内存中了,若是没有加载的话,它不知道这个对象如何创建对不对?所以会根据之前提到的,从运行时常量池中,获取到该class的符号引用,并将它初始化。

上一步之后,需要先给新生对象分配内容,对象所需要的内存在类加载完成后就可以完全确定了,这很好理解,有了模版,创建的东西都是在它的规划之类,这里分配对象内存等同于把一块确定大小的内存从Java堆中划出来。虚拟机有两种分配内存的方式:

  • 空闲列表
  • 指针碰撞

先不用纠结这两种方法,一句话来说,就是根据内存是否是连续规整来使用不同的内存分配划分。

在内存分配完毕之后,虚拟机需要将分配的内存空间都初始化为零值。这一步操作就保证了对象的实例字段在Java代码中不用赋值就可以直接使用,因为此时,程序能访问到这些字段的数据类型所对应的零值。

到这一步,一个对象就已经诞生了,但是它现在还有实际的意义,需要继续对它初始化,这一步是初始化用户的内容,执行\方法,此方法执行完成后,按照用户的思路,这时候才真正完成了对象的初始化。

Java对象的内存布局

Java对象的内存布局主要分为以下三部分:

  • 对象头
  • 实例数据
  • 对齐填充

对于对象头,主要是存储了一些对象自身的运行时数据,这展开就比较多,比较复杂了。可以这么理解,类比,网络协议Http的Http Header,它里面存储了很多用于客户端,服务端信令交互的内容,都是为了说明自身目前的状态,以便别人了解而做出相应的处理。实际上,Java 对象头也是如此的。

实例数据这是对象真正存储有效信息的区域

对齐填充没什么好讨论的,主要就是为了方便处理数据。

Java内存错误

既然是Java内存区域,那有错自然是内存相关错误,之前有说了,几乎所有的Java内存区域都会抛出OutOfMemoryError,除了程序计数器在执行native方法的时候。

总结下可能出现的内存区域错误:

  • Java 堆溢出
  • 虚拟机栈溢出
  • 本地方法栈溢出
  • 方法区与运行时常量池溢出
  • 本机直接内存溢出
原创粉丝点击