自动垃圾回收
在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏,称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收
内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出
Java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制,通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收,其他很多现代语言比如C#、Python、Go都拥有自己的垃圾回收器
垃圾回收器如果发现某个对象不再使用,就可以回收该对象
垃圾回收的对比:
自动垃圾回收:自动根据对象是否使用由虚拟机来回收对象
- 优点:降低程序员实现难度,降低对象回收bug的可能性
- 缺点:程序员无法控制内存回收的及时性
手动垃圾回收:由程序员编程实现对象的删除
- 优点:回收及时性高,由程序员把控回收的时机
- 缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题
方法区的回收
线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁,而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存,所以这一部分不需要垃圾回收器负责回收
方法区中能回收的内容主要就是不再使用的类
判断一个类可以被卸载,需要同时满足下面三个条件:
- 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象
- 加载该类的类加载器已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
/**
* 类的卸载
*/
public class ClassUnload {
public static void main(String[] args) throws InterruptedException {
try {
ArrayList<Class<?>> classes = new ArrayList<>();
ArrayList<URLClassLoader> loaders = new ArrayList<>();
ArrayList<Object> objs = new ArrayList<>();
while (true) {
URLClassLoader loader = new URLClassLoader(
new URL[]{new URL("file:D:\\lib\\")});
Class<?> clazz = loader.loadClass("com.itheima.my.A");
Object o = clazz.newInstance();
// objs.add(o);
// classes.add(clazz);
// loaders.add(loader);
System.gc();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
添加这两个虚拟机参数进行测试:
-XX:+TraceClassLoading -XX:+TraceClassUnloading
如果注释掉代码中三句add调用,就可以同时满足3个条件,但是需要手动调用System.gc()
方法,让垃圾回收器进行回收
如果需要手动触发垃圾回收,可以调用System.gc()方法
语法: System.gc()
注意事项:调用System.gc()方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收Java虚拟机会自行判断
类卸载的应用场景:
开发中此类场景一般很少出现,主要在如 OSGi、JSP 的热部署等应用场景中
每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器,重新创建类加载器,重新加载jsp文件
堆回收
如何判断堆上的对象可以回收
Java中的对象是否能被回收,是根据对象是否被引用来决定的,如果对象被引用了,说明该对象还在使用,不允许被回收
判断对象是否可以回收,主要有两种方式:
- 引用计数法
- 可达性分析法
引用计数法
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1
引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法,但它也存在如下缺点:
- 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
- 存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题
如果想要查看垃圾回收的信息,可以使用-verbose:gc
参数
语法: -verbose:gc
可达性分析法
Java使用的是可达性分析算法来判断对象是否可以被回收
可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系
下图中A到B再到C和D,形成了一个引用链,可达性分析算法指的是如果从GC Root对象到某个对象是可达的(类似树的遍历),该对象就不可被回收,如果去掉A和B之间的引用,则B、C、D都可被回收
GC Root对象:
- 线程Thread对象,引用线程栈帧中的方法参数、局部变量等
- 系统类加载器加载的java.lang.Class对象,引用类中的静态变量
- 监视器对象,用来保存同步锁synchronized关键字持有的对象
- 本地方法调用时使用的全局对象
通过arthas和eclipse Memory Analyzer (MAT) 工具可以查看GC Root,MAT工具是eclipse推出的Java堆内存检测工具。具体操作步骤如下:
- 使用arthas的heapdump命令将堆内存快照保存到本地磁盘中
- 使用MAT工具打开堆内存快照文件
- 选择GC Roots功能查看所有的GC Root
五种对象引用
- 强引用
- 软引用
- 弱引用
- 虚引用
- 终结器引用
强引用
可达性算法中描述的对象引用,一般指的是强引用,即GC Root对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收
软引用
软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收
在JDK 1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中
注意:软引用对象本身也需要被强引用,否则软引用对象也会被回收掉
软引用的使用方法
软引用的执行过程如下:
- 将对象使用软引用包装起来,new SoftReference<对象类型>(对象)
- 内存不足时,虚拟机尝试进行垃圾回收
- 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象
- 如果依然内存不足,抛出OutOfMemory异常
/**
* 软引用案例 - 基本使用
*/
public class SoftReferenceDemo2 {
public static void main(String[] args) throws IOException {
byte[] bytes = new byte[1024 * 1024 * 100];
SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes);
bytes = null;
System.out.println(softReference.get());
byte[] bytes2 = new byte[1024 * 1024 * 100];
System.out.println(softReference.get());
//
// byte[] bytes3 = new byte[1024 * 1024 * 100];
// softReference = null;
// System.gc();
//
// System.in.read();
}
}
软引用对象本身回收:
软引用中的对象如果在内存不足时回收,SoftReference对象本身也需要被回收
SoftReference提供了一套队列机制:
- 软引用创建时,通过构造器传入引用队列
- 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
- 通过代码遍历引用队列,将SoftReference的强引用删除
/**
* 软引用案例 - 引用队列使用
*/
public class SoftReferenceDemo3 {
public static void main(String[] args) throws IOException {
ArrayList<SoftReference> softReferences = new ArrayList<>();
ReferenceQueue<byte[]> queues = new ReferenceQueue<byte[]>();
for (int i = 0; i < 10; i++) {
byte[] bytes = new byte[1024 * 1024 * 100];
SoftReference studentRef = new SoftReference<byte[]>(bytes,queues);
softReferences.add(studentRef);
}
SoftReference<byte[]> ref = null;
int count = 0;
while ((ref = (SoftReference<byte[]>) queues.poll()) != null) {
count++;
}
System.out.println(count);
}
}
软引用的缓存案例:
使用软引用实现学生信息的缓存,能支持内存不足时清理缓存
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
/**
* 软引用案例 - 学生信息的缓存
*/
public class StudentCache {
private static StudentCache cache = new StudentCache();
public static void main(String[] args) {
for (int i = 0; ; i++) {
StudentCache.getInstance().cacheStudent(new Student(i, String.valueOf(i)));
}
}
private Map<Integer, StudentRef> StudentRefs;// 用于Cache内容的存储
private ReferenceQueue<Student> q;// 垃圾Reference的队列
// 继承SoftReference,使得每一个实例都具有可识别的标识。
// 并且该标识与其在HashMap内的key相同。
private class StudentRef extends SoftReference<Student> {
private Integer _key = null;
public StudentRef(Student em, ReferenceQueue<Student> q) {
super(em, q);
_key = em.getId();
}
}
// 构建一个缓存器实例
private StudentCache() {
StudentRefs = new HashMap<Integer, StudentRef>();
q = new ReferenceQueue<Student>();
}
// 取得缓存器实例
public static StudentCache getInstance() {
return cache;
}
// 以软引用的方式对一个Student对象的实例进行引用并保存该引用
private void cacheStudent(Student em) {
cleanCache();// 清除垃圾引用
StudentRef ref = new StudentRef(em, q);
StudentRefs.put(em.getId(), ref);
System.out.println(StudentRefs.size());
}
// 依据所指定的ID号,重新获取相应Student对象的实例
public Student getStudent(Integer id) {
Student em = null;
// 缓存中是否有该Student实例的软引用,如果有,从软引用中取得。
if (StudentRefs.containsKey(id)) {
StudentRef ref = StudentRefs.get(id);
em = ref.get();
}
// 如果没有软引用,或者从软引用中得到的实例是null,重新构建一个实例,并保存对这个新建实例的软引用
if (em == null) {
em = new Student(id, String.valueOf(id));
System.out.println("Retrieve From StudentInfoCenter. ID=" + id);
this.cacheStudent(em);
}
return em;
}
// 清除那些所软引用的Student对象已经被回收的StudentRef对象
private void cleanCache() {
StudentRef ref = null;
while ((ref = (StudentRef) q.poll()) != null) {
StudentRefs.remove(ref._key);
}
}
// // 清除Cache内的全部内容
// public void clearCache() {
// cleanCache();
// StudentRefs.clear();
// //System.gc();
// //System.runFinalization();
// }
}
class Student {
int id;
String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
弱引用
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收
在JDK 1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用
弱引用对象本身也可以使用引用队列进行回收
import java.io.IOException;
import java.lang.ref.WeakReference;
/**
* 弱引用案例 - 基本使用
*/
public class WeakReferenceDemo2 {
public static void main(String[] args) throws IOException {
byte[] bytes = new byte[1024 * 1024 * 100];
WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes);
bytes = null;
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
}
}
虚引用
虚引用也叫幽灵引用(幻影引用),不能通过虚引用对象获取到包含的对象
虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知
Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现
终结器引用
终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,之后FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收,在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做
/**
* 终结器引用案例
*/
public class FinalizeReferenceDemo {
public static FinalizeReferenceDemo reference = null;
public void alive() {
System.out.println("当前对象还存活");
}
@Override
protected void finalize() throws Throwable {
try{
System.out.println("finalize()执行了...");
//设置强引用自救
reference = this;
}finally {
super.finalize();
}
}
public static void main(String[] args) throws Throwable {
reference = new FinalizeReferenceDemo();
test();
test();
}
private static void test() throws InterruptedException {
reference = null;
//回收对象
System.gc();
//执行finalize方法的优先级比较低,休眠500ms等待一下
Thread.sleep(500);
if (reference != null) {
reference.alive();
} else {
System.out.println("对象已被回收");
}
}
}