深入理解 JVM 内存结构

1. 概述

JVM 把内存进行了划分,不同的内存区域有不同的功能。有的内存区域是线程私有的,比如 Java 虚拟机栈、本地方法栈和程序计数器,每一条线程都有自己独立的空间。有的内存区域是线程共享的,比如方法区和堆。

所以不同内存区域的功能、作用域和生命周期是不同的。本文做一个详细的分析。

根据 JVM 虚拟机规范,内存结构如下:

JVM-内存区域

JVM 虚拟机规范属于概念模型,具体的实现各个厂商的会有所差异。比如方法区的设计,hotspot 在 1.7 之前使用永久代,1.7 后使用元空间。

本文主要分析 HotSpot 虚拟机的实现。

2. 程序计数器

JVM 支持多线程,采用时间片轮转的方式实现多线程并发。一个内核每一刻只能有一个线程执行,多线程下需要线程上下文切换。为了确保切换过程中,不同的线程指令和数据不会发生混乱,需要单独开辟内存空间给每个线程,进行线程隔离。这些区域包含了程序计数器、虚拟机栈、本地方法栈。这些都是线程私有内存,生命周期和线程一致。

如果执行的不是本地方法,程序计数器记录当前线程执行的指令地址,字节码解释器通过改变该计数器的值,来决定选取下一个要执行的指令。如果执行的是本地方法,值为空(undefined)。

程序计数器的内存空间非常小,是 JVM 规定的唯一不会发生内存溢出(Out Of Memory)的区域。

3. Java 虚拟机栈

Java 虚拟机栈由栈帧组成,Java 虚拟机栈和其他常规语言的栈类似,存储本地变量或部分计算结果,处理方法的调用和返回。虚拟机栈内容不能进行直接操作,只能用来进行栈帧的入栈和出栈。方法的调用到执行完成对应的就是栈帧的入栈和出栈过程。

Java 虚拟机栈的生命周期和线程对应,在线程创建的同时创建,和程序计数器一样都是线程私有内存区域。

JVM-虚拟机栈

Java 虚拟机规范对虚拟机栈大小有这样的描述:

  • 可以使用固定大小或者动态扩展和收缩。如果是固定大小,空间大小在栈创建的时候就会确定下来。
  • 可以配置 Java 虚拟机栈的初始大小。
  • 如果栈空间可以动态扩展或者收缩,可以配置栈的最大值和最小值。

HotSpot 虚拟机栈的配置:

  • -Xss,设置虚拟机栈大小,JDK1.5 之后默认为 1M。栈深度受到这个堆栈大小的约束。在固定物理内存下减小 Java 虚拟机栈大小可以产生更多线程,但是一个进程的线程数量有约束,不能无限增加。

Java 虚拟机栈可能会发生的异常有:

  • 如果线程请求需要的栈深度大于 JVM 限定的,会发生 StackOverflowError 异常。
  • 如果 JVM 大小可以动态扩展,在扩展的时候内存不足,或者在创建新线程时内存不够创建虚拟机栈,均会发生 OutOfMemoryError 异常。

3.1. 栈深度

方法的从调用到执行完成,对应了虚拟机栈的入栈到出栈的过程。

在编译期就可以确认局部变量表的大小和操作数栈的深度,并且写入到方法表的 code 属性中,运行期间不会发生改变。所以在编译器每个栈帧的需要大小就可以确定了。栈深度由运行期决定。

具体的栈深度受虚拟机栈大小和栈帧大小的影响,要看使用了多少栈帧,栈帧大小多少。每个栈帧的大小不一定一样,取决于各栈帧对应方法的局部变量表和操作数栈大小等。

假设我们的虚拟机栈大小固定,栈帧数量达到最大值,也就是达到最大深度,深度大小和栈帧大小的示意图如下:

JVM-虚拟机栈深度

上面的示意图可以看出,在 Java 虚拟机栈大小固定的情况下,如果每个栈帧都很大,最大可用深度就会变小。

上面只是一个示意图,实际上虚拟机栈深度没这么小。默认情况下 Java 虚拟机栈有 1M,平时开发时的栈帧也不会很大。

