JVM内存的那些事儿

生活赋予我们一种巨大的和无限高贵的礼品,这就是青春:充满着力量,充满着期待志愿,充满着求知和斗争的志向,充满着希望信心和青春。 —— 奥斯特洛夫斯基

JVM内存简介

Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。Java虚拟机所管理的内存区域包括以下几个运行时数据区域,如下图所示:

JVM内存区域

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  在多线程环境中,每个线程都有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储,因此程序计数器是线程私有的。
  如果线程正在执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计数器则为空(Undefined)。

Java虚拟机栈

Java虚拟机栈也是线程私有的,它和Java线程在同一时间创建,它保持方法的局部变量、部分结果,并参与方法的调用和返回。
  Java虚拟机栈规范允许Java栈的大小是动态是动态或者固定的。在Java虚拟机栈规范中,定义了两种异常与栈空间有关:
StackOverflowError和OutOfMemoryError。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机动态扩展时无法申请到足够内存时,将抛出OutOfMemoryError异常。
  在Hotspot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。
  以下示例展示了栈的溢出。

public class StackTest {
    private int count = 0;
    public void recursion() {
        count++;
        recursion();
    }
    @Test
    public void testStack() {
        try {
            recursion();
        } catch (Throwable e) {
            System.out.println("deep of stack is " + count);
            e.printStackTrace();
        }
    }
}

默认情况下,程序输出结果:

deep of stack is 18904
java.lang.StackOverflowError

使用参数-Xss2M再次执行程序,程序输出结果:

deep of stack is 42442
java.lang.StackOverflowError

很明显,栈的内存增大后,程序支持的函数调用深度也同时增大。

public class StackTest {
    private int count = 0;
    public void recursion(long a, long b, long c, long d) {
        long e = 0, f = 0, g = 0;
        count++;
        recursion(a, b, c, d);
    }

    @Test
    public void testStack() {
        try {
            recursion(1L, 2L, 3L, 4L);
        } catch (Throwable e) {
            System.out.println("deep of stack is " + count);
            e.printStackTrace();
        }
    }
}

同样使用参数-Xss2M执行程序,程序输出结果:

deep of stack is 21055
java.lang.StackOverflowError

随着参数和局部变量的增多,栈帧的空间也随之增大。(函数调用次数由无参时的42442降至21055)。

虚拟机栈在运行时使用栈帧的数据结构保存上下文数据。在栈帧中,存放了方法的局部变量表、操作数栈、动态连接方法和返回地址等信息。每一个方法的调用都伴随着栈帧的入栈操作,相应地,方法的返回则表示栈帧的出栈操作。方法调用时,方法参数和局部变量相对较多,那么局部变量表会比较大,栈帧会膨胀以满足需求,因此单个方法调用所需的栈空间大小也会比较多。

栈帧结构图如下:

栈帧结构

注意:对一个函数而言,它的参数越多,内部局部变量越多,它的栈帧就越大,其可达深度就越低。

  • 局部变量表
      用于存放方法参数和方法内部定义的局部变量,其大小在代码编译期间已经确定,在方法运行期间不会改变。局部变量表以变量槽(Slot)为最小存储单位,每个Slot能够存放一个boolean、byte、char、shot、int、float、reference和returnAddress类型的32位数据,对于64位的数据类型long和double,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。
      在方法执行时,如果是实例方法,即非static方法,局部变量表中第0位Slot默认存放对象实例的引用(虚拟机通过局部变量表将当前对象传递给当前方法),方法中可以通过关键字 this 进行访问,方法参数按照参数列表顺序,从第1位Slot开始分配,方法内部变量则按照定义顺序进行分配其余的Slot。

  • 操作数栈
      操作数栈是一个基本的栈,那么它自然也遵守栈的后入先出的原则。其次,它里面主要存放的是一些算数运算用到的参数也可能是中间结果,也可能是在调用其他方法时需要用到的参数。通过这点可以看出,方法刚刚开始执行的时候,这个里面是空的。最后 要说明的是操作数栈中可以存放任意的Java数据类型,包括long和double,且32位的数据类型占一个栈空间,64位的数据类型占2个栈空间。

  • 动态连接
      在说明什么是动态连接之前先看看方法的大概调用过程。首先,在虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用。如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法。这时候就有一点需要注意,如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。

动态连接.
  • 返回地址
      方法的返回分为两种情况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法。
      不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,如果是因为异常退出的,则是需要通过异常处理表来确定。

