垃圾回收
- 字节码文件通过类加载器,将类的信息加载到运行时数据区的方法区中
- 接下来执行引擎的解释器开始解释执行字节码信息的字节码指令,将对象创建出来,放到运行时数据区的堆上
- 在堆上的对象,后面不再使用之后,垃圾回收器会将这些对象进行销毁(自动垃圾回收)
常见内存管理方式
手动回收:C++内存管理
- 在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏。
- 内存泄漏:不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出。
【模拟内存溢出】
在这段代码中,通过死循环不停创建Test类的对象,每一轮循环结束之后,这次创建的对象就不再使用了。但是没有手动调用删除对象的方法,此时对象就会出现内存泄漏。
这段代码中,手动调用delete
删除对象,就不会出现内存泄漏。
我们称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收。
自动回收(GC):Java内存管理
Java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制。通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。其他很多现代语言比如C#、Python、Go都拥有自己的垃圾回收器。
垃圾回收器如果发现某个对象不再使用,就可以回收该对象。
自动、手动回收优缺点
- 自动垃圾回收,自动根据对象是否使用由虚拟机来回收对象
- 优点:降低程序员实现难度、降低对象回收bug的可能性
- 缺点:程序员无法控制内存回收的及时性,对象不用之后,不会及时被清理掉,需要等待垃圾回收器工作,有一定的滞后
- 手动垃圾回收,由程序员编程实现对象的删除
- 优点:回收及时性高,由程序员把控回收的时机
- 缺点:编写不当容易出现悬空指针(对象内存被释放,但是指针没有指向null)、重复释放(delete返回执行)、内存泄漏、程序员粗心忘记释放等问题
应用场景
解决系统僵死的问题
:大厂的系统出现的许多系统僵死问题,即程序在运行,但是得不到回应,这个现象与频繁的垃圾回收有关,JVM忙于垃圾回收,把用户请求晾一边性能优化
:对垃圾回收器进行合理的设置可以有效地提升程序的执行性能高频面试题
:- 常见的垃圾回收器
- 常见的垃圾回收算法
- 四种引用
- 项目中用了哪一种垃圾回收器
垃圾回收器需要对哪些部分内存进行回收?
不需要垃圾回收器回收
- 首先是线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁(线程不再使用的时候,会将线程的内存释放)。
- Java虚拟机栈、本地方法栈中存储了方法的栈帧,方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存。
需要垃圾回收器回收
方法区的回收
方法区中能回收的内容主要就是不再使用的类。判定一个类可以被卸载。需要同时满足下面三个条件:
- 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
这段代码中就将局部变量对堆上实例对象的引用去除了,所以对象就可以被回收。
- 加载该类的类加载器已经被回收。
这段代码让局部变量对类加载器的引用去除,类加载器loader就可以被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用。
代码测试
package chapter04.gc;
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
-XX:+TraceClassLoading
:程序运行过程中,打印出类的加载
-XX:+TraceClassUnloading
:类被卸载的时候,会打印出日志
如果注释掉代码中三句add调用,就可以同时满足3个条件。
手动调用垃圾回收方法System.gc()
注意:调用System.gc()方法并不一定会立即回收垃圾,仅仅是向Java虚拟机发送一个垃圾回收的请求,具体是否需要执行垃圾回收Java虚拟机会自行判断。
代码运行
执行之后,日志中就会打印出类卸载的内容:
- Unloading:类被卸载
- 三个条件都存在,加载之后会被回收
【第一个条件不满足】
运行之后,发现没有卸载
【还可以模拟其他条件】
当其他条件没有满足的时候,一样不触发类的卸载
思考
我们所写的类是由应用程序加载器加载的,这个加载器是不会被回收的,所以一直不会满足条件二,那么类卸载主要用在什么场景下呢?
- 开发中此类场景一般很少出现,主要在如 OSGi、JSP 的热部署等应用场景中。
- 每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
**注:**方法区的回收我们很难用到,了解一下即可
堆回收
堆是java程序中最大的一部分内存,里面有很多对象
如何判断堆的对象是否可以回收
- 垃圾回收器要回收对象的第一步就是判断哪些对象可以回收。
- Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。
案例介绍
【简单案例】
第一行代码执行之后,堆上创建了Demo类的实例对象,同时栈上保存局部变量引用堆上的对象。
第二行代码执行之后,局部变量对堆上的对象引用去掉,引用关系失效,那么堆上的对象就可以被回收了。
【复杂案例】
这个案例中,如果要让对象a和b回收,必须将局部变量到堆上的引用去除。
- 直接找A的引用需要去除,即a1 = null
- 通过B找A的引用也需要去除,即b1.a = null
如果a1 = null;b1 = null;这样可以回收A和B对象吗?
答:可以,虽然a1.b = b1; 但是a1都为null了,就找不到b1了。A和B互相之间的引用需要去除吗?答案是不需要,因为局部变量都没引用这两个对象了,在代码中已经无法访问这两个对象,即便他们之间互相有引用关系,也不影响对象的回收。
判断对象是否可以回收,主要有两种方式:
- 引用计数法
- 可达性分析法。
引用计数法(JVM不使用)
引用计数法会为每个对象维护一个引用计数器(初始值为0),当对象被引用时加1,取消引用时减1。如下图中,对象A的计数器初始为0,局部变量a1对它引用之后,计数器就变成了1。同样A对B产生了引用,B的计数器也是1。
如果把A对B的引用去除,则变为下图
即如果一个对象的引用计数器是0,说明这个对象没有引用,就可以被垃圾回收了。
优缺点
优点:
- 实现简单,C++中的智能指针就采用了引用计数法
缺点:
- 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
- 存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题,导致内存泄漏。
这张图上,由于A和B之间存在互相引用,所以计数器都为1,两个对象都不能被回收。但是由于没有局部变量对这两个代码产生引用,代码中已经无法访问到这两个对象,理应可以被回收。
验证JVM没有使用引用计数法
可以做一个实验,验证下Java中循环引用不会导致内存泄漏,如果不会泄露,说明JVM没有使用引用计数法。
可以通过垃圾回收日志去看一个对象有没有被回收,如果想查看垃圾回收的信息,可以添加虚拟机参数:-verbose:gc
。
【代码】
public static void main(String[] args) throws IOException {
while (true){
A a1 = new A();
B b1 = new B();
a1.b = b1;|
b1.a = a1;
a1 = null;
b1 = null;
System.gc();
}
}
【运行】
加上这个参数之后执行代码,发现对象确实被回收了,因为内存大小始终差不多,一直维持在1000K,说明每轮循环创建的两个对象在垃圾回收之后都被回收了。如果没有回收的话,随着循环的进行,每次new A和new B,占用的内存会越来越大。说明JVM没有使用引用计数法
可达性分析法(JVM使用)
Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:
- 垃圾回收的根对象(GC Root)
- 普通对象
根对象和普通对象之间,会存在引用关系。下图中A到B再到C和D,形成了一个引用链。可达性分析算法指的是如果从某个到GC Root对象是可达的,对象就不可被回收。
- 通过对象A,可以找到对象B。通过对象B,又可以找到对象C、对象D。所以对象A、B、C、D都不可以被回收。
- 如果断掉对象A和对象B的联系,则对象B、C、D都可以回收。
四类GC Root对象
GC Root对象是不可以被回收的,**哪些对象被称之为GC Root对象呢?**满足如下四大类即是GC Root对象,否则是普通对象
第一类:线程Thread对象
,它会引用线程栈帧中的方法参数、局部变量等。
第二类:系统类加载器加载的java.lang.Class对象
,引用类中的静态变量。(GC Root可以关联到静态变量,所以A的实例也不能被回收)
第三类:监视器对象
,用来保存同步锁synchronized关键字持有的对象。(如下图的ReferenceCouting对象不可以被回收)
第四类:本地方法调用时使用的全局对象
。(虚拟机控制调用,Java程序员可以不用太关注)
查看GC Root对象的工具
通过Arthas
和eclipse Memory Analyzer (MAT)
工具可以查看GC Root,MAT工具是eclipse推出的Java堆内存检测工具。具体操作步骤如下:
步骤一
:使用arthas的heapdump命令将堆内存快照保存到本地磁盘中。
步骤
二:使用MAT工具打开堆内存快照文件选择GC Roots功能查看所有的GC Root。
【演示步骤详解】
1、代码如下:
package com.itheima.jvm.chapter04;
import java.io.IOException;
public class ReferenceCounting {
public static A a2 = null;
public static void main(String[] args) throws IOException {
// while (true){
A a1 = new A();
B b1 = new B();
a1.b = b1;
b1.a = a1;
a2 = a1;
// 程序阻塞
System.in.read();
// a1 = null;
// b1 = null;
// }
}
}
class A {
B b;
// byte[] t = new byte[1024 * 1024 * 10];
}
class B {
A a;
// byte[] t = new byte[1024 * 1024 * 10];
}
2、使用arthas连接到程序,输入如下命令:
heapdump 内存快照文件输出目录/test2.hprof(文件名.hprof)
这样就生成了一个堆内存快照(后面介绍,简单来说就是包含了所有堆中的对象信息)。
3、下载MAT工具。
下载路径:https://download.csdn.net/download/laodanqiu/89495359
如果出现如下错误,请将环境变量中的JDK版本升级到17以上
4、选择菜单中的打开堆内存快照功能,并选择刚才生成的文件。
5、选择内存泄漏检测报告,并确定。
6、通过菜单找到GC Roots。
7、MAT对4类GC Root对象做了分类。
8、找到局部变量。
9、找到静态变量。
System Class下面的类太多了,可以通过复制A的内存地址来找
拿到内存地址之后,通过地址反向寻找关联它的RC Root
查询出来的结果
常见的五种引用对象
- 强引用:可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。除了强引用之外,Java中还设计了几种其他引用方式:
- 软引用
- 弱引用
- 虚引用
- 终结器引用
软引用
- 软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收(强引用的对象还是会保留)。
- 软引用常用于缓存中(不能用来存储重要数据,不然有被回收的风险)
- 如何实现软引用?在JDK 1.2版之后提供了SoftReference类来实现软引用。
如下图中,对象A被GC Root对象强引用了,同时我们创建了一个软引用SoftReference对象(它本身也是一个对象),软引用对象中引用了对象A。
接下来强引用被去掉之后(例如给静态变量赋值一个null),对象A暂时还是处于不可回收状态,因为有软引用存在并且内存还够用。
如果内存出现不够用的情况,对象A就处于可回收状态,可以被垃圾回收器回收。
这样做有什么好处?如果对象A是一个缓存,平时会保存在内存中,如果想访问数据可以快速访问。但是如果内存不够用了,我们就可以将这部分缓存清理掉释放内存。即便缓存没了,也可以从数据库等地方获取数据,不会影响到业务正常运行,这样可以减少内存溢出产生的可能性。
特别注意:
软引用对象本身,需要被强引用(真实环境中一定存在这个关系),否则软引用对象也会被回收掉。
软引用的使用方法
软引用的执行过程如下:
- 将对象使用软引用包装起来,new SoftReference<对象类型>(对象)。
- 内存不足时,虚拟机尝试进行垃圾回收。
- 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
- 如果依然内存不足,抛出OutOfMemory异常。
代码:
/**
* 软引用案例2 - 基本使用
*/
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());
// 再创建一个100M的数组,内存不够用,软引用的对象要被释放掉
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();
}
}
添加虚拟机参数,限制最大堆内存大小为200m,因为堆内存中还需要放一些其他对象,所以堆内存实际上只有100多M可以使用,理论上是放不下两个100M的数组的:
执行后发现,第二个100m对象创建之后需,软引用中包含的对象已经被回收了。
软引用对象本身怎么回收呢?
如果软引用对象里边包含的数据已经被回收了,那么软引用对象本身其实也可以被回收了。
SoftReference提供了一套队列机制:
1、软引用创建时,通过构造器传入引用队列
2、在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
3、通过代码遍历引用队列,将SoftReference的强引用删除
代码
/**
* 软引用案例3 - 引用队列使用
*/
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);
}
}
最终展示的结果是:
这9个软引用对象中包含的数据已经被回收掉,所以可以手动从ArrayList中去掉,这样就可以释放这9个对象。
工作场景:软引用的缓存案例
使用软引用实现学生信息的缓存,内存不足时,会清理Map里面的值存储的Student对象
注意:value回收了,key也要同步回收
代码:
import lombok.Data;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
/**
* 软引用案例4 - 学生信息的缓存
*/
public class StudentCache {
private static StudentCache cache = new StudentCache();
/**
* 测试,限制堆内存,死循环,堆内存也不会溢出,因为会及时回收
*
* @param args
*/
public static void main(String[] args) {
for (int i = 0; ; i++) {
StudentCache.getInstance().cacheStudent(new Student(i, String.valueOf(i)));
}
}
/**
* 用于Cache内容的存储
*/
private Map<Integer, StudentRef> StudentRefs;
/**
* 被回收的Reference的队列
*/
private ReferenceQueue<Student> q;
/**
* 继承SoftReference,使得每一个实例都具有可识别的标识。
* 并且该标识与其在HashMap内的key相同。
*/
private class StudentRef extends SoftReference<Student> {
/**
* 根据软引用获取key,这样才能在map中删除相应的key
*/
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>();
}
/**
* 取得缓存器实例
*
* @return
*/
public static StudentCache getInstance() {
return cache;
}
/**
* 以软引用的方式对一个Student对象的实例进行引用并保存该引用
*
* @param em
*/
private void cacheStudent(Student em) {
// 清除垃圾引用,删除map的key和value
cleanCache();
StudentRef ref = new StudentRef(em, q);
StudentRefs.put(em.getId(), ref);
System.out.println(StudentRefs.size());
}
/**
* 依据所指定的ID号,获取相应Student对象的实例
*
* @param id
* @return
*/
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;
// q里面存储的是已经被回收的软引用,不停将软引用弹出来,然后删除相应的key
while ((ref = (StudentRef) q.poll()) != null) {
StudentRefs.remove(ref._key);
}
}
}
@Data
class Student {
int id;
String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
}
弱引用(一般不使用)
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。在JDK 1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。
弱引用对象本身也可以使用引用队列进行回收。
package chapter04.weak;
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());
}
}
执行之后发现gc执行之后,对象已经被回收了。
虚引用和终结器引用(常规开发不使用)
这两种引用在常规开发中是不会使用的。
- 虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
- 终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。
下面的代码仅仅是为了面试,工作中不会写出这种代码
package chapter04.finalreference;
/**
* 终结器引用案例
*/
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()执行了...");
// 调用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;
// 回收对象,回收的时候,finalize方法会被终结器线程调用一次
System.gc();
// 执行finalize方法的优先级比较低,休眠500ms等待一下
Thread.sleep(500);
if (reference != null) {
reference.alive();
} else {
System.out.println("对象已被回收");
}
}
}
【运行】
文章说明
该文章是本人学习 黑马程序员 的学习笔记,文章中大部分内容来源于 黑马程序员 的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 黑马程序员 的优质课程表示感谢。