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

Published on with 130 views

 最近几天又重新拜读了一下周志明的《深入理解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

Responses