JVM
内存泄漏与内存溢出
什么是内存泄漏和内存溢出?
- 内存溢出(Out Of Memory):是指程序在运行过程中,申请的内存超过了系统或进程所能提供的最大内存限制,导致程序无法继续运行,最终崩溃或被强制终止
- 原因:
- 内存泄漏:未释放不再使用的内存,导致内存逐渐耗尽
- 大数据处理:一次性加载或处理的数据量过大,超过了可用内存
- 递归过深:递归调用未正确终止,导致栈空间耗尽
- 资源未释放:如文件,网络连接等未及时关闭,占用大量内存
- 配置不当:如JVM堆内存设置过小,无法满足程序需求
- 原因:
- 内存泄漏:程序在运行过程中,动态分配的内存未能正确释放,导致这部分内存无法被再次使用,最终可能导致内存耗尽
- 原因:
- 未释放对象引用:如集合类中存储的对象未及时清理
- 静态集合类:静态集合类会一直持有对象引用,导致对象无法被回收
- 未关闭资源:如数据库连接,文件流等未显式关闭
- 监听器未注销:注册的事件监听器未及时注销,导致对象无法被回收
- 内部类持有外部类引用:非静态内部类隐式持有外部类的引用,可能导致外部类无法被回收
- 原因:
内存泄漏与内存溢出的关系
- 内存泄漏的堆积最终会导致内存溢出
- 内存溢出就是你要的内存空间超出了系统实际分配给你的内存空间
- 内存泄漏就是指你向系统申请内存进行使用,使用完却不归还,结果这块内存你自己也不能再访问,系统也不能把它分配给别的程序
Java中常见的OOM
- Java堆溢出
- 由于不断常见对象实例,当对象数量达到了堆的最大容量限制后产生内存溢出异常
- java.lang.OutOfMemoryError: Java heap space
- 虚拟机栈溢出
- 在单线程情况下,虚拟机栈容量太小或者定义了大量的本地变量
- java.lang.StackOverflowError
- 本地方法栈溢出
- 在多线程下,大量创建新线程,会抛出OOM,每个线程的栈分配的空间越大,越容易产生
- java.lang.OutOfMemoryError:unable to create new native thread
- 运行时常量池溢出
- 代码在运行时创建了大量的常量,超出了常量池的上限
- java.lang.OutOfMemoryError:PermGen space at java.lang.String.intern(Native Method)
- 方法区溢出
- 在运行时,ClassLoader动态加载了大量的Class信息,超出了方法区的上限
- java.lang.OutOfMemoryError:PermGen space at java.lang.ClassLoader.defineClass(Native Method)
JVM内存结构有哪几种内存溢出的情况
- 堆内存溢出:当出现Java.lang.OutOfMemoryError:Java heap space异常时,就是堆内存溢出了。原因是代码中可能存在大对象分配,或者发生了内存泄漏,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象
- 栈溢出:如果我们写一段程序不断地进行递归调用,而且没有退出条件,就会导致不断的进行压栈。类似这种情况,JVM实际会抛出StackOverFlowError;当然,如果JVM试图去扩展栈空间的时候失败,则会抛出OutOfMemoryError
- 元空间溢出:元空间溢出会抛出Java.lang.OutOfMemoryError:Metaspace。出现这个异常的问题原因是系统的代码非常多或引用的第三方包非常多或者动态代码生成类加载等方法,导致元空间的内存占用很大
- 直接内存内存溢出:在使用ByteBuffer中的allocateDirect()的时候会遇到。
JMM(Java Memory Model)
Java的内存模型
根据JDK8规范,JVM运行时内存共分为:虚拟机栈,堆,元空间,程序计数器,本地方法栈五个部分.还有一部分内存叫直接内存,属于操纵系统的本地内存,也是可以直接操作的
JVM内存结构主要有以下几个部分:
- 程序计数器:可以看作是当前线程所执行的字节码的行号指示器,用于存储当前线程正在执行的Java方法的JVM指令地址。每个线程都有一个独立的程序计数器,属于线程私有.如果程序执行的是本地方法,计数器值为null。是唯一一个在Java虚拟机规范中没有规定任何
OutOfMemoryError
情况的区域,生命周期与线程相同 - Java虚拟机栈:每个线程都有自己独立的Java虚拟机栈,生命周期与线程相同.每个方法在执行时都会创建一个栈帧,用于存储局部变量,操作数栈,动态链接,方法出口信息等.可能会抛出StackOverflowError和OutOfMemoryError异常
- 本地方法栈:与Java虚拟机栈类似,主要为虚拟机使用到的本地方法服务,在HotSpot虚拟机中和Java虚拟机栈合二为一.本地方法执行时也会创建栈帧,同样可能出现StackOverflowError和OutOfMemoryError两种错误
- Java堆:是JVM中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,用于存放对象实例.从内存回收角度,堆被划分为新生代和老年代,新生代又分为Eden区和两个Survivor区(From Survivor和To Survivor).如果在堆中没有内存完成实例分配,并且堆也无法扩展时会抛出OutOfMemoryError异常
- 方法区(元空间):在JDK1.8及以后版本中,方法区被元空间取代,使用本地内存.用于存储已被虚拟机加载的类信息,常量,静态变量等数据.虽然方法区被描述为堆的逻辑部分,但有"非堆"的别名.方法区可以选择不实现垃圾收集,内存不足时会抛出OutOfMemoryError异常
- 运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,具有动态性,运行时也可以将新的常量放入池中.当无法申请到足够内存时,会抛出OutOfMemoryError异常
- 直接内存:不属于JVM运行时数据区的一部分,通过NIO类引入,是一种堆外内存,可以显著提高I/O性能.直接内存的使用受到本机总内存的限制,若分配不当,可能导致OutOfMemoryError异常
JVM内存模型里的堆和栈有什么区别
- 用途:栈主要用于存储局部变量,方法调用的参数,方法返回地址以及一些临时数据.每当一个方法被调用,一个栈帧(stack frame)就会在栈中创建,用于存储该方法的信息,当方法执行完毕,栈帧也会被移除.堆用于存储对象的实例(包括类的实例和数组).当你使用
new
关键字创建一个对象时,对象的实例就会在堆上分配空间 - 生命周期:栈中的数据具有确定的生命周期,当一个方法调用结束,其对应的栈帧就会被销毁,栈中存储的局部变量也会随之消失.堆中的对象生命周期不确定,对象会在垃圾回收机制(GC)检测到对象不再被引用时才被回收
- 存取速度:栈的存取速度通常比堆快,因为栈遵循先进后出(LIFO)的原则,操作简单快速.堆的存取速度相对较慢,因为对象在堆上的分配和回收需要更多的时间,而且垃圾回收机制的运行也会影响性能
- 存储空间:栈的空间相对较小,且固定,由操作系统管理.当栈溢出时,通常是因为递归过深或局部变量过大.堆的空间较大,动态扩展,由JVM管理.堆溢出通常是由于创建了太多的大对象或未能及时回收不再使用的对象
- 可见性:栈中的数据对线程是私有的,每个线程有自己的栈空间.堆中的数据对线程是共享的,所有线程都可以访问堆上的对象
内存区域分析
线程私有的内存区域
- 程序计数器:一块比较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器
- JVM虚拟机栈:每个方法执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息.每一个方法从调用到执行完成的过程,就对应一个栈帧在虚拟机中入栈和出栈的过程
- 本地方法栈:本地方法栈和JVM虚拟机栈的作用完全一样,它们俩的区别就是本地方法栈为虚拟机使用的Native方法服务,而虚拟机栈为JVM执行的Java方法服务
线程共享的内存区域
- 堆:在JVM启动时创建,所有的对象实例以及数组都要往堆上分配;如果堆上没有足够的内存完成实例分配且堆也无法扩展时,会抛出OOM
- 方法区/元数据区:用于存储已经被加载过的类信息,常量,静态变量,即时编译器编译后的代码等数据
- 直接内存:在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道和缓冲区的IO方式,它可以使用Native函数库直接分配对外内存,然后通过一个存储在Java堆中的DirectBuffer对象作为这块内存的引用进行操作
- 字符串常量池:用于存储字符串对象,或者字符串对象的引用
- 运行时常量池:编译器或者运行期间产生的常量被放在运行时常量池中,这里的常量包括:基本类型,包装类和String
- 类常量池:类常量池中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
方法区中的方法的执行过程
当程序中通过对象或类直接调用某个方法时,主要包括以下几个步骤:
- 解析方法调用:JVM会根据方法的符号引用找到实际的方法地址(如果之前没有解析过的话)
- 栈帧创建:在调用一个方法前,JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息
- 执行方法:执行方法内的字节码指令,涉及的操作可能包括局部变量的读写,操作数栈的操作,跳转控制,对象创建,方法调用等
- 返回处理:方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境
引用类型有哪些?有什么区别?
引用类型主要分为强软弱虚四种:
- 强引用指的是代码中普遍存在的赋值方式,如:A a = new A()这种,强引用关联的对象,永远不会被GC回收
- 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收
- 弱引用可以用WeakReference描述,它的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够
- 虚引用也被称为幻影引用,是最弱的引用关系,可以用PhantomReference描述,它必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存
弱引用的使用场景
弱引用通过Java.lang.ref.WeakReference类实现。弱引用的一个主要用途是创建非强制性的对象引用,从而避免内存泄漏
- 缓存系统
- 对象池
- 避免内存泄漏