Java内存区域与内存溢出异常

前言

项目中,本人编写的程序在开发时调试都没什么问题,上线运行了一段时间,便出现了请求卡顿的情况,有时甚至会出现崩溃。这一切造成的原因,是因为本人对Java只是浅层面的会用,而对它底层和在JVM中运行的原理知之甚少。众所周知,《深入理解Java虚拟机》是Java开发人员的必修基础书,亡羊补牢,为时不晚,是时候捧起书来好好学一学了。

本章知识点

本章是《深入理解Java虚拟机》的第二章知识梳理,主要内容有:

  • 运行时数据区域
  • 对象创建过程
  • 对象内存布局与访问定位

Java运行时数据区域

JVM为了更好地进行内存管理,将它所管理的内存划分为若干个不同的数据区域,根据《Java虚拟机规范(JavaSE 7版)》的规定,JVM会包括以下几个运行时数据区域:

区域名称 是否线程共享
程序计数器(Program Counter Register) N
虚拟机栈(VM Stack) N
本地方法栈(Native Method Stack) N
堆(Heap) Y
方法区(Method Area) Y

上表中是否线程共享为N代表着该区域是线程私有的内存,不同线程拥有各自的内存区域且互相不受影响,独立存储。下面,是对各区域的解释说明:

程序计数器

我们在初学JAVA的时候就知道,我们编写的.java格式的源码需要通过javac编译成字节码才能被JVM执行,在这里,程序计数器可以看作是当前线程所执行的字节码的行号指示器,JVM可以通过改变它的值来选取下一条线程中的指令,并且控制分支、循环、跳转等操作都依赖于它。
JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器来记录当前线程的执行位置。
如果当前线程正在执行一个Java方法(可以看做是类中的普通方法),这个计数器记录的是正在执行的虚拟机字节码指令地址。如果正在执行的是Native方法(一个Native 方法就是一个java调用非java代码的接口),这个计数器则为空。该内存区域是唯一没有规定任何OutOfMemoryError情况的区域

虚拟机栈

虚拟机栈也是线程私有的。每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表(七种基本数据类型、对象引用类型地址)、操作数栈动态连接方法出口等信息。每个方法从调用到执行完成,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
除了64位长度的long和double类型占2个局部变量空间(Slot),其余的数据类型只占用1个;
该内存区域规定了两种异常情况:

  • StackOverflowError 线程请求的栈深度大于虚拟机允许的深度
  • OutofMemoryError 虚拟机栈动态扩展时无法申请到足够的内存

本地方法栈

本地方法栈类似于虚拟机栈,他们的区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。HotSpot虚拟机把本地方法栈与虚拟机栈合二为一。

Java堆

堆是JVM所管理的内存中最大的一块,目的是存放对象实例,所有线程共享该空间。
为了更好地垃圾回收(GC),通常将该空间划分为新生代老年代,更细致的划分如使用复制算法会将其划分为Eden空间、From Survivor空间、To Survivor空间。
堆可以处于不连续的物理内存空间中,只需要是逻辑上连续即可。可以通过-Xmx-Xms控制其空间拓展,当堆无法拓展时,会抛出OutOufMemoryError异常。

方法区

方法区也是线程间共享,用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据,其中运行时常量池用于存放编译期生成的各种字面量(如:String a = "aaa"中的"aaa"是字面量)和符号引用(符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可)。
因为HotSpot JVM选择把GC分代收集扩展至该区,所以因为该区域经常被称为永久代
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

直接内存

直接内存并不属于JVM运行时数据区的一部分,而是属于本机内存,该部分内存也会被频繁使用,使用NIO时,会分配直接内存给缓冲区,如果忽略了直接内存并且超出了物理内存的限制,将抛出OutOfMemoryError异常。

HotSpot虚拟机对象探秘

在大概了解了JVM所管理的内存分配区域后,便可以将对象按照各区域的功能进行拆分,放入对应区域中。接下来,我们来看一下对象的创建。

对象的创建

我们在代码中是创建对象很简单,只要通过下面这段代码便完成了对象的创建:

/**
 * @author ccoke
 */
