第二章 内存区域和 OOM内存溢出

目录
一、内存区域
二、内存溢出和内存泄漏
三、实操OutOfMemoryError
四、对象的创建和对象的内存布局
五、JVM调参集锦

一、内存区域

image-20200712120044489.png

线程共享内存区:方法区、堆
线程独有内存区:虚拟机栈、本地方法栈、程序计数器
注意:线程共享内存区就是我们常要考虑内存回收的地方、而线程独有内存区是会随着线程的执行结束而消亡的,不需要考虑内存回收

1.程序计数器(program counter register)

  • 定义:当前线程所执行的字节码的行号指示器

  • 独占线程:程序计数器属于线程独占去,每个线程都有一个程序计数器

  • 字节码计数器如何工作?

    通过改变程序计数器的值来读取下一条字节码指令
    (分支、循环、异常处理、线程恢复都依赖计数器)

  • java方法:计数器记录的是正在执行的字节码指令的地址

  • native方法:值为空(undefined)

  • 此内存无OOM:此区域是唯一一个在jvm规范中没有固定任何OutOfMemory情况的区域

  • 保留字:java中的goto,现在不用,可能以后会用,暂不开发,防止以后jdk出现新功能的时候之前的程序全部报错

2.虚拟机栈

  • 定义:
    描述的是java方法执行的动态内存模型,
    线程私有(创建一个方法就有创建一个栈帧push,方法调用执行完成,对应的栈帧pop)
    每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
  • 栈帧定义:存放着局部变量表,操作栈,动态链接,方法出口
  1. 局部变量表

    • 定义:基本数据类:(boolean,byte,char,short,int,long,float,double)
    • 对象引用:(reference),可能是引用指针,对象句柄,对象相关的位置,字节码指令地址
    • 局部变量表内存固定:局部变量表内存在编译的期间完成分配,运行期间不会改变局部变量表的大小
    • 总结:Java的八大基础数据(32位可放下),64位高低两个存放,局部的对象则存放用对象的引用
  2. 操作数栈

    • 定义:是栈帧中一个先进后出的数据结构,虚拟机将这里作为他的工作区,方法区中进行add算数运算或者参数传递是通过操作数栈
    • 操作数栈理论独立,但大部分虚拟机会优化,一部分数据可以实现栈帧共享,这样方法调用,无需额外的参数复制传递
    • 总结:操作的元素是任意的java数据类型,一个方法刚刚开始的时候,操作数栈是空的,操作数栈运行方法是JVM一直在入栈/出栈的过程
  3. 动态链接
    每个栈帧都包含一个(指向常量池中该栈帧所属方法)的引用,用来支持动态链接的实现,符号引用(名字)和直接引用(地址)在运行时进行解释和链接的过程,称为动态链接

    • 部分符号引用在类加载阶段(解析)的时候就转化为直接引用,这种转化为静态链接
    • 部分符号引用在运行期间转化为直接引用,这种转化为动态链接
  4. 方法出口:(返回地址)
    正常返回:调用PC程序计数器的地址进行返回,即调用该方法的下一条指令地址
    异常返回:异常处理器表<非栈帧中的>来确定,栈帧一般不会保存这部分信息

  5. 图解


    image-20200716065525058.png
  • StackOverflowError:当栈深度大于虚拟机所允许的最大长度

    Exception in thread "main" java.lang.StackOverflowError

  • OOM:虚拟机栈可动态扩展的情况,无法申请到足够的物理内存,会报OOM错误

3.本地方法栈

(HotSpot不分虚拟机栈和本地方法栈)
因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss .

  • 本地方法栈:为虚拟机使用到的native方法服务
  • 虚拟机栈:为虚拟机执行Java方法(字节码)服务
  • 但本地方法栈并不是用 Java 实现的,而是由 C 语言实现的。
  • 何为native方法:

    "A native method is a Java method whose implementation is provided by non-java code."
    1.Native方法就是java调用非java代码的接口,这个接口的实现不是java写的
    2.C++中,你可以extern "C"告知c++编译器调用c函数

4.堆内存

定义:存放所有对象实例,GC垃圾回收器管理最大的区域,可以处于物理上不连续的内存空间
"不一定"存所有对象实例:JIT编译器优化----出现虚拟机栈上分配

