热衷学习,热衷生活!😄

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、JDK1.6、JDK1.7、JDK1.8内存模型演变

JDK1.6、JDK1.7、JDK1.8内存模型演变规程如下图:

从上图我们可以看出这些版本的JVM内存模型主要有以下差异:

  • JDK1.6:有永久代,静态变量存放在永久代(方法区)。
  • JDK1.7:有永久代,但是已经把字符串常量池、静态变量存放到堆中,逐渐减少永久代的使用。
  • JDK1.8:无永久代,运行时常量池、类常量池都保存到元数据区,也就是常说的元空间。但字符串常量池仍存在堆中。

二、JVM运行时内存区域概述

JVM在运行时Java程序时,会把管理的内存划分为若干个区域,每个区域都有自己的用途和创建销毁时间。如下图所示,可以分为两大部分,线程私有区域和线程共享区域。线程私有区域包括:虚拟机栈、本地方法栈、程序计数器,线程共享区域包括:Java堆、方法区。

线程私有区域

虚拟机栈

线程私有的,与线程同一时间创建,管理Java方法执行时的内存模式。每个方法执行的时候都会创建一个帧栈来储存方法的局部变量表、操作数帧、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法可调用的深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的深度超过最大可用深度,则会抛出stackOverflowError错误;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemeryError错误。

  • 局部变量表:是一组变量值存储空间,存放方法的参数以及局部变量,以及编译期间可知的八大基本数据类型,对象引用和 returnAddress类型(指向了一条字节码指令的地址)。
  • 操作数帧:保存计算过程中的中间结果,同时作为计算过程中变量的临时储存空间。
本地方法栈

与虚拟机栈结构作用相似。不同的是虚拟机栈为Java方法服务,本地方法栈为本地方法服务(native方法)。

程序计数器

较小的内存空间,记录这当前线程所执行的字节码的行号,字节码解析的时候通过改变程序计数器的值来获取下一条需要执行的指定。在多线程程序中,每个线程都有独立的程序计数器,所以程序计数器是私有的。如果程序执行的是Java方法,那么这个程序计数器记录着当前执行的字节码指令地址,如果执行的是一个navtive方法,则计数器为空。程序计数器区域没有任何OutOfMemoryError定义。

线程共享区域

Java堆

Java堆是被所有线程共享的一块内存区域,存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老生代。刚创建的对象在新生代Eden区中,经过GC后进入新生代的S0区,再经过GC进入新生代的S1区15次GC后扔存在则进入老生代。若堆的空间不够实例的分配,则会抛出OutofMemeryError错误。

JDK8元空间

元空间是从虚拟机Java堆中转移到本地内存,默认情况下,元空间的大小受本地内存的限制,说白了也就是以后不会因为永久代空间不够而抛出 OOM 异常出现了。 jdk1.8 以前版本的 class 和 JAR 包数据存储在 PermGen 下面 PermGen 大小是固定的,而且项目之间无法共用,公有的 class ,所以比较容易出现 OOM 异常。

方法区

方法区是线程共享的,用于存放虚拟机加载的类信息、常量、静态变量以及即时编译期编译后的代码。

三、堆和栈的区别

栈是运行时单位,代表这逻辑,储存基本类型和堆中对象引用,所在区连续,没有碎片,是线程私有的。

堆是储存单位,代表这数据,可被多个栈共享,所在区域连续,会有碎片,是线程共享的。

  • 功能不同:栈内存是存储局部变量和方法调用、堆中对象引用,而堆是用来储存对象的,无论是成员变量、局部变量还是类变量,它们指向的对象都储存在堆内存中。
  • 共享性不同:栈是线程私有的,堆是线程共享的。
  • 异常错误不同:如果栈内存或者堆内存不足都会抛出异常,栈抛出的错是java.lang.StackOverFlowError,堆内存抛出的错是java.lang.OutOfMemoryError
  • 空间大小:栈的空间远远小于堆的。

四、OOM常见的原因

  • 内存加载的数据量太大:一次性从数据库读取多数据。
  • 集合类中有对对象的引用,使用后未清空,GC不能进行回收。
  • 代码中存在循环产生过多的对象。
  • 启动参数堆内存值小。