当线程请求的栈深度大于虚拟机的所允许的栈深度会发生 StackOverflowError 异常。毕竟如果一个线程不断地往虚拟机栈中加入栈帧,会消耗掉大量的内存,影响到其他线程的执行。

比如写了一个递归方法,没有设置退出条件,当要超过该线程的虚拟机栈达到最大深度会发生异常。

3.2. 栈帧

栈帧用来存储方法执行需要用到的数据。同时还可以执行动态链接,返回值给方法,分发异常。所以一个栈帧一般会划分成以下几个区域:局部变量表、操作数栈、动态链接、方法出口。

栈帧的生命周期和方法对应,在方法调用的时候就会创建新的栈帧,当方法执行结束时栈帧销毁栈帧。即使是因为未捕获异常退出方法,栈帧也会被销毁。栈帧的内存由 JVM 虚拟机栈分配。每个栈帧有自己独立的局部变量表、操作数栈、指向运行时常量池的引用。

栈帧的内容可扩展,比如加入调试信息。

在编译期就可以根据栈帧对应的方法代码,确定局部变量表和操作数栈的大小。栈帧的具体大小依赖于 JVM 虚拟机的实现。编译期决定了大小,方法被调用时分配内存。

线程在同一时刻只会处理一个栈帧,被称为当前帧,位于 Java 虚拟栈的栈顶。该帧对应的方法被称为当前方法,定义该方法的类被称为当前类。方法的执行会操作当前帧的局部变量表和操作数栈。

调用新方法时,当前帧暂停,新的栈帧加入到虚拟机栈的栈顶并成为新的当前帧,开始处理新方法。当方法结束调用,当前帧出栈,返回处理结果,回到上一个栈帧,上一个栈帧成为当前帧,继续操作局部变量表和操作数栈。

栈帧属于当前线程私有,不会被其他线程引用到。

3.2.1. 局部变量表

每一个栈帧都会有一个局部变量表,大小在编译期就决定,用来记录方法执行需要用到的请求参数、局部变量,如果不是静态方法的话,还会存储 this 指针来表示当前对象实例。

局部变量的存储基本单位为 变量槽(Variable Slot)。单个 Slot 可以存储 boolean,byte,char,short,int,float,reference 或者 returnAddress。两个 Slot 可以存储 long 和 double。虚拟机规范没有对 Slot 的物理内存大小做出明确规定,可以随着处理器、操作系统和虚拟机的不同而变化。但因为 int、float 等都可以用 32 位的物理内存存放,所以一个 Slot 的物理内存必须大于 32 位

局部变量表采用 索引 进行寻址。第一个局部变量的索引为 0。在实例方法中,始终使用局部变量 0 用来表示当前对象实例,在 Java 中就是 this 指针。所以实例方法的局部变量的索引总是从 1 开始。

long 和 double 比较特殊,需要使用两个连续的 Slot 存储。这样会占用两个索引,取值小的那个。比如一个 double 存入局部变量表,它的索引值是 n,其实占用了 n 和 n+1 两个索引,而 n+1索引是无法加载的。下一个局部变量的索引为 n+2。虚拟机规范并没有要求 n 一定是偶数,所以在在局部变量表中 long 和 double 并不一定是要 64 位对齐的。不同 JVM 的实现,可以选择合适的方式实现两个局部变量存储 long 和 double。

这里做个实验,创建一个空方法,请求参数包含所有基础数据类型和一个 String 引用类型,方法内有一个 String 局部变量。

public void show(boolean a, byte b, char c, short d, int e, long f, float h, double i, String j) {
    String str = "str";
}