image-20200712150442873.png

  • 新生代(Young Generation):很快就会被GC回收掉的或者不是特别大的对象。

    1.划分为Eden(伊甸园)和Survivor区,最后Survivor由FromSpace和ToSpace组成,三个区域默认情况下是按照8:1:1分配,也可以手动配置。
    2.JVM每次只会使用Eden区和一块Survivor区来为对象服务,另一块Survivor区域是空的,用于垃圾回收
    3.举例:
    第一次GC,将Eden+from存活的对象复制到to,清空Eden和from(存活对象小于to的空间)
    第二次GC,虚拟机使用Eden+to,将存活对象移到from
    就这样反复在from,to循环,复制过程内存不够使用则向老年代分配担保

  • 老年代(Old Generation):
    Tenured Gen
    新生代每一次GC后,都给对象"+1岁",当年龄到达一定数量则会进入老年代
    (默认15, -XX:MaxTenuringThreshold设置)
    大的对象直接进入老年代
    (默认15, -XX:PretenuringThreshold设置)
    -XX:PretenureSizeThreshold3M,那么大于3M的对象就会直接就进入老年代。

  • 永久代/持久代(Permanent Generation ):即JVM的方法区
    存放一些虚拟机加载的类信息的静态文件,更不易被回收

  • TLAB(thread local allocation buffer):分配缓冲区
    堆内存是线程共享的,堆上分配的内存需要加锁,为了提高效率,会为每个新建的线程在Eden上分配一块独立空间线程独享,在TLAB上不需要加锁,所有JVM给线程中的对象分配内存会优先使用AB分配缓冲区

如何创建对象
对象分配内存 -> 内存初始化 -> 对象初始化 -> 构造方法

  • 指针碰撞(双指针) free区 |正在分配内存|已用区域
  • 空闲列表
    线程安全:TLAB

5.方法区

  • 定义:用于存储被虚拟机加载的(类信息包括常量池),静态变量,常量,编译后的代码
    • 方法区是堆的逻辑部分,但是与堆内存分开的。
    • 类信息:类的版本,类的全限定名,字段,方法,接口
  • 执行类.class的过程
    • 在加载类(加载、(验证、准备、解析3步称为连接)、初始化)
  • 永久代:HotSpot虚拟机特有的概念,HotSpot 虚拟机使用永久代来实现方法区,
  • 版本的改动:
    1. JDK1.7的HotSpot:方法区移除了字符串常量池
    2. JDK1.8的HotSpot:取消了方法区,用元空间(Meta Space)代替
  • 元空间的大小参数
    JDK1.7及以前(初始和最大值):-XX:PermSize -XX:MaxPermSize
    JDK1.8(初始和最大值):-XX:MetaSpaceSize -XX:MaxMetaSpaceSize

6.元空间

  • 定义:元空间的本质和永久代类似,都是对JVM中方法区的实现,最大的区别是元空间使用的是本地内存
  • 为什么要用元空间代替永久代?
    1. 性能和OOM问题:字符串在永久代,容易出现性能问题和内存溢出问题
    2. 空间大小导致溢出:类及方法的信息比较难确定大小,永久代的内存大小指定困难,小-->永久代溢出(方法区太小), 大-->老年代溢出(堆内存太小)
    3. GC回收效率低

7.运行时常量池

  • 定义:方法区的一部分(.class文件)中的常量池(存放编译时的引用)会在类加载后存放到方法区的运行时常量池
  • 幼儿园化:java文件编译成class文件,常量池的字面量和符号引用只是个名字而已,还没指向对应的内存地址,类加载的时候会把这些字面量和符号引用放入运行时常量池(runtime constant pool)并指向对应的内存地址
  • 动态性:String.intern()方法调用时可以将常量放入到运行时常量池中
  • OOM:常量池无法从方法区获得内存,也会抛异常OutOfMemoryError
  • JDK6中,不推荐大量使用intern方法,因为这个版本字符串缓存在永久代中,这个空间是有限了,除了FullGC之外不会被清除,大量的缓存在这容易OutOfMemoryError。之后的版本把字符串放入了堆中,避免了永久代被挤满。
  • JDK1.7:字符串常量池从方法区移至堆内存中
  • JDK1.8:方法区的移除,运行时常量池移至元空间中(Meta Space)
  • 避免在一个系统中产生大量的String对象,引入了字符串常量池。


    image-20200713094221170.png

8.直接内存

  • 定义:直接内存并不是虚拟机运行时的一部分,这部分却频繁的使用,在JDK1.4中加入NIO(New Input/Output)类,使用Native函数可以直接分配堆外地址,存储在Java堆里面的DirectByteBuffer对象作为这块堆外内存的引用
  • 好处:在一些场景中可以显著提高性能,避免了在堆内存和堆外内存中来回拷贝数据

