JVM01 - java 内存区域和相关异常

本贴最后更新于 2064 天前,其中的信息可能已经事过景迁

 最近几天又重新拜读了一下周志明的《深入理解 java 虚拟机》一书,这本书可真的是经典,时隔一年再读又是另外一番感悟。鉴于上次匆匆读完后没做任何笔记导致好像遗忘速度挺快的,所以这次就好好做一下笔记吧。(本书基于 jdk1.7,1.8 后版本变化好像有点大,所以现在就做笔记的过程中也跟 jdk1.8 中 jvm 的做下对比吧,与时俱进...)

Java 内存区域

本书中讲到的内存区域有五块

  1. 本地方法栈(Native Method Stack - 线程私有)
  2. 虚拟机栈(VM Stack - 线程私有)
  3. 方法区(Method Area - 线程共享)
  4. java 堆(Heap - 线程共享)
  5. 程序计数器(Program Counter Register - 线程私有)

JVM Memory area 图片来自于网络

几个概念问题

  1. 解释方法区和永久代

方法区(method area)只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT 编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。而永久代是 Hotspot 虚拟机特有的概念,是方法区的一种实现,别的 JVM 都没有这个东西。方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。

  1. 新生代和老年代

HotSpot 虚拟机堆内存被分为新生代和老年代,对堆内存进行分代管理,新生代进一步可以划分为 Eden 空间,From Survivor 空间、To Survivor 空间。

jdk1.8 永久代被元空间所替代了。

jdk8 jvm 图片来自于网络

如上图所示,jdk1.8 后内存区域只有四块了,去除了方法区,其中的一部分移到堆中(比如常量池),另外的直接移到外部内存中了。

各内存区域介绍

虚拟机栈

Java 虚拟机栈是线程私有的,它的生命周期与线程相同。它描述的是 java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。

局部变量表存放了编译期可知的各种基本数据类型、对象引用和 returnAddress 类型。注意:局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的。

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。
如果在动态扩展内存的时候无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,区别:虚拟机栈为虚拟机执行 java 方法服务,本地方法栈则是为虚拟机使用到的 Native 方法服务。

和虚拟机栈出现的异常一样。

方法区(jdk1.8 废弃,被元空间取代)

方法区也是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。相对而言,垃圾收集行为在这个区域是比较少出现的,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

java 堆

java 堆是被线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一目的是存放对象实例(当然也不是绝对,还有栈上分配等)。java 堆是垃圾收集器管理的主要区域,由于现在收集器基本都是采用的分代收集算法,所以 java 堆还可以细分为:新生代、老年代。

实现堆可以是固定大小的,也可以通过设置配置文件设置该为可扩展的。
如果堆上没有内存进行分配,并无法进行扩展时,将会抛出 OutOfMemoryError 异常。

程序计数器

程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。它属于线程私有的内存。如果线程正在执行一个 java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的事 Native 方法,这个计数器值为空。

此内存区域是唯一一个没有规定“内存溢出”情况的区域。

直接内存

直接内存是本地物理机的内存,并不是虚拟机运行时的数据区的一部分。也不是 java 虚拟机规范中定义的内存区域。由于 jdk1.4 中加入了 NIO 类,引入了一种基于通道(Channel)和缓冲区(Buffer)的 I/O 方式,所以这部分内存也被频繁的使用。它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储再 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。因为避免了再 Java 堆和 Native 堆中来复制数据,所以能在一些场景中显著提高性能。

此内存区也可能导致 OutOfMemoryError 异常出现


下面几种生成对象时的内存情况

  1. int i =10 一个方法对应一个栈帧,方法中的基本数据类型变量直接在栈帧中分配。如果是 static、final 类型的基本数据类型则存储在运行时常量池中,和 String 一样。
  2. Object o1 = new Object();,对象引用(Object o1)存储在栈帧中,但是对象数据(new Object())存储在 java 堆中,对象类型数据(Class 等信息)存储在方法区中。
  3. String s1 = new String("abcd");,使用 new 声明的对象,对象引用(String s1)存储在栈帧中,对象数据(new String(“abcd”))存储在 java 堆中,字符串值(“abcd”)存储在运行时常量池中。
  4. String s2 = “abc”,对象引用(String s2)存储在栈帧中,字符串值(“abc”)存储在运行时常量池中。

内存溢出异常(OutOfMemoryError)

此异常一般产生的原因:由于 JVM 内存过小,产生了过多的垃圾,并且 gc 回收后还不够用就会产生该异常。

  • 内存中加载的数据量是否过于庞大,比如一次从数据库取出大量数据
  • gc 是否回收异常。比如集合类有对对象的引用,但是使用完后没有正常的清空,而 gc 无法回收,则内存越来越大。
  • 代码中有死循环产生大量对象。
  • 参数的内存值设定过小。

增加 JVM 物理内存使用量,VM Args: -Xms256m -Xmx512m ,表示 JVM 分配的堆内存最小为 256MB,最大为 512MB。

栈溢出异常(StackOverflowError)

如果 java 栈的栈深度大于 JVM 允许的深度,就会抛出该错误。如使用递归的方式不同的调用某个方法就会引起该异常(上面所说,每个方法调用执行时就会在调用栈上分配一个栈帧, 这个栈帧包含引用方法的各种信息)。

相关指令:VM Args: -Xss256k,表示 JVM 分配的栈容量为 256KB。


参考
https://www.cnblogs.com/zhouyuqin/p/5161677.html
https://segmentfault.com/a/1190000016694247

  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...