public class Student {
  private String name;
  private int age;
  public Student(String name, int age) {
    this.name = name;
    this.age = age;
  }
  public static void main(String[] args) {
    Student student = new Student("ccoke", 24);
    System.out.println("student name:" + student.name);
  }
}

那么在生成对象之前,JVM为我们做了哪些事情?简略步骤如下:

  1. 监测类加载。虚拟机遇到一条new指令时,检查这个指令是否在方法区常量池中定位到对应类的符号引用,并且检查该符号引用代表的类是否已被加载解析初始化过。如果没有,则必须先执行相应的类加载过程
  2. 为新生对象分配内存。内存的分配方式可以分为指针碰撞空闲列表指针碰撞指的是Java堆中内存绝对规整,所有正在使用的内存都放一边,空闲的放另一边,中间使用一个指针指示分界点,分配内存时把指针向空闲那边挪动一段与对象相等的距离;空闲列表指的是Java堆中的内存不是规整的,已使用的内存和未使用的内存互相交错,虚拟机需要维护一个表,用于记录哪些内存块可用。Java堆是否规整由采用的垃圾收集器是否带有压缩整理功能决定。
  3. 将分配到的内存空间初始化为零值(如上段代码中,会将name设置为null,age设置为0),并设置对象头
  4. 执行<init>方法,将对象根据程序员的意愿初始化(本人的理解是类似于执行构造函数,如上段代码中将name设置为"ccoke", age设置为24)。

对象的内存布局

这部分是对象在内存中存储区域的补充,在HotSpot虚拟机中,对象在内存中可分为三块区域:对象头实例数据对齐填充
对象头包括两部分信息,第一个部分用于存储对象自身的运行时的数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机用这个指针来确定这个对象是那个类的实例。
实例数据用于存储对象的有效信息,也是程序代码中所定义的各种类型的字段内容,HotSpot默认的分配策略中,相同宽度的字段总是被分配到一起,在满足这个的条件下,在父类定义的变量会出现在子类之前。
对象填充没有特别含义,仅仅起着占位符的作用。因为HotSpot的自动内存管理系统要求对象起始地址必须是8字节的整数倍(对象的大小必须是8字节的整数倍),当对象实例部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

对象生成后,便可以开始对对象进行调用了。一般我们调用对象,目的就是为了获取对象的属性值,或者是为了使用对象对应类提供的方法。前面我们讲过,对象的实例属性再堆中,而类信息(方法)在方法区中,那么,我们如何通过引用变量来获取它们呢?主流的访问对象的方式有使用句柄直接指针两种。
使用句柄访问对象的方式如下图所示,Java堆中划分出一块内存作句柄池,用于存放对象实例数据的指针类型数据的指针,栈上的引用变量存储的是对象的句柄地址。这个方式的好处是当对象被移动时,只改变句柄中的实例数据指针,引用变量本身并不需要修改。

通过句柄访问对象.jpg

而使用直接指针访问对象,跟通过句柄访问对象不同的是,栈上的引用变量存储的是对象在堆中的地址,这个方式的好处是速度快,节省了一次指针定位的时间开销。
通过直接指针访问对象.jpg

总结

通过本章的学习,我们基本知道了JVM运行时数据区域以及功能,概括一下,程序计数器用来记录当前线程执行字节码的位置;虚拟机栈会在每个方法执行时创建一个用于存储局部变量表、操作数栈、动态连接、方法出口的栈帧;本地方法栈类似于虚拟机栈,它为虚拟机使用到的Native方法服务;用于存放对象实例,在垃圾回收中,通常将该空间划分为新生代和老年代,并且它只需要处于逻辑连续的物理内存空间;方法区用于存储已被JVM加载的类信息、常量、静态变量、即时编译器后的代码等数据,运行时常量池包含其中,用于存放编译期生成的字面量与符号引用。除了堆与方法区,其他区域都是线程私有的。接下来我们学习了简单的对象创建过程、对象内存布局与访问定位,加深了对Java内存区域的理解。第2章总体来说没有难度,理解并掌握它,我们下章见!

...
// hey guy!
if( isValuable(this.article) && (like(this.article) || follow("ccoke"))) {
  System.out.println("Thank you! XD");
} else {
  System.out.println("I will continue to work hard!T.T");
}
...
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容