二、内存溢出和内存泄漏

内存溢出

内存溢出:JVM在申请内存的时候,因为没有足够的内存而引发的错误OOM

1.堆溢出

  • 原因:
    1. 堆内存过小或者内存泄漏
    2. 大对象(大数组)分配内存
    3. 频繁的调用String.intern()方法,1.7(不包括)以后字符串的常量池在堆内存中
    4. GC回收效率低,98%时间回收不到%2的空间
  • 常用参数:-Xms -Xmx(堆内存初始容量和最大容量)

    -Xms512m

2.栈溢出

  • 官方:

    如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
    如果虛拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

  • OutOfMemory(多线程):每个线程都有对应的栈帧与之对应,栈内存过大或者线程数过多都会造成OOM

    1. 栈内存过大

      每个线程分配到的栈容量大,可以建立的线程数就少,需要建立新线程就容易把剩下的内存耗尽

    2. 线程数过多

      栈内存有限,随着线程数的增加,栈内存的空间就慢慢减少

  • StackOverflowError单线程:往往发生于单线程的错误地无限递归,当线程所请求的栈最大深度大于虚拟机所允许的最大深度(-Xss),抛出StackOverflowError

    1. 单线程下的栈内存减少
    2. 新建本地变量,使得线程栈帧的局部变量表变大
  • 常用参数

    -Xss(栈容量大小,可理解为栈帧深度)

3.方法区溢出

  • 定义:方法区存储的是JVM加载的类信息,静态变量,常量,JIT编译后的代码,经常动态生成大量class的应用中,要注意会不会出现OOM
  • 出现得到情况
    1. 大量的JSP,JSP第一次访问时,JSP引擎会将JSP转换编译成Class,加入内存
    2. JDK1.7以前(不包括1.7)的HotSpot频繁的使用String.intern().那时的字符串常量池还在方法区
    3. 存在大量反射的场景,也会把class加载进内存
  • 常用参数:

    1.7及其之前,通过-XX:PermSize -XX:PermSizeMax:限制方法区的大小
    1.8:-XX:MetaSpaceSize -XX:MaxSpaceSize:元空间初始与最大容量

4.直接内存溢出

  • 定义:在JDK1.4中加入NIO(New Input/Output)类,使用Native函数可以直接分配堆外地址,存储在Java堆里面的DirectByteBuffer对象作为这块堆外内存的引用
  • 直接内存只能Full GC清理回收不像新生代和老年代,发现空间不足就通知GC回收
  • Minor GC Major GC Full GC
    从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。 Major GC 是清理永久代。Full GC 是清理整个堆空间—包括年轻代和永久代。(年轻代收集不能把对象放进永久代时,就会触发一次完全收集Major-GC)

5.内存泄漏

Memory Leak,程序在申请内存后,无法释放已申请的内存空间(一个对象不再被需要了,可是收集器却无法将它回收) , 一次内存泄漏危害很小,但是长期堆积后很严重,无论有多少内存,最终会被耗光,memory leak最终导致----> OOM

三、实操OutOfMemoryError

--------------------------------------进阶1-------------------------------------
--------------------jvm运行时中添加arguments参数-------------------
Run as -> Run Configuration -> arguments(VM arguments) -> 添加以下内容

-XX:+HeapDumpOnOutOfMemoryError -Xms20m -Xmx20m

再次运行生成java_pid8374.hprof的快照文件

---------------------------------------进阶2----------------------------------------
--------------------MAT工具分析出现OOM的原因--------------------------
将java_pid8374.hprof导入MAT中 -> 首页有圆形图分析 -> open dominator tree for entire heap(树结构查看堆结构)
shallowed heap:自身对象占用空间
retained heap:自身对象及其引用占用空间


image-20200711110902546.png

参数解释:
-XX:+HeapDumpOnOutOfMemoryError -----拍摄OOM溢出的快照+配合MAT工具查看使用
-Xms20m ------初始的Heap的大小。
-Xmx20m --------Xmx最大Heap的大小。
-Xss规定了每个线程栈的大小。一般情况下256K是足够了。影响了此进程中并发线程数大小。

--------------------------------------------------进阶3--------------------------------------------
------------------------------自带的jconsole程序(实际是lib/tools.jar)--------------------------
cmd ->(配置java环境变量的情况下) -> jconsole(出现小程序页面) -> 本地进程(sun.tools.jconsole.JConsole)

四、对象的创建和对象的内存布局

