分为 4 个方面来介绍内存分配与回收, 分别是内存是如何分配的哪些内存需要回收在什么情况下执行回收如何监控和优化 GC 机制
java GC(Garbage Collction)垃圾回收机制, 是 java 与 C/C++ 的主要区别之一通过对 jvm 中内存进行标记, 自主回收一些无用的内存目前使用的最多的是 sun 公司 jdk 中的 HotSpot, 所以本文也以该 jvm 作为介绍的根本
1.Java 内存区域
在 java 运行时的数据取里, 由 jvm 管理的内存区域分为多个部分:
程序计数器 (program counter register): 程序计数器是一个比较校的内存单元, 用来表示当前程序运行哪里的一个指示器由于每个线程都由自己的执行顺序, 所以程序计数器是线程私有的, 每个线程都要由一个自己的程序计数器来指示自己(线程) 下一步要执行哪条指令
如果程序执行的是一个 java 方法, 那么计数器记录的是正在执行的虚拟机字节码指令地址; 如果正在执行的是一个本地方法 (native 方法), 那么计数器的值为 Undefined 由于程序计数器记录的只是当前指令地址, 所以不存在内存泄漏的情况, 也是 jvm 内存区域中唯一一个没有 OOME(out of memory error) 定义的区域
虚拟机栈 (JVM stack): 当线程的每个方法在执行的时候都会创建一个栈帧(Stack Frame) 用来存储方法中的局部变量方法出口等, 同时会将这个栈帧放入 JVM 栈中, 方法调用完成时, 这个栈帧出栈每个线程都要一个自己的虚拟机栈来保存自己的方法调用时候的数据, 因此虚拟机栈也是线程私有的
虚拟机栈中定义了两种异常, 如果线程调用的栈深度大于虚拟机允许的最大深度, 抛出 StackOverFlowError, 不过虚拟机基本上都允许动态扩展虚拟机栈的大小这样的话线程可以一直申请栈, 直到内存不足的时候, 会抛出 OOME(out of memory error)内存溢出
本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈类似, 只是本地方法栈存放的栈帧是在 native 方法调用的时候产生的有的虚拟机中会将本地方法栈和虚拟栈放在一起, 因此本地方法栈也是线程私有的
堆(Heap): 堆是 java GC 机制中最重要的区域堆是为了放置对象的实例, 对象都是在堆区上分配内存的, 堆在逻辑上连续, 在物理上不一定连续所有的线程共用一个堆, 堆的大小是可扩展的, 如果在执行 GC 之后, 仍没有足够的内存可以分配且堆大小不可再扩展, 将会抛出 OOME
方法区(Method Area): 又叫静态区, 用于存储类的信息常量池等, 逻辑上是堆的一部分, 是各个线程共享的区域, 为了与堆区分, 又叫非堆在永久代还存在时, 方法区被用作永久代方法区可以选择是否开启垃圾回收 jvm 内存不足时会抛出 OOME
直接内存 (Direct Memory): 直接内存指的是非 jvm 管理的内存, 是机器剩余的内存用基于通道(Channel) 和缓冲区 (Buffer) 的方式来进行内存分配, 用存储在 JVM 中的 DirectByteBuffer 来引用, 当机器本身内存不足时, 也会抛出 OOME
举例说明: Object obj = new Object();
obj 表示一个本地引用, 存储在 jvm 栈的本地变量表中, new Object()作为一个对象放在堆中, Object 类的类型信息 (接口, 方法, 对象类型等) 放在堆中, 而这些类型信息的地址放在方法区中
这里需要知道如何通过引用访问到具体对象, 也就是通过 obj 引用如何找到 new 出来的这个 Object()对象, 主要有两种方法, 通过句柄和通过直接指针访问
通过句柄:
在 java 堆中会专门有一块区域被划分为句柄池, 一个引用的背后是一个对象实例数据 (java 堆中) 的指针和对象类型信息 (方法区中) 的指针, 而这两个指针都是在 java 堆上的这种方法是优势是较为稳定, 但是速度不是很快
通过直接指针:
一个引用背后是一个对象的实例数据, 这个实例数据里面包含了到对象类型信息的指针这种方式的优势是速度快, 在 HotSpot 中用的就是这种方式
2. 内存是如何分配和回收的
内存分配主要是在堆上的分配, 如前面 new 出来的对象, 放在堆上, 但是现代技术也支持在栈上分配, 较为少见, 本文不考虑分配内存与回收内存的标准是八个字: 分代分配, 分代回收那么这个代是什么呢?
jvm 中将对象根据存活的时间划分为三代: 年轻代 (Young Generation) 年老代 (Old Generation) 和永久代 (Permannent Generation) 在 jdk1.8 中已经不再使用永久代, 因此这里不再介绍
年轻代: 又叫新生代, 所有新生成的对象都是先放在年轻代年轻代分三个区, 一个 Eden 区, 两个 Survivor 区, 一个叫 From, 一个叫 To(这个名字是动态变化的)当 Eden 中满时, 执行 Minor GC 将消亡的对象清理掉, 仍存活的对象将被复制到 Survivor 中的 From 区, 清空 Eden 当这个 From 区满的时候, 仍存活的对象将被复制到 To 区, 清空 From 区, 并且原 From 区变为 To 区, 原 To 区变为 From 区, 这样的目的是保证 To 区一直为空当 From 区满无对象可清理或者 From-To 区交换的次数超过设定 (HotSpot 默认为 15, 通过 - XX:MaxTenuringThreashold 控制) 的时候, 仍存活的对象进入老年代年轻代中 Eden 和 Servivor 的比例通过 - XX:SerivorRation 参数来配置, 默认为 8, 也就时说 Eden:From:To=8:1:1 年轻代的回收方式叫做 Minor GC, 又叫停止 - 复制清理法这种方法在回收的时候, 需要暂停其他所有线程的执行, 导致效率很低, 现在虽然有优化, 但是仅仅是将停止的时间变短, 并没有彻底取消这个停止
年老代: 年老代的空间较大, 当年老代内存不足时, 将执行 Major GC 也叫 Full GC 如果对象比较大, 可能会直接分配到老年代上而不经过年轻代用 - XX:pertenureSizeThreashold 来设定这个值, 大于这个的对象会直接分配到老年代上
3. 垃圾收集器
在 GC 机制中, 起作用的是垃圾收集器 HotSpot1.6 中使用的垃圾收集器如下(有连线表示有联系):
Serial 收集器: 新生代 (年轻代) 收集器, 使用停止 - 复制算法, 使用一个线程进行 GC, 其他工作线程暂停
ParNew 收起: 新生代收集器, 使用停止 - 复制算法, Serial 收集器的多线程版, 用多个线程进行 GC, 其他工作线程暂停, 关注缩短垃圾收集时间
Parallel Scavenge 收集器: 新生代收集器, 使用停止 - 复制算法, 关注 CPU 吞吐量, 即运行用户代码的时间 / 总时间
Serial Old 收集器: 年老代收集器, 单线程收集器, 使用标记 - 整理算法(整理的方法包括 sweep 清理和 compact 压缩, 标记 - 清理是先标记需要回收的对象, 在标记完成后统一清楚标记的对象, 这样清理之后空闲的内存是不连续的; 标记 - 压缩是先标记需要回收的对象, 把存活的对象都向一端移动, 然后直接清理掉端边界以外的内存, 这样清理之后空闲的内存是连续的)
Parallel Old 收集器: 老年代收集器, 多线程收集器, 使用标记 - 整理算法(整理的方法包括 summary 汇总和 compact 压缩, 标记 - 压缩与 Serial Old 一样, 标记 - 汇总是将幸存的对象复制到预先准备好的区域, 再清理之前的对象)
CMS(Concurrent Mark Sweep)收集器: 老年老代收集器, 多线程收集器, 关注最短回收时间停顿, 使用标记 - 清除算法, 用户线程可以和 GC 线程同时工作
G1 收集器: JDK1.7 中发布, 使用较少, 不作介绍
Java GC 是一个非常复杂的机制, 想要详细说清楚他需要很多时间, 如有错误恳请指正
来源: https://www.cnblogs.com/GoForMyDream/p/8693605.html