虚拟机内存优化
为什么要懂垃圾回收
当前虚拟机垃圾自动回收技术非常强大,基本上对于很多人都已经实现了自动化,为什么我们还有去学内存分配和垃圾回收原理呢?
- 当系统出现内存溢出、内存泄漏等问题时
- 当垃圾回收成为系统并发瓶颈时
垃圾回收主要针对java堆以及方法区,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支可能需要的内存可能也不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存回收都是动态的
对象存活判断
栈 | 堆 |
---|
如图所示,栈内变量objA引用堆内对象 A(0x1234),栈内变量objB引用堆内对象B(0x2345),同时堆内对象A(0x1234)中变量instance引用堆内对象B(0x2345),堆内对象B(0x2345)中变量instance引用堆内对象A(0x1234)
如果关闭objA和objB对堆内对象的引用,以下两种算法会有区别吗?会有什么区别?
引用计数算法
给对象添加一个引用计数器,有一个引用时,计数器+1,失去引用时,计数器-1,为0时表示该对象为垃圾对象
可达性算法
根据GC Roots逐步向堆中查找所有被引用的对象,并标记,最后没被标记的是垃圾对象
GC Roots代表:
- 虚拟机栈(栈帧中的本地变量表)中的对象变量
- 方法区中类静态属性的对象变量
- 方法区中常量的对象变量
- 本地方法栈中JNI(及Native方法)的对象变量
HotSpot的可达性分析
-
从GC Roots 节点找引用链,可是现在很多应用的引用比较复杂,比如方法区就有数百兆,如果逐个检查这里面的引用,必定会消耗大量时间
-
为了保证分析期间整个执行系统被冻结,不产生新的引用,jvm会执行线程停顿(stop the world)
解决方案
- 枚举根节点,使用一组OopMap的数据结构来存放对象引用,这个数据结构在类加载完成的时候,已经计算出来了,GC在扫描的时候就可以得知这些信息,从而降低GC Roots 时间以及减少停顿时间
- OopMap 中的引用关系可能会变化,或者OopMap的指令太多,反而需要更多的空间,此时解决方案是 , OopMap会根据虚拟机选定的安全点(safepoint, 可以简单理解为执行到哪一行),在这个安全点内去生成指令的OopMap,在GC的时候,驱使所有的线程都运行到最近的安全点,STW才会发生,应用才停顿。
- 对于挂起的线程来说,sleep或bocked状态无法运行到安全点,可以通过增大安全域(Safe Region),如果线程已经达到安全域,做一个标记,GC就不需要管理这些线程
引用
-
强引用
用到的new了一个对象就是强引用,例如 Object obj = new Object();
当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不会回收具有强引用的存活着的对象!强引用对象回收:
当一个普通对象没有其他引用关系,只要超过了引用的作用域或者显示的将引用赋值为null时,你的对象就表明不是存活着,这样就会可以被GC回收了。当然回收的时间是不一定的具体得看GC回收策略 -
软引用
软引用的生命周期比强引用短一些。软引用是通过SoftReference类实现的。Object obj = new Object(); //SoftReference softObj = new SoftReference(obj);//转换为软引用<br><br>ReferenceQueue qu = new ReferenceQueue() 1 SoftReference softObj = new SoftReference(obj, qu); 1 obj = null; //去除强引用
这样就是一个简单的软引用使用方法。当JVM认为内存空间不足时,就回去试图回收软引用指向的对象,也就是说在JVM抛出OutOfMemoryError之前,会去清理软引用对象。软引用可以与引用队列(ReferenceQueue)联合使当softObj软引用的obj被GC回收之后,softObj 对象就会被塞到queue中,之后我们可以通过这个队列的poll()来检查你关心的对象是否被回收了,如果队列为空,就返回一个null。反之就返回软引用对象也就是softObj。软引用一般用来实现内存敏感的缓存,如果有空闲内存就可以保留缓存,当内存不足时就清理掉,这样就保证使用缓存的同时不会耗尽内存。例如图片缓存框架中缓存图片就是通过软引用的。
用途:比如考虑一个图像编辑器的程序。该程序会把图像文件的全部内容都读取到内存中,以方便进行处理。而用户也可以同时打开多个文件。当同时打开的文件过多的时候,就可能造成内存不足。如果使用软引用来指向图像文件内容的话,垃圾回收器就可以在必要的时候回收掉这些内存。
-
弱引用
Object obj = new Object(); WeakReference<Object> weakObj = new WeakReference<Object>(obj); obj = null;
弱引用是通过WeakReference类实现的,它的生命周期比软引用还要短,也是通过get()方法获取对象。在GC的时候,不管内存空间足不足都会回收这个对象,同样也可以配合ReferenceQueue 使用,也同样适用于内存敏感的缓存。ThreadLocal中的key就用到了弱引用。
用途:
弱引用的作用在于解决强引用所带来的对象之间在存活时间上的耦合关系。弱引用最常见的用处是在集合类中,尤其在哈希表中。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。
对于这种情况的解决办法就是使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。Java中提供了WeakHashMap来满足这一常见需求。
4.幻象引用
也称虚引用,是通过PhantomReference类实现的。任何时候可能被GC回收,就像没有引用一样。无法通过虚引用访问对象的任何属性或者函数。那就要问了要它有什么用?虚引用仅仅只是提供了一种确保对象被finalize以后来做某些事情的机制。比如说这个对象被回收之后发一个系统通知啊啥的。虚引用是必须配合ReferenceQueue 使用的,具体使用方法和上面提到软引用的一样。主要用来跟踪对象被垃圾回收的活动。
幽灵引用及其队列的使用情况并不多见,主要用来实现比较精细的内存使用制,这对于移动设备来说是很有意义的。程序可以在确定一个对象要被回收之后,再申请内存创建新的对象。通过这种方式可以使得程序所消耗的内存维持在一个相对较低的数量。比如下面的代码给出了一个缓冲区的实现示例。
public class PhantomBuffer {
private byte[] data = new byte[0];
private ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
private PhantomReference<byte[]> ref = new PhantomReference<byte[]>(data, queue);
public byte[] get(int size) {
if (size <= 0) {
throw new IllegalArgumentException("Wrong buffer size");
}
if (data.length < size) {
data = null;
System.gc(); //强制运行垃圾回收器
try {
queue.remove(); //该方法会阻塞直到队列非空
ref.clear(); //幽灵引用不会自动清空,要手动运行
ref = null;
data = new byte[size];
ref = new PhantomReference<byte[]>(data, queue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return data;
}
}
方法取回收
方法区(Hotspot的永久代),也可以存在离阿基回收,尽管在java虚拟机规范中确实表示可以不要求虚拟机在方法区实现垃圾回收,且性价比比较低,我们通过配置相关参数,在何时时候来回收永久代,以保证不会溢出
- 永久代回收信息:废弃的常量和无用的类
废弃的常量:没有任何地方使用常量引用,也没有任何地方使用常量对应的字面量
无用的类:该类所有的实例被回收,加载该类的类加载器被回收,该类的字节码对象没有被任何地方引用 - 在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成jsp以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备卸载功能
垃圾收集算法
标记-清除
标记清除(mark-sweep)算法,算法分为标记 和 清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续算法都是基于这种思路并对其缺点进行改进而得到的。
主要缺点有两个:
- 效率问题,标记和清除过程的效率都不高;而且需要暂停整个应用(STW)
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而得不到提前触发另一次垃圾收集动作。
标记-整理
标记-整理(Mark-Compact)算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收的对象进行清理,而是让存活状态的对象都想一端移动,然后清空边界外内存
复制
复制(copying)算法,将内存分为大小相等两块,每次使用其中一块,其中一块内存使用完后,将活着的对象复制到空的一块内存上,内存使用完的这块清空
优点:内存分配时不用考虑内存碎片,只需要移动堆顶指针,按需分配内存,实现简单,运行高效。
缺点: 内存只能使用一半,持续复制长生存期的对象则导致效率降低
分代收集
分代基本前提:绝大部分对象的生命周期非常短暂,而且不同的对象生命周期是不一样的
分代收集(Generational Collection)算法,把Java堆分为新生代和老年代
-
新生代;主要存放新创建的对象,内存较小,垃圾回收比较频繁,垃圾收集之后会有大批对象回收,只有少量存活,适合使用复制算法(以下为复制算法举例)
新生代内存空间分为1个Eden Space 和 2个Survivor Space,对象创建将放入Eden Space,垃圾回收会扫描 Eden Space 和 A Survivor Space,如果对象存活,将对象复制到B Survivor Space,如果对象已经经过多次扫描,将对象移动到Old Gen,B Survivor Space如果满了将对象移动到Old Gen,扫描完毕后JVM将Eden Space 和 A Survivor Space清空。然后交换A 和 B 角色(即下次垃圾回收会扫描Eden Space 和 B Survivor Space) -
老年代:JVM认为生命周期较长的对象,内存空间较大,垃圾回收频次低,垃圾回收后存活对象多,适合使用标记-清理 或 标记-整理算法,算法的选择与垃圾回收器相关
垃圾回收按照系统区分
1. 串行收集:使用单线程处理所有的垃圾回收工作,实现容易,效率高,性能限制多,适用于单处理器或小数据量(100M以下)的多处理器机器上,工作时停止正常程序(STW)
2. 并行收集:串行收集的多线程模式,适用于多处理器机器上,CPU越多优势越明显,工作是停止正常程序(STW)
3. 并发收集:同时执行正常程序和垃圾回收程序