对象的创建

  • 对象创建过程

    1. new 类
    2. new 的参数在常量池中(方法区或者Meta Space)定位符号引用
    3. 没有引用,则进行类加载,链接,初始化
    4. 在堆中给对象分配内存空间
    5. 内存初始化=0(不包括对象头)
    6. 调用对象的init方法
  • 总结

    • 对象分配内存 -> 内存初始化 -> 对象初始化 -> 构造方法
    • 指针碰撞(双指针) free区 |正在分配内存|已用区域(GC器有相应的划分内存区域的功能)
    • 空闲列表
      线程安全:TLAB

对象的内存布局

image-20200724221104887.png

image-20200812155310278.png
image-20200724221141087.png
  • 对象头Header
    • 自身运行数据(Mark Word)
      • 哈希值hashcode / 偏向线程ID + epoch(偏向锁的时间戳)
      • GC分代年龄
      • 是否偏向,锁状态标志
    • 类型指针:JVM通过这个指针确定这个对象是哪个类的实例
  • 实例数据InstanceData
    • 对象真正存储的有效信息,定义各种类型的数据,包括父类继承下来的
  • 对齐填充Padding
    • 相当于占位符,HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,so当实例数据部分没有对齐时,会以对齐填充来补全(对象头部部分正好8字节的1或者2倍)
  • 对象的访问定位
    通过栈的引用去操作堆中的对象,引用访问堆中对象有两种方式
    • 句柄:Java堆中划分一块内存作为句柄池,栈的(引用reference)存放的是对象的句柄信息,句柄包含了对象实例数据和类型数据两块地址信息,
      好处:reference中存储的是稳定的句柄地址,对象在移动时(GC非常的常见)只会改变句柄的实例数据指针,reference本身不需要修改
    • 直接指针:堆对象需考虑如何放置访问类型数据的相关信息,reference中直接存储对象地址信息
      好处:速度更快,节省了一次指针定位的开销
      Hotspot 使用的是直接指针的方式访问对象。

五、JVM调参集锦

基础

  • 打印gc信息

    -verbose:gc -XX:+PrintGCDetails

  • xms xmx xss xmn

    1. -Xms 为jvm启动时分配的内存,比如-Xms200m,表示分配200M,默认为物理内存的1/64
    2. -Xmx 为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存,默认为物理内存的1/4
    3. -Xss (虚拟机栈)为jvm启动的设置每个线程的堆栈大小,默认JDK1.4中是256K,JDK1.5+中是1M
      在相同物理内存下,减小这个值能生成更多的线程。
    4. -Xmn4g:设置年轻代大小为4G.
      -Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn
  • 非堆内存分配:

    -XX:NewRatio=4
    设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5。
    -XX:SurvivorRatio=4
    设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6。(4:1:1)。
    -XX:MaxPermSize=16m 设置持久代大小为16m。
    -XX:MaxTenuringThreshold=0
    设置垃圾最大年龄。大于这个年龄的都会进入老年代区。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

进阶

  • 修改收集器

    串行收集器DefNew:是使用-XX:+UseSerialGC(新生代,老年代都使用串行回收收集器)
    并行收集器ParNew:是使用-XX:+UseParNewGC(新生代使用并行收集器,老年代使用串行收集器)
    并行收集器PSYoungGen:是使用-XX:+UseParallelOldGC(新生代,老年代都使用并行回收收集器)或者-XX:+UseParallelGC(新生代使用并行回收收集器,老年代使用串行收集器)
    并发收集器CMS: -XX:+UseConcMarkSweepGC(新生代使用并行收集器,老年代使用CMS)。
    并发收集器garbage-first heap:是使用-XX:+UseG1GC(G1收集器)

  • 大对象直接进入老年代

    why?如果放在年轻代的Eden,GC频繁,采用复制算法导致经常移动大对象,这会消耗资源
    -XX:PretenureSizeThreshold=6M 超过6M直接分配到进老年代

  • 存活年龄多大的对象进入老年代

    -XX:MaxTenuringThreshold 15 设置的是年龄阈值,默认15(对象被复制的次数)

  • 允许分配担保:Eden区域从老年代借空间(相当于贷款)
    JVM继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则正常进行一次YGC,尽管有风险(因为判断的是平均大小,有可能这次的晋升对象比平均值大很多);
    如果小于,或者HandlePromotionFailure设置不允许空间分配担保,这时要进行一次FGC。

    -XX:+HandlePromotionFailure 允许分配担保

你可能感兴趣的:(第二章 内存区域和 OOM内存溢出)