彻底搞懂JVM中的OOM,体系结构,内存结构

参考文章:
https://www.cnblogs.com/zongheng14/p/12041005.html

JVM的体系结构

在这里插入图片描述

1. 类装载器ClassLoader ** (4个知识点,①概念,②种类(3种系统自带,1种自定义),③双亲委派机制,④沙箱安全机制)

1.1 类装载器的概念

负责加载class文件,class文件 在文件开头有特定的文件标识 ,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

1.2 类装载器的种类

1.2.1 虚拟机自带的加载器

① 启动类加载器 也叫根加载器 (Bootstrap) ,由C++编写 ,程序中自带的类,存储在$JAVAHOME/jre/lib/rt.jar中,如object类等
② 扩展类加载器 (Extension) ,Java 编写 ,在我们平时看到的类路径中,凡是以javax 开头的,都是拓展包,存储在$JAVAHOME/jre/lib/ext/*.jar 中
③ 应用程序类加载器 (AppClassLoader),即平时程序中自定义的类 new出来的 Java也叫系统类加载器,加载当前应用的classpath的所有的类

1.2.2 用户自定义加载器

Java.lang.ClassLoader的子类,用户可以定制类的加载方式,即如果你的程序有特殊的需求,你也可以自定义你的类加载器的加载方式
进入ClassLoader的源码,其为抽象类,
因此在你定制化开发的时候,需要你定义自己的加载器类来继承ClassLoader抽象类即可,即 MyClassLoader extends ClassLoader

在这里插入图片描述
所以,Java 的类的加载机制,永远是从 启动类加载器 --> 拓展类加载器 --> 应用程序类加载器 这样的一个顺序进行加载

1.3 类装载器的双亲委派机制

  先举一个栗子,来说明下啥叫双亲委派,比如 有一个类叫 A.java  ,当要使用A类时,类加载器要先去 启动类加载器(Bootstrap)中去找,如果找到就使用启动类加载器中的A类,不继续往下执行,
  但是如果找不到,则依次下放,去 拓展类加载器 中找,同理找到就用,找不到就继续下放,再去 应用程序类加载器中找,找到就用,此时找不到就会报classNotFund Exception的异常。 

概念:
    当一个类收到类加载请求,它首先不会尝试自己去加载这个类,而是先把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的类加载请求都是应该传到启动类加载器中的,只有
    当其父类加载器自己无法完成这个请求的时候(在他的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

    采用双亲委派的一个好处就是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是会委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终
    得到的都是同一个Object对象。

1.4 类装载器沙箱安全机制

   通过双亲委派机制,类的加载永远都是从 启动类加载器开始,依次下放,保证你所写的代码,不会污染Java自带的源代码,所以出现了双亲委派机制,保证了沙箱安全

Execution Engine 执行引擎负责解释命令,提交操作系统执行。

2. Native Interface 本地接口

native :在Java中是一个关键字,有声明,无实现。

以线程为例,不要以为线程是属于Java的一个东西,其实它是属于操作系统底层的,Java中通过Thread类的start() 类启动一个线程,

在这里插入图片描述
进入Thread的start()的源码,你会看到虽然调用的是start(),但其实调用的start0()这个方法, 最终是由 private native void start0();
这段代码去跟底层做了交互实现,有声明,无实现,Java到此交由系统去处理了。

在这里插入图片描述

运行时数据区在下面有讲解

什么是OOM,OOM为什么会发生,如何解决?

概念

OOM,全称“Out Of Memory”,意思是“内存用完了”

来源于 java.lang.OutOfMemoryError

这是个特别严重的问题,因为这个问题已经 严重到应用程序自己无法处理了。

原因

官方的文档称,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出 java.lang.OutOfMemoryError: ···

具体原因一般有这两个:

自身原因

分配的内存少了,比如JVM虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。

-XX:+HeapDumpOnOutOfMemoryError -Xms20m -Xmx20m -XX:HeapDumpPath=D:\oomTemp
``-XX:+HeapDumpOnOutOfMemoryError`:表示 导出内存溢出的堆信息(hprof文件)
-Xms:表示 设定程序启动时占用内存大小
-Xmx:表示 程序运行期间最大可占用的内存大小
-XX:HeapDumpPath=:表示 生成得快照路径

外部原因

内存被应用程序使用的太多,而且用完后没有释放,浪费了内存。这种情况下会造成内存泄漏or内存溢出

内存泄漏:应用进程申请并使用完的内存,没有被释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为当前的申请者不用了,但又不能被JVM虚拟机分配给其他申请者。
内存溢出:申请的内存超出了JVM虚拟机能提供的内存大小。

Java虚拟机所管理的内存包括以下 7个 运行时数据区域:

程序计数器 (Program Counter Register)

  • 一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器

  • 线程私有的内存
    如果正在执行的是本地(native)方法,这个计数器值则应该为空(Undefined)

  • 值得注意的是:《Java虚拟机规范》中,唯一一个没有规定任何OutOfMemoryError情况的区域!!!

Java虚拟机栈 (VM Stack)

虚拟机描述的是java方法执行的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧用于存储局部变量表,操作数栈,动态连接,方法出口等信息

  • Java方法执行的线程内存模型
  • 为虚拟机执行Java方法(也就是字节码)服务
  • 线程私有的内存
  • 其生命周期与线程相同
  • 每个Java方法的执行对应着一个栈帧的进栈和出栈的操作
  • 两类异常:
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • 如果JVM栈容量可以动态扩展,当栈扩展时无法申请到足够的内存时,会抛出OutOfMemoryError异常,周志明老师书中写道:hotspot虚拟机栈容量不能动态扩展,所以不会有OOM

本地方法栈 (Native Method Stacks)

  • 区别于 “Java虚拟机栈” :本地方法栈只为虚拟机使用到的本地(Native)方法服务,为其运行提供内存环境
  • 同 “Java虚拟机栈” 一样,本地方法栈也有两类异常:
  • 栈深度溢出时,将抛出StackOverflowError异常
  • 栈扩展失败时,会抛出OutOfMemoryError异常
    周志明老师书中写道:hotspot虚拟机栈容量不能动态扩展,所以不会有OOM

Java堆 (Java Heap):有OOM

  • 虚拟机所管理的内存中最大的一块
  • Java堆是被所有线程共享的一块内存区域
  • 唯一的目的:存放对象示例。
    jdk1.8将方法区中的字符串常量池移到堆中
  • Java中 “几乎” 所有的对象实例都在这里分配内存;
  • 但是,由于现在技术发展,说 “Java对象示例都分配在堆上” 也渐渐变得不是那么绝对了。
  • Java堆是垃圾收集器管理的内存区域,也称“GC堆”
  • Java堆可以处于物理上不连续的内存空间,但在逻辑上它应该是被视为连续的。
  • 如果在Java堆中没有内存完成实例分配,并且Java堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常

方法区(Method Area)

  • 和 “Java堆” 一样,是被所有线程共享的一块区域。
    用于存储已被虚拟机加载类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。

在jdk6之前用的是永久代来实现方法区,因为永久代容易使得内存溢出
jdk6放弃永久代,改为采用本地内存来实现方法区
jdk7的hotspot,把原本放在永久代的字符串常量池,静态变量等移出
到了jdk8,用元空间来实现方法区,将类型信息都移到元空间中

  • 在《Java虚拟机规范》中,把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作 “非堆” ,目的是与Java堆区分开来。
  • 1.7以前的jdk采用的是永久代作为方法区的实现,在1.7及以前的jdk版本,永久代的空间不足也会导致fullGC,
    1.7以前,永久代空间如果设小了,就会触发整个堆的一次full
    GC(注意是触发堆的full GC),经过这样的一次定位就初步定位到了是由于永久代空间不足导致了堆的full GC

  • 元空间不会发生垃圾回收
    1.8以后由于改成了元空间,它的垃圾回收就不是由java来控制了,元空间的默认情况下内存空间是使用的操作系统的内存空间,所以空间的容量是比较充裕的,不会发生元空间的空间不足问题。

根据《java虚拟机规范》规定,如果方法区无法满足新的内存分配需求时,将抛出OOM

运行时常量池 (Running Constant Pool)

类加载后,class文件中的字面量与符号引用存放于此,由符号引用翻译出来的直接引用也存储在此
具备动态性,运行期间也可以将新的常量放入该池中,也就是String的intern()方法

  • 运行时常量池是方法区的一部分
  • 当常量池无法再申请到内存时,会抛出OutOfMemoryError异常

直接内存 (Direct Memory)

  • 既不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
  • 但是这部分内存区域也被频繁地使用,而且也可能导致OutOfMemoryError异常出现
  1. 在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库 直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
  2. 在本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

OOM的error类型

1. java.lang.OutOfMemoryError: Java heap space
  • Java堆 内存溢出,是最常见的一种情况。

原因:

  • 一般由于内存泄露或者堆的大小设置不当引起。

解决:

  • 对于内存泄露:需要通过内存监控软件,查找程序中的泄露代码,

  • 对于堆大小,可以通过虚拟机VM参数进行修改:

  • -Xms1024M -Xmx2048M

2.java.lang.OutOfMemoryError: PermGen space
  • 方法区溢出
  • PermGen space的全称是Permanent Generation space(指内存的永久保存区域)。

原因:

  • 加载了大量的Class(类)
  • 在单一的Tomcat实例下运行多个Web应用程序(大量色jsp页面)
  • 在运行的Tomcat实例中反复“热部署”Web应用程序
  • 采用cglib等反射机制
  • 过多的常量也会导致方法区溢出,尤其是字符串

解决:

  • 修改方法区的大小(缺省默认为64M):
  • -XX:PermSize=128M -XX:MaxPermSize=256M
3.java.lang.StackOverflowError
  • 不会抛出OOM Error,但是也是比较常见的Java内存溢出情况。
  • Java虚拟机栈or本地方法栈,在栈深度溢出(线程请求的栈深度大于虚拟机所允许的深度),将抛出StackOverflowError异常

原因:

  • 最常见的:无限递归循环调用(死循环)
  • 栈深度溢出
  • 执行了大量方法,导致线程栈空间耗尽
  • 方法内声明了大量的局部变量

解决:

  • 通过程序抛出的异常堆栈,利用内存监控软件,查找程序中执行死循环的代码
  • 排查是否存在类之间的循环依赖
  • 设置JVM启动参数 -Xss ,增加线程栈内存空间
  • 线程栈的默认大小依赖于操作系统、JVM 版本和供应商
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值