当前位置: 首页 > news >正文

JVM5-垃圾回收

自动垃圾回收

在C/C++这类没有自动垃圾回收机制的语言中,一个对象如果不再使用,需要手动释放,否则就会出现内存泄漏,称这种释放对象的过程为垃圾回收,而需要程序员编写代码进行回收的方式为手动回收

内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出

Java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制,通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收,其他很多现代语言比如C#、Python、Go都拥有自己的垃圾回收器

垃圾回收器如果发现某个对象不再使用,就可以回收该对象

垃圾回收的对比:

自动垃圾回收:自动根据对象是否使用由虚拟机来回收对象

  • 优点:降低程序员实现难度,降低对象回收bug的可能性
  • 缺点:程序员无法控制内存回收的及时性

手动垃圾回收:由程序员编程实现对象的删除

  • 优点:回收及时性高,由程序员把控回收的时机
  • 缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题

方法区的回收

线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁,而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存,所以这一部分不需要垃圾回收器负责回收

方法区中能回收的内容主要就是不再使用的类

判断一个类可以被卸载,需要同时满足下面三个条件:

  1. 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象
  2. 加载该类的类加载器已经被回收
  3. 该类对应的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++中的智能指针就采用了引用计数法,但它也存在如下缺点:

  1. 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
  2. 存在循环引用问题,所谓循环引用就是当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堆内存检测工具。具体操作步骤如下:

  1. 使用arthas的heapdump命令将堆内存快照保存到本地磁盘中
  2. 使用MAT工具打开堆内存快照文件
  3. 选择GC Roots功能查看所有的GC Root

五种对象引用

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用
  • 终结器引用

强引用

可达性算法中描述的对象引用,一般指的是强引用,即GC Root对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收

软引用

软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收

在JDK 1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中

注意:软引用对象本身也需要被强引用,否则软引用对象也会被回收掉

软引用的使用方法

软引用的执行过程如下:

  1. 将对象使用软引用包装起来,new SoftReference<对象类型>(对象)
  2. 内存不足时,虚拟机尝试进行垃圾回收
  3. 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象
  4. 如果依然内存不足,抛出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提供了一套队列机制:

  1. 软引用创建时,通过构造器传入引用队列
  2. 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
  3. 通过代码遍历引用队列,将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("当前对象还存活");}@Overrideprotected 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("对象已被回收");}}
}

垃圾回收算法

Java是如何实现垃圾回收的呢?简单来说,垃圾回收要做的有两件事:

  1. 找到内存中存活的对象
  2. 释放不再存活对象的内存,使得程序能再次利用这部分空间

垃圾回收算法的历史和分类

1960年John McCarthy发布了第一个GC算法:标记-清除算法

1963年Marvin L. Minsky发布了复制算法

本质上后续所有的垃圾回收算法,都是在上述两种算法的基础上优化而来

垃圾回收算法的评价标准

Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程,这个过程被称之为Stop The World,简称STW,如果STW时间过长则会影响用户的使用

判断GC算法是否优秀,可以从三个方面来考虑:

1.吞吐量

吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高

2.最大暂停时间

最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值

比如如下的图中,黄色部分的STW就是最大暂停时间,显而易见上面的图比下面的图拥有更少的最大暂停时间,最大暂停时间越短,用户使用系统时受到的影响就越短

3.堆使用效率

不同垃圾回收算法,对堆内存的使用方式是不同的

比如标记清除算法,可以使用完整的堆内存;而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法

上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得

一般来说,堆内存越大,最大暂停时间就越长;想要减少最大暂停时间,就会降低吞吐量

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • pandas读取带有表头的数据文件,读取无表头的数据文件
  • C语言深入理解指针4
  • 小琳AI课堂:深入学习Transformer模型
  • 2024国赛数学建模备战:灰色预测,国赛数学建模思路代码 模型
  • 如果文件从存储卡中被误删除,存储卡数据恢复如何恢复?
  • 亚信安全荣获“2024年网络安全优秀创新成果大赛”优胜奖
  • Android Radio2.0——交通公告状态设置(二)
  • Linux网络——Socket编程函数
  • jupyter里怎么设置代理下载模型
  • log4j日志封装说明—slf4j对于log4j的日志封装-正确获取调用堆栈
  • 八股集合1
  • OpenCV结构分析与形状描述符(11)椭圆拟合函数fitEllipse()的使用
  • 目标检测-YOLOv5
  • Java 日志
  • js逆向-实现哈希算法
  • 《Javascript数据结构和算法》笔记-「字典和散列表」
  • 0基础学习移动端适配
  • Brief introduction of how to 'Call, Apply and Bind'
  • create-react-app项目添加less配置
  • HashMap剖析之内部结构
  • iOS 系统授权开发
  • JAVA之继承和多态
  • JS题目及答案整理
  • node学习系列之简单文件上传
  • Octave 入门
  • PAT A1050
  • php面试题 汇集2
  • XML已死 ?
  • 第2章 网络文档
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 面试题:给你个id,去拿到name,多叉树遍历
  • 如何使用 OAuth 2.0 将 LinkedIn 集成入 iOS 应用
  • 十年未变!安全,谁之责?(下)
  • 详解移动APP与web APP的区别
  • 一个普通的 5 年iOS开发者的自我总结,以及5年开发经历和感想!
  • 函数计算新功能-----支持C#函数
  • ​​​​​​​ubuntu16.04 fastreid训练过程
  • ​水经微图Web1.5.0版即将上线
  • #ifdef 的技巧用法
  • $.proxy和$.extend
  • (BFS)hdoj2377-Bus Pass
  • (vue)el-tabs选中最后一项后更新数据后无法展开
  • (附源码)计算机毕业设计SSM智能化管理的仓库管理
  • (附源码)计算机毕业设计高校学生选课系统
  • (附源码)小程序儿童艺术培训机构教育管理小程序 毕业设计 201740
  • (经验分享)作为一名普通本科计算机专业学生,我大学四年到底走了多少弯路
  • *Algs4-1.5.25随机网格的倍率测试-(未读懂题)
  • .NET 5种线程安全集合
  • .net 7 上传文件踩坑
  • .NET C# 使用GDAL读取FileGDB要素类
  • .NET 药厂业务系统 CPU爆高分析
  • .NET:自动将请求参数绑定到ASPX、ASHX和MVC(菜鸟必看)
  • .net经典笔试题
  • .Net转前端开发-启航篇,如何定制博客园主题
  • /*在DataTable中更新、删除数据*/