本地方法栈

本地方法栈和虚拟机栈类似,两者之间的区别是本地方法栈为Native方法服务。

Java堆

Java堆是Java运行时内存中最为重要的部分,几乎所有的对象实例以及数组都都是在堆中分配空间的。Java堆是所有线程共享的内存区域。
Java堆分为新生代和老年代两部分,新生代用于存放刚刚产生的对象和年轻的对象,(大对象除外,直接进入老年代,因为大对象占用空间多,为了有足够空间容纳大对象,JVM不得不移动大量新生代中的年轻对象至老年代,这对GC来说是不利的,另外,若是由于内存空间紧张,JVM很可能不得不将部分年轻对象提前向老年代压缩),如果对象经历过N(该次数可通过参数配置,默认是15)次GC而未被回收,则会被移入老年代。
  新生代又可进一步分为eden、from space(s0)、to space(s1)(默认eden:s0:s1=8:1:1,该比例可配置)。eden,即对象的出生地,大部分对象刚建立时,都会存放在这里。s0和s1为survivor空间,直译为幸存者,也就是说存放在其中的对象,至少经历了一次垃圾回收,并得以幸存,如果在幸存区的对象到了指定年龄仍未被回收,则有机会进入老年代。

方法区

方法区(又称永久代)和Java堆一样,是所有线程共享的内存区域。方法区主要保存的是类的元数据。
  方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。类型信息包括类的完整名称、父类的完整名称、类型修饰符(public/protected/private)和类型的直接接口类表。常量池包括这个类的方法、域等信息所引用的常量。域信息包括域名称、域类型和域修饰符。方法信息包括方法名称、返回类型、方法参数、方法修饰符、方法字节码、操作数栈和方法栈帧的局部变量区大小以及异常表。总之,方法区内保存的信息,大部分是来自于class文件。
  运行时常量池用于存放编译期间生成的各种字面常量(文本字符串、声明为final的常量值)和符号引用(类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符)。在JDK1.6中,常量池是方法区的一部分,在JDK1.7中,常量池存放在堆内存里,在JDK1.8中,常量池存放在MetaSpace里。
  目前1.8的HotSpot中,已经将方法区移除,取而代之的是MetaSpace。
  当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
  在HotSpot虚拟机中,在永久区中的对象,同样也是可以被回收的。对永久区GC的回收,主要从以下两个方面分析:一是GC对永久区的常量池的回收,二是永久区对类元数据的回收。

  • 常量池的回收
      只要常量池中的常量没有被任何地方引用,就可以被回收。
  • 类元数据的回收
      所有该类的实例被回收,且装载该类的ClassLoader被回收。

JVM内存分配参数

  • -Xms:设置堆的初始大小。
  • -Xmx:设置堆的最大值。
  • -Xss:设置线程栈的大小。
  • -XX:MinHeapFreeRatio:设置堆空间最小空闲比例。当堆空间的空闲内存小于这个值时,便会扩展堆空间。
  • -XX:MaxHeapFreeRatio:设置堆空间最大空闲比例。当堆空间的空闲内存大于这个值时,便会压缩堆空间,得到一个较小的堆。
  • -XX:NewSize:设置新生代大小。
  • -XX:NewRatio:设置老年代与新生代的比例,它等于老年代大小除以新生代大小。
  • -XX:SurvivorRatio:设置新生代中Eden与survivor区的比例。
  • -XX:MaxPerPermSize:设置最大的持久区大小。
  • -XX:PerPermSize:设置持久区的初始大小。
  • -XX:TargetSurvivorRatio:设置survivor区的可使用率。当survivor区的空间使用率达到这个值时,会将对象送入老年代。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • JVM内存模型Java虚拟机(Java Virtual Machine=JVM)的内存空间分为五个部分,分别是: ...
    光剑书架上的书阅读 2,477评论 2 26
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,515评论 3 83
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,169评论 11 349
  • 阿里招司机,面试官问: 你知道发动机的原理吗?你知道离合器是如何实现的?发动机的曲柄连杆机构是做了什么事情?电控燃...
    月光在心中阅读 738评论 0 0
  • 文/琪琪love 有一种距离, 明明很近却无法触及。 有一种感情, 明明喜欢却不敢表明。 有一种逃避, 因为得不到...
    琪琪love阅读 713评论 0 2