引言
Java的内存动态分配和垃圾收集的问题,都交给了JVM来处理。注意,将JVM运行数据区(虚拟机栈【栈帧】,程序计数器,堆内存)粗略的分为栈和堆(所有线程共享),回收的是堆中的对象实例。不是栈中的引用类型。
那么JVM是如何处理的?
从三个问题来分析:
1. 哪些内存需要回收?
2. 什么时候进行回收?
3. 如何回收?
注:现代收集器基本采用分代收集算法,堆分为:新生代和老年代。
1. 哪些内存需要回收?什么时候回收?
1.1 了解下对象的创建:
- 通过new 关键字。
- JVM遇到new指令,检查是否能在常量池中定位到一个类的符号引用。
- 检查是否已被加载,解析,初始化过。
- 没有,则执行相应的类加载过程。
- 类加载检查通过后,为新生对象分配内存。(类加载后确定对象所需内存大小)
- 从Java堆中划分出一块确定的内存。
1.2 若为“死亡”的对象,则需要回收,如何判断对象是否存活?
1.2.1 引用计数算法
- 给对象添加一个引用计数器
- 每当有一个地方引用它时,计数器指加1
- 当引用失效时,计数器指减1
- 任何时刻计数器为0的对象就是需要回收的
Java虚拟机没有采用这种方法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题。(python中采用)
1.2.2 可达性分析算法
- 用一个“GC Roots”的对象(指一系列中的其中一个并非某一种)作为起始点
- 从该节点向下搜索,搜索走过的路径称为引用链
- 若一个对象没有与任何引用链相连,即不可达
- 证明该对象是不可用,死亡
Java,C#采用此方法。
1.2.3 哪些可作为GC Roots的对象?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
1.2.4 无论哪种算法都是与“引用”有关,下面分析引用的4种类型,强度依次减弱。
- 强引用:Java中普遍存在的(如:Object obj = new Object()),只要强引用还在,垃圾收集器永远不会回收掉被引用的对象实例。
- 软引用:用于有用但非必需的对象。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。[SoftReference]
- 弱引用:用于非必需对象。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。[WeakReference]
- 虚引用:不影响生存时间,唯一目的就是能在这个对象被收集器回收时收到一个系统通知。[PhantomReference]
1.2.5 详解,真正的”死亡”对象(两次标记后进行回收)
事实上,不可达的对象,也并非“非死不可”,这时它们处于“缓刑”阶段,真正死亡至少要经历两次标记过程:
1.2.5.1 第一次标记
- 对对象进行可达性分析后发现没有与GC Roots相连接的引用链
- 筛选是否有必要执行finalize()方法[没有覆盖finalize()方法或finalize()方法已经被JVM调用过,视为“无需执行”]
1.2.5.2 第二次标记【需要执行finalize(),通过筛选】
- 将该对象放置F-Queue的队列中
- JVM自动建立一个低优先级的Finalizer线程去执行它(会触发执行,但不一定等待它运行结束)
- GC对F-Queue中的对象进行标记(对象可通过finalize()拯救自己,重新关联引用链等)
不建议使用finalize()
1.3 上面讲的是需要回收的堆内存,关于回收方法区(或HotSpot虚拟机中的永久代),效率低
1.3.1 废弃常量
- 与堆类似,没有任何对象和其它地方引用的常量
- 随内存回收,被系统清理出常量池
1.3.2 无用的类
- 该类所有的实例都已经被回收(堆中无该类的实例)
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类的方法。
2. 如何回收?【垃圾收集算法】
2.1 标记-清除[Mark-Sweep]算法
- 标记出所有需要回收的对象
- 统一回收(清除)所有被标记的对象
缺点:效率不高;标记清除后会产生大量不连续的内存碎片。
2.2 复制[Copying]算法
- 将可用内存按容量划分为大小相等的两块
- 每次只使用其中的一块
- 当使用的这块内存用完了,则将还存活的对象复制到另一块上面
- 把使用过的那块内存一次性清理掉
优点:实现简单,运行高效,可按顺序分配内存。
缺点:内存直接缩小为原来一半,代价太大;对象存活率较高时,效率变低。
2.3 标记-整理[Mark-Compact]算法
- 标记出所有需要回收的对象
- 让所有存活的对象都向一端移动(整理)
- 清理掉存活对象端以外的内存
适合堆中的老年代的垃圾收集
2.4 分代收集[Generational Collection]算法(商业虚拟机主要采用方法)
- 根据对象存活周期的不同将内存划分为几块
- 一般分为新生代和老年代
- 不同年代采用最适当的收集算法
- 新生代,一般复制算法
- 老年代,一般“标记-清理”或“标记-整理”
具体如何回收,需依据具体的垃圾收集器实现
关于内存泄露
- OutOfMemoryError异常,java堆溢出:-Xms ,-Xmx
对象不断被创建,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
- VM Stack溢出。-Xss
- 常量池溢出。-XX:PermSize,-XX:MaxPermSize
版权声明:本文为博主原创文章,未经博主允许不得转载。
时间: 2024-11-14 12:47:21