使用 javap -v 查看 show 方法在 class 文件中的局部变量表。

  public void show(boolean, byte, char, short, int, long, float, double, java.lang.String);
    descriptor: (ZBCSIJFDLjava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=13, args_size=10
         0: ldc           #2                  // String str
         2: astore        12
         4: return
      LineNumberTable:
        line 14: 0
        line 15: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Loblee/demo/jvm/stack/SimpleObject;
            0       5     1     a   Z
            0       5     2     b   B
            0       5     3     c   C
            0       5     4     d   S
            0       5     5     e   I
            0       5     6     f   J
            0       5     8     h   F
            0       5     9     i   D
            0       5    11     j   Ljava/lang/String;
            4       1    12   str   Ljava/lang/String;

这个方法的为局部变量表 LocalVariableTable ,类加载后会作为方法的元数据存储到方法区,然后方法被调用的时候载入到新创建的栈帧中。

可以看到编译期已经确认了表中每个局部变量的索引和大小。局部变量表的大小已经写入到 Code 属性: locals=13

这 13 个基本单位是如何计算出来的?我们上面的案例,所有方法参数一共需要的基本单位数 1 + 1 + 1 + 1 + 1 + 2 + 1 + 2 + 1 = 11 ,一个局部变量 str 占用 1 个 Slot,有 12 个基本单位了。还有一个 Slot 呢?

这个是实例方法,加入了 this 指针用来表示当前对象实例的引用,在 Slot 0 中:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0       5     0  this   Loblee/demo/jvm/stack/SimpleObject;

this 指针占用 1 个 Slot,所以局部变量表总体大小为 13 个 Slot。

因为 this 指针是通过参数默认传递给方法的,应该归到方法参数中,所以实际该方法有 10 个参数,也写入到了 code 属性:args_size=10

从反编译的局部变量表还可以看到索引的设计,show 中参数 f 为 long 类型,索引到 Slot 6,因为占用两个 Slot,下一个变量 h 索引到 Slot 8。

JVM 对局部变量表进行了优化,变量槽 Slot 是可以复用的。

如果是静态方法的话就不存在 this 引用了。比如我们创建一个静态方法 staticShow

public static void staticShow(boolean a, byte b, char c) {
    String str = "str";
}

使用 javap -v 查看局部变量表如下:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0       8     0     a   Z
    0       8     1     b   B
    0       8     2     c   C
    3       5     3  str1   Ljava/lang/String;
    7       1     4  str2   Ljava/lang/String;

3.2.2. 操作数栈

每一个栈帧都有一个后进先出(LIFO)的操作数栈。操作数栈应用于字节码执行引擎中,JVM 描述字节码执行引擎是基于 “栈” 的,指的就是操作数栈。

操作数栈的每个条目可以保存 JVM 任何类型的值,long 和 double 占据深度的两个单位,其他类型占据一个单位。操作数栈的最大深度由编译期通过方法要执行的字节码计算出来,并记录在 Code 属性中。

栈帧刚创建时,操作数栈为空。JVM 提供了一系列字节码指令,将数据从局部变量表加载到操作数栈中。还有一些指令,从操作数栈中读取操作数,进行处理,然后把结果入栈。操作数栈还可以用来准备参数传递给方法,或者接收方法返回结果。比如,指令 iadd 用来对两个 int 值进行相加。之前的指令已经将两个 int 值压入到操作数栈中了,iadd 将两个 int 值出栈,相加后将和入栈。

操作数栈中的数据,必须用合适的类型的字节码指令进行操作。比如入栈两个 int 值,不能当做 long 处理。入栈 float 不能使用 iadd 指令进行相加。有少量的 JVM 指令不关心值的类型,这些指令无法修改值。在类加载流程中,类文件的校验阶段,会强制实施。

设计了一个 calculate 方法来做一些加减法计算:

public int calculate(int a, int b) {
    int c = a + b;
    int d = a - b;
    int e = c + d;
    return e;
}

反编译得到:

  public int calculate(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=6, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_1
         5: iload_2
         6: isub
         7: istore        4
         9: iload_3
        10: iload         4
        12: iadd
        13: istore        5
        15: iload         5
        17: ireturn

可以看到操作数栈深度最大为 2,本地变量表大小 6 个 Slot(索引 0 - 5)。这些字节码的解读如下:

         0: iload_1             加载 Slot 1(从局部变量表加载,1 表示索引)。实际为从局部变量表加载 a。
         1: iload_2             加载 Slot 2。实际为从局部变量表加载 a。
         2: iadd                执行加法。实际为 a + b。
         3: istore_3            存储计算结果到 Slot 3。实际为存储 c 到局部变量表。
         4: iload_1             加载 Slot 1。实际为从局部变量表加载 a。
         5: iload_2             加载 Slot 2。实际为从局部变量表加载 b。
         6: isub                执行减法。实际为 a - b。
         7: istore        4     存储计算结果到 Slot 4。实际为存储 d 到局部变量表。
         9: iload_3             加载 Slot 3。实际为从局部变量表加载 c。
        10: iload         4     加载 Slot 4。实际为从局部变量表加载 d。
        12: iadd                执行加法。实际为 c + d。
        13: istore        5     存储计算结果到 Slot 5。实际为存储 e 到局部变量表。
        15: iload         5     加载 Slot 5 的数据。实际为从局部变量表加载 e。
        17: ireturn             返回计算结果

我们传入 a = 1, b = 2 进行计算 calculate(1, 2),第一个加法操作操作数栈的变化如下:

JVM-操作数栈和局部变量表变化

这里的代码是可以优化的,因为局部变量 e 没有做其他计算,可以直接返回。如果直接返回结果会有什么效果?代码如下:

public int calculate(int a, int b) {
    int c = a + b;
    int d = a - b;
    return c + d;
}

查看字节码如下:

  public int calculate(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_1
         5: iload_2
         6: isub
         7: istore        4
         9: iload_3
        10: iload         4
        12: iadd
        13: ireturn

局部变量表少了一个 Slot,也就是原本 e 的存储空间。要执行的字节码指令也少了 3 条。所以平时开发过程中要注意优化,可以提高性能。

3.2.3. 动态链接

每一个帧都包含了一个指向运行时常量池的引用,用来实现字节码中的 动态链接(Dynamic Linking)。类文件中包含了一些字段和方法的符号引用。动态链接会将这些符号引用转换成直接引用,比如在内存中的具体偏移地址。

如果对应的类还没有被加载,会触发该类的加载流程。

符号引用记录在类常量池中,是一个由字面量组成的字符串,和具体地址无关。比如所有对象的类构造方法的符号引用为 java/lang/Object."<init>":()V 。编译并不知道运行时的地址,所以用符号引用代替。

动态链接又称动态绑定。除了该方式,还有种发生在类文件加载过程中,这个这个阶段就把符号引用转换为直接引用,这样的方式为饥饿方式或者静态绑定。

静态绑定和动态绑定都可以归为是类加载机制中的 解析(Resolution) 的一部分。

JVM-类加载机制

可以看出类加载机制中的环节是有可能交叉进行的。比如解析可能发生在准备阶段后,静态绑定。也可能延迟到初始化后,在栈帧创建后进行动态绑定。

绑定只发生一次,绑定后不再更改。

3.2.4. 方法正常结束

方法调用结束,没有发生异常。这里指直接返回结果或者是显式调用 throw 抛出异常。

被调用方法的结果需要传递给调用者方法。被调用的方法会执行和方法返回相关的指令,这些指令和返回值的类型对应。

当前栈会被复原为调用者方法的执行状态,包括局部变量表和操作数栈的数据,程序计数器会跳过刚刚调用方法的指令指向下一条。被调用方法的返回值被加入到操作数栈中,程序继续运行。

3.2.5. 方法异常结束

方法内部发生了异常,而且没有被捕获,方法会被终止,并且没有返回值给调用者。

4. 堆

堆由 JVM 所有的线程共享,一般情况下是 JVM 内存区域中最大的一块。按照 JVM 虚拟机规范,堆是一个用来存储类对象实例或者数组的运行时数据区。

在 HopSpot 上,类对象实例不一定就是放在堆中,应用了 JIT(Just-In-Time) 技术,进行逃逸分析(Escape Analysis)和标量替换(Scalar Replacement)。符合条件的对象实例会在栈上分配。

JVM 启动的时候堆就会创建。堆内对象实例不会显式释放,由自动内存管理系统,也就是垃圾收集器进行回收,是垃圾收集器主要管理区域。JVM 规范没有说明垃圾收集器应该是怎样的,具体由实现由 JVM 厂商来提供。

比如 HotSpot 虚拟机中,垃圾回收器采用分代回收算法,会将堆进行进一步细分,分为新生代和老生代。新生代还可细分为 Eden 、From Survivor 和 To Survivor。这实际上是为了能够更好地服务于垃圾回收。HotSpot 在 JDK 1.7 中堆还有一个永久代,其实是 JVM 规范中方法区的实现,在 JDK1.8 移除。

HotSpot 的 JDK 1.7 堆图示:

JVM-Heap-1.7

HopSpot 的 JDK 1.8 堆图示,永久代(PermGen)被移除,使用元空间(Metaspace)存储类信息。

JVM-Heap-1.8

新生代和老年代的内存分配流程:

  • 优先 Eden 分配,Eden 空间不足会触发 Minor GC。
  • Minor GC 后,Eden + S0 还存活的对象移动到 S1 中,清空 S0。
  • S1 放不下,存活次数达到要求的对象移动到老年代。
  • 大对象直接分配到老年代。
  • 老年代内存不足会发生 Major GC
  • 进行垃圾回收后,Eden 仍然没有足够的空间,抛出 OutOfMemory 异常。

Java 虚拟机规范对堆大小有这样的描述:

  • 可以是固定大小,也可以动态的扩展和收缩。
  • 堆的内存不一定要连续。(逻辑上连续)
  • 可以配置本地方法栈初始大小,如果可动态扩展和收缩,可配置最大值和最小值。

主流虚拟机都是采用可动态扩展和收缩的方式实现的。堆内存物理上可以不连续,但是逻辑上需要连续。

HotPot 虚拟机的堆内存配置:

  • -Xms,初始大小,默认物理内存的 1/64。

  • -Xmx,最大内存,默认物理内存的 1/4。

  • -Xmn,新生代大小,因为持久代的大小一般默认为 64M,在整个堆固定的情况下,增大新生代会相应地减少老年代的大小。官方推荐

  • -XX:NewSize,新生代最小空间大小。

  • -XX:MaxNewSize,新生代最大空间大小。

  • -XX:NewRatio,新生代和老年代的比例,新生代和老年代的默认比例为 1:2。

  • -XX:SurvivorRatio,Eden 和 Survivor 的比例,默认为 Eden:S0:S1 = 8:1:1,即 survivor = 1/10 新生代大小。

HotSpot 采用的就是动态扩展和收缩的方式,根据堆的空闲情况,当空闲大于 70%,会减少至 -Xms;空闲小于 40%,会增大到 -Xmx。所以服务器如果配置 -Xms = -Xmx,可以避免堆自动扩展。

堆会发生的异常:

  • 如果程序请求的堆内存大于 JVM 内存管理系统能提供的最大值,会抛出 OutOfMemoryError 异常。

5. 方法区

方法区由 JVM 所有线程共享。方法区类似一个用来存储编译后的代码的区域。主要用来存储加载的类信息,运行时常量池,类和方法的数据,即时编译后的代码等。

JVM 启动的时候方法区就会创建。

根据 JVM 虚拟机规范,方法区逻辑上是堆的一部分,实现上可以选择不进行垃圾回收,并且没有要求方法区的位置等。所以在方法区的具体实现各个虚拟机又不同的方式。虽然 JVM 虚拟机规范把方法区逻辑上划给了堆,为了和实际堆进行了区分,方法区还叫做 “非堆”。

Java 虚拟机规范对方法区大小的描述:

  • 可以是固定大小,也可以动态的扩展和收缩。
  • 方法区的内存不一定要连续。
  • 用户或者开发者能够配置方法区初始大小,如果方法区可以动态扩展或收缩,需要提供方法区的最大值和最小值。

HotSpot 在 JDK1.7 中方法区内存大小配置:

  • -XX:PermSize,最小可分配空间,初始分配空间。

  • -XX:MaxPermSize,最大可分配空间,默认大小为 64M(64 位 JVM 默认为 85M)

在 JDK1.8 使用了元空间后,方法区的大小配置:

  • -XX:MetaspaceSize,初始空间大小。
  • -XX:MaxMetaspaceSize,最大空间大小,默认是没有限制的。

方法区可能发生的异常:

  • 如果方法区请求的内存无法被满足,抛出 OutOfMemoryError 异常。

5.1. 去永久代过程

HotSpot 虚拟机在 JDK1.7 采用永久代,在堆中分配内存。在 JDK1.8 后使用元空间,使用本地内存。

从 JDK1.7 开始 “去永久代”,JDK 1.7 将静态变量、字符串常量池移动到堆内存中,JDK1.8 去掉永久代,将类信息、即时编译后的代码等移动到了元空间。

JVM-方法区到元空间

之所以要进行去永久代,主要还是该方案存在很多问题,留下很多 bug。主要有:

  • 字符串存在永久代,容易发生内存溢出。
  • 类信息比较难确定大小,永久代的大小难以指定,太小永久代容易 OOM,太大老年代容易 OOM。
  • 永久代 GC 回收复杂,效率低。

6. 运行时常量池

运行时常量池是 class 文件的常量池在运行时的表示。主要有字面量和符号引用。

要理解运行时常量池,我们得先了解 class 的常量池。

创建类 ObjectA 和 Object B,其中 ObjectA 如下:

public class ObjectA {

    private ObjectB b;

    public void setB(ObjectB b) {
        this.b = b;
    }
    
    public ObjectB getB() {
        return b;
    }
}

编译后使用 javap -v 查看 class 文件中的常量池如下。

JVM-Constant Pool

运行时,在进行类加载时,类常量池会被载入到 JVM 方法区。

JVM 虚拟机规范没有约束运行时常量池只能放编译期的常量,虚拟机的实现可以自行支持。比如 HotSpot 虚拟机, Java 调用 String.intern() 方法,可以在运行期把常量加入池中。

在 HotSpot JDK 1.7 之后,对常量池进行了优化:字符串常量池被放在了 JVM 堆中,运行时常量池的字面量也存在 JVM 堆中,而符号引用被移动到了本地内存。

以下的异常可能会发生:

  • 当创建一个 class 或者 interface 时,如果运行时常量池构造需要的内存超过 JVM 所能提供的,抛出 OutOfMemoryError 异常。

7. 本地方法栈

JVM 的实现可能需要使用 "C 栈" 去支持本地方法调用。有可能使用 C 之类的语言,实现 JVM 指令的解释器,也会使用到本地方法栈。本地方法栈和 Java 虚拟机栈类似,只是这里提供的是本地方法服务。虚拟机规范没有明确指出本地方法栈使用什么语言、数据结构等,不同厂商的虚拟机又不同的实现。比如 HotSpot 虚拟机把本地方法栈和 Java 虚拟机栈合并了。

本地方法栈的生命周期线程对应,线程创建的时候创建。如果 JVM 不需要调用本地方法,可以不需要本地方法栈。

JVM 规范对本地方法栈大小的描述

  • 可以使用固定大小,或者动态扩展和收缩。如果是固定大小,当栈被创建的时候能够独立选择。
  • 可以配置本地方法栈初始大小,如果可动态扩展和收缩,可配置最大值和最小值。

以下异常可能发生:

  • 如果线程请求的栈深度大于系统规定的,报 StackOverflowError
  • 如果本地方法栈可以动态扩展,没有足够的内存扩展。或者创建新的线程没有足够的内存创建本地方法栈,抛出 OutOfMemoryError异常。

8. 参考资料

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

推荐阅读更多精彩内容

  • 第二部分 自动内存管理机制 第二章 java内存异常与内存溢出异常 运行数据区域 程序计数器:当前线程所执行的字节...
    小明oh阅读 1,089评论 0 2
  • 内存溢出和内存泄漏的区别 内存溢出:out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,...
    Aimerwhy阅读 689评论 0 1
  • 参考原地址 JVM内存模型 Java虚拟机(Java Virtual Machine=JVM)的内存空间分为五个部...
    流年划破容颜_cc55阅读 326评论 0 0
  • 《深入理解Java虚拟机》笔记_第一遍 先取看完这本书(JVM)后必须掌握的部分。 第一部分 走近 Java 从传...
    xiaogmail阅读 4,966评论 1 34
  • 无题 人生就是一场聚会, 接着是一场分离, 然后又是下一次的聚会, 下一次的分离, 反反复复。 每一次的聚会, 都...
    朝花夕拾木本易阅读 1,230评论 13 14