JVM内存结构:
- 程序计数器:
- 存储线程执行程序的指令地址,从而记录线程当前所执行的程序位置
- CPU多线程切换执行时,为CPU提供某一线程的当前执行的程序位置
- 每个线程独有一份,生命周期与线程相同
- 不会发生OOM
- 虚拟机栈:
- 每个线程都具有自己的虚拟机栈,当线程执行方法时会创建一个属于自己的栈帧
- 存放线程执行方法时的变量、对象的引用、临时工作区[执行方法的操作]、动态链接[方法内调用其他方法]、方法出口[return语句]
- 栈溢出:比如死循环递归。造成java.lang.StackOverflowError
- 生命周期与线程相同
- 本地栈:
- 主要服务于Native关键字,Native不由JVM管理。
- 主要用于Java语言无法实现的功能,Native可以让C或/C++语言来执行
- 此时PC的指向会是undenfy
- 堆:
- 所有线程共享,用于存放对象实例
- 从内存回收角度来看,堆分为新生代和老生代。
- 新生代:是临时工区,只是用于临时对象创建。
- 当新生代内存满了就会触发GC回收,因为内存不大所以回收的蛮频繁的
- 老年代:如果一个对象在新生代被来回调用多次,将会移入老年代。
- 大对象一般会直接被分配到老年代中
- 常量池:(Java7之前在方法区,而后常量池搬到了堆中,因为方法区的内存太小)
- 字符串常量池:当程序执行到String s=“abc”时,会去字符串常量池寻找abc。如果找到了就将s指向abc。如果没找到就创建一个abc在字符串常量池并将s指向它。
- 此时会将abc放进字符串常量池,而s会存放在栈中成为一个引用
- 元空间(方法区是统称,Java8之前叫永久代,之后叫元空间):
- 所有线程共享,存放类的各种信息
- 当类被加载时,会将class文件中的信息加载进入元空间
- 运行时常量池:类被加载进元空间后,类中的各种常量会进入运行时常量池中
- 为实例的创建提供类的信息
内存泄漏和内存溢出:
内存泄漏是病因,指对象不被及时回收而导致内存越用越少。
内存溢出是结果,是真不够用了。两者的最终结果就是OutOfMemory
- 内存泄漏:
- 常见原因:
- 使用静态合集进行存储数据而不清理
- 使用监听器但未注销
- ThreadLocal未move
- 线程池未关闭或者任务队列爆满
- 常见原因:
- 内存溢出:
- 常见原因:
- 对象创建数量过多,堆内存溢出
- 在方法内深度递归导致虚拟机栈溢出
- 常见原因:
OOM的几种经典情况:
- 堆内存溢出:可能是对象分配过多或者强引用导致的内存泄漏,当堆内存无法容纳下新对象后就会报OOM
- 栈内存溢出:方法内多次进行递归调用,不断压入栈帧导致栈溢出
- 元空间溢出:系统的代码特别是类中引用了过多的外部包,或者动态加载类过多导致方法区内存溢出
- 堆内存溢出应对方法:
- 开启JVM堆内存快照:通过指令开启堆内存快照功能,并使用工具来分析快照,查看是哪些对象占用过大空间
- 查看有没有过长的静态集合和长生命周期引用持有短生命周期对象而导致内存泄漏,梳理对象引用链。
- 举例:批量处理任务时,代码把所有任务都存到一个List中,导致List内存过大。后续优化为批量从List中拿到任务并写入数据库,同时清空List内容。
- 栈内存溢出应对方法:
- 排查递归逻辑:检查是否有层级过深的递归调用,杜绝无限递归
- 拆分大型方法:将一个复杂的方法拆分成数个独立的小方法
- 优化栈帧:减少局部变量的数量,避免在栈中创建大对象,尽量通过new关键字将大对象创建在堆中
垃圾回收:
判断垃圾的方式:
- 引用计数法:JVM为每个对象分配一个引用计数器,每当有一个方法引用此对象时,计数器+1,反之-1。当计数器为0时,此对象可以被GC回收
- 当两个对象相互引用,会导致计数器无法为0,无法被GC回收
- 可达性分析算法:从一组成为GC Roots(垃圾收集根)的对象开始,向下追溯他们引用的对象而形成引用链。如果一个对象无法通过引用链到GC Root,则此对象被认为不可达,将会被回收。
垃圾回收算法:
- 分代回收策略:
- 将内存分为新生代和老年代,当GC回收时,存活的对象的age+1,默认十五岁时转入老年代
- 因为新生代空间有限,大对象一般会直接被分配进老年代
- 标记-清除算法:(作用于老年代)
- 分为标记和清除两个阶段,首先通过可达性分析标记出所有需要清除的对象,然后统一回收
- 只标记和清除对象,做事干练,速度快
- 清除后会有大量碎片内存空间,当一个较大的对象放不下去则又会爆OOM
- 标记-整理算法:(作用于老年代)
- 基于标记-清除算法的基础上,将标记的对象移动到安全区,将其他区域直接清除,无内存碎片
- 基于复制算法和分代回收算法之间。前者需要复制大量对象可用且内存空间小,后者老年代对象难以回收
- 但是移动对象会消耗性能。
- 标记-复制算法:(作用于新生代)
- 将内存一分为二,只使用其中一半内存空间。通过可达性分析算法标识需要回收的对象,并将存活对象赋值到另一半内存中,之后直接清除需要回收的那一半内存
- 当对象数量很多的时候,复制操作十分消耗性能
- 可用内存空间直接砍半
- 选择算法原因:
- 新生代对象小,复制开销小
- 绝大部分对象都需要被回收,不如直接筛选复制存活对象
Stop The World!!!:以G1垃圾回收器为例(G1是默认的JVM回收器)
GC过程中,暂停所有业务进程。为了保证GC过程中对象的引用不变化
- 标记阶段:
- 初始标记阶段:从GC Roots根节点开始遍历直接子节点,过程触发STW,通常耗时极短
- 并发标记阶段:从直接子节点出发,通过引用链遍历堆中的所有对象,此阶段与线程并发,不触发CTW,过程慢
- 再标记阶段:触发CTW,遍历在并发标记阶段由于线程允许而变化的对象,抓住漏网之鱼
- 清理阶段:
- 注意,此阶段并不对垃圾直接进行清理。而是清点没有存活对象的分区,全程CTW
- 复制阶段:
- 对标记的分区进行存活对象的转移,此过程CTW
MinorGC、MajorGC、FullGC:
- MinorGC:
- 作用于年轻代,包括新生区和两个幸存区
- 当新生区内存不足时,会触发MinorGC,并将存活对象搬入幸存区或老年代。
- 十分频繁,因为年轻代中的对象生命周期一般都很短
- MajorGC:
- 作用于老年代
- 当老年代内存不足时,会触发MajorGC
- 频率较低,而且老资历也有点回收不动
- FullGC:
- 对整个堆(年轻代+老年代)进行统一的GC回收,通常还会波及元空间
- 当老年代可能会出现内存溢出时触发
- 当元空间内存不足时触发
- 最昂贵的操作,会直接对所有线程Stop The World!!!!!!!!!并且遍历整个堆中的对象来尝试进行回收



