Java笔记_3
垃圾回收器
主要负责在堆上进行内存回收
- 自动垃圾回收可以降低实现难度, 降低回收bug的可能性
- 但是程序员无法控制内存回收的及时性, 也无法完全避免内存溢出
应用场景
- 解决系统僵死(因为频繁的垃圾回收)
- 性能优化
- 常见垃圾回收, 四种引用等
方法区回收
线程不共享的程序计数器和
Java
虚拟机栈以及本地方法栈, 都只需要等待线程销毁自己就销毁了, 不需要垃圾回收
方法区回收的条件(三个)
方法区中的类不再使用, 即可被回收
- 类的所有实例都已经被回收了, 在堆中不存在任何该类的实例对象以及子类对象
1
2
3Class<?> clazz = loader.loadClass("类的全限定名");
Object o = clazz.newInstance();
o = null; // 此时对象o不再使用, 就可以让gc自动回收clazz类 - 加载该类的类加载器已经被回收了
1
2URLClassLoader loader = new URLClassLoader(new URL[]{new URL(spec:"路径")});
loader = null; - 该类对应的
java.lang.Class
对象没有任何地方被引用1
2Class<?> clazz = loader.loadClass("类的全限定名");
clazz = null;
使用
System.gc()
可以手动触发垃圾回收
- 开发过程中此类场景出现较少, 主要在
OSGI, JSP
的热部署等场景中 - 每个
jsp
文件对应一个类加载器, 一个jsp
文件被修改了, 直接写在这个jsp
类加载器, 创建新的类加载器, 重新加载jsp
文件
堆回收
堆上的对象主要看是否还被引用, 如果被引用, 说明不能回收
1 | public class Demo { |
1 | public class ReferenceCounting { |
- 如果想要查看垃圾回收的信息, 可以使用
-verbose:gc
判断堆上的对象是否被引用(两种方法)
- 引用计数法: 每个对象维护一个计数器, 初始值为0, 对象被引用就加1, 取消引用就减1
- 缺点:
引用和取消引用需要维护计数器, 对性能有一定影响
循环引用, 比如
A
引用B
,B
引用A
, 就会导致对象无法回收(上面的例子如果用引用计数法, 就无法回收了) - 可达性分析法(
Java
使用, 性能更高, 解决了循环引用的问题)- 可达性分析算法将对象分为两类: 垃圾回收的根对象(
GC Root
); 普通对象, 对象与对象之间存在引用关系 GC Root
对象一般不可以被回收,JVM
也会维护一个GC Root
对象列表- 每次从某个
GC Root
遍历引用链, 如果某个普通对象可以从GC Root
到达, 说明不可被回收
- 可达性分析算法将对象分为两类: 垃圾回收的根对象(
什么样的对象可以作为GC Root
对象?
- 线程
Thread
对象, 引用线程栈帧中的方法参数、局部变量等 - 系统类加载器加载的
java.lang.Class
对象, 引用类中的静态变量1
2
3
4
5
6public class ReferenceCounting {
public static A a2 = new A();
}
// sun.misc.Launcher是一个GC Root对象, 可以找到应用程序类加载器, 以及扩展类加载器
// 自定义的ReferenceCounting是由应用程序类加载器加载的, 所以可以由GC Root找到
// a2 引用了A, 所以GC Root可以找到a2, 所以不会回收 - 监视器对象, 用来保存同步锁
synchronized
关键字持有的对象
synchronized(Reference.class)
只要这个关系建立起来, 监视器对象就可以找到ReferenceCounting
, 就无法回收 - 本地方法调用时使用的全局对象(不需要程序员过多关注)
- 通过
Arthas
以及eclise MAT(Memory Analyzer)
工具可以查看GC Root
Arthas
使用heapdump <dir/文件名.hprof>
命令将堆内存快照保存到本地磁盘中- 使用
MAT
工具打开堆内存的快照文件 - 使用
GC Roots
功能查看所有的GC Root
对象引用(五种)
强引用, 软引用, 弱引用, 虚引用, 终结器引用
-
强引用
可达性算法中的对象引用一般指强引用, 就是
GC Root
对象对普通对象有引用关系, 那么普通对象就不会被回收 -
软引用
如果一个对象只有软引用关联到它, 如果程序内存不足, 则会将软引用进行回收
JDK 1.2
提供SoftReference
实现软引用, 经常用于缓存中因此使用软引用应该创建两个对象, 一个是
SoftReference
对象, 用于引用真正使用的对象, 而SoftReference
本身应该被Gc Root
引用, 保证可以找到软引用的执行过程
- 对象使用软引用包装起来,
new SoftReference<对象类型>(对象);
- 内存不足时,
JVM
进行垃圾回收 - 垃圾回收仍然不能解决内存不足的问题, 回收软引用中的对象
- 如果依然内存不足, 会抛出
OutOfMemory
异常
1
2
3byte[] bytes = new byte[1024 * 1024 * 100];
SoftReference<byte[]> softReference = new SoftReference<byte[]>(bytes);
// 这段代码将100M的数据放在软引用中Java
中的Caffeine
可以在创建缓存的过程中将缓存对象设置成softValues()
也就是软引用
- 软引用中的对象如果内存不足会被回收,
SoftReference
对象本身也需要被回收, 但是SoftReference
一旦被回收了, 就无法知道其引用的对象是否真的被回收了, 所以SoftReference
提供了一套队列机制来进行判断:- 软引用创建时, 通过构造器传入引用队列(程序员自定义)
- 软引用中包含对象被回收时, 该软引用对象会被放入引用队列
- 通过代码遍历引用队列, 将
SoftReference
的强引用删除
- 软引用可以继承
SoftReference
类的方式来实现,SoftReference
类就是一个软引用对象, 通过构造器传入软引用包含的对象, 以及引用队列 - 使用软引用实现学生数据的缓存, 软引用如果被回收了, 则要清理
HashMap
中的key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73public 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();
}
// 如果没有这个软引用, 或者这个软引用得到的实例为空, 则重新构建一个实例, 保存对这个实例的软引用
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() {
StudenRef ref = null;
while((ref = (StudentRef)q.poll() != null)) {
StudentRefs.remove(ref._key);
}
}
}
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
中, 弱引用本身也可以使用引用队列回收除了
ThreadLocal
以外, 基本上不会使用这个,Caffeine
中也有弱引用的实现, 但是一般不用 -
虚引用(幽灵引用, 幻影引用)
在常规开发中不会使用
不能通过虚引用对象获取包含的对象, 唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知
使用
PhantomReference
实现了虚引用直接内存为了及时知道直接内存中的对象不再使用, 从而回收内存, 就会用虚引用实现
Cleaner
类(解决了直接内存中内存的释放问题) -
终结器引用
在常规开发中不会使用
对象需要被回收的时候, 终结器引用会关联对象并且放在
Finalizer
类中的引用队列, 由一条FinalizerThread
线程从队列中获取对象. 然后执行对象的finalize()
方法(这个方法实际上重写了Object
中的方法, 作用是回收对象时做一些收尾的工作), 在对象第二次被回收时, 该对象才会被真正的回收. 这个过程中finalize()
方法再将自身对象使用强引用关联上, 但是不建议这么做, 因为这个finalize()
方法什么时候调用, 甚至可能不调用, 都是GC
决定的, 不是程序员决定的. (不管是实现自救, 也就是用强引用关联; 还是实现清理工作都是不合适的, 所以基本不会用)