Java垃圾回收和内存泄漏
关于GC机制,堆是Java虚拟机进行垃圾回收的主要场所,其次是方法区。什么是垃圾?简单就是内存中已经不再被使用的空间
GC机制的实现原理:
- 引用计数算法
给对象添加引用计数器,每当有一个方法引用它,计数器+1;引用失效则计数器-1,当计数器在一段时间为0时则判断该对象应该被回收,JVM一般不采用该算法,它很难解决对象之间相互引用的问题,比如两个对象相互引用但是已经没有作用,此时应当将其回收,但是这种情况不符合引用计数算法,所以并不会回收
2.可达性分析算法
基本思路是通过一系列叫GC Roots的对象作为起始点,从其开始向下检索、如果一个对象到GC Roots没有任何引用链相连,则说明该对象不可用
那么被GC判断为垃圾的对象会立刻回收吗?
并不会,垃圾回收过程中,当对象变成GC Roots不可达,GC会判断该对象是否重写了finalize方法--->没覆盖过,则直接回收;如果覆盖过且还没执行该finalize的方法,就会将其放入F-Queue队列,由一个低优先级的线程来执行finalize方法,执行后GC会再次判断该对象是否可达,若不可达直接回收,否则对象“复活”(可以把待回收对象赋值给GC Roots可达对象的引用放入finalize方法中,从而达到对象再生的目的)
关于finaize
该方法是Object中的protected方法,子类可以覆盖该方法从而来实现资源清理工作,被调用在GC回收对象之前。需要注意的是该方法调用时间是不确定性的所以通常来说并不推荐使用finalize来完成非内存资源的清理工作。
关于Java的内存泄漏
内存泄漏是指不再被引用的对象或变量一直占据在内存中
内存泄漏的几种情况:
- 长生命周期的对象持有短生命周期对象的引用
因为尽管短生命周期对象已经不再需要,但是长生命周期对象持有它的引用而导致不能被回收,该对象本质上是应该被垃圾回收器回收的
可以看出object只作用在method1方法中,其他地方并不会用到它,但是当该方法执行过后,object对象所分配的内存不会立马被认为是可以释放的对象,只有Simple类创建的对象被释放后才会被释放
解决方法:
转自JAVA 内存泄露详解(原因、例子及解决)_anxpp的博客-CSDN博客_java内存泄露
2.那么另一种情况就是当对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值不相同。即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象造成内存泄漏
对象的深浅拷贝
浅拷贝-实现Cloneable接口并重写Object类中的clone方法
该拷贝仅仅复制所拷贝的对象,但是不复制它所引用的对象,复制的只是拷贝对象的引用(即指向堆区的同一对象),所以是引用了被拷贝对象的相同对象,引用的对象与其共享,如果其中一个对象的引用对象被修改,那么另一个对象的引用对象也会修改(因为是同一对象)。
实现方法:覆盖Object类的clone方法实现浅拷贝
使用该方法时,一定要注意两点(
1.实现Cloneable接口
2.重写的clone方法要pubic,默认是protected
)
实例
Dog类:
public class Dog implements Cloneable{
private String name;
private int age;
private Person person;
public int getAge() {
return age;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public Object clone() throws CloneNotSupportedException {
Dog d = (Dog)super.clone();
return d;
}
}
Person类:
public class Person implements Cloneable{
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试类:
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Person p = new Person("老王");
Dog dog = new Dog();
dog.setPerson(p);
Dog dog2 = (Dog) dog.clone();
System.out.println("原数据:"+dog.getPerson().getName());
System.out.println("原数据:"+dog2.getPerson().getName());
System.out.println("------------------修改克隆对象数据----------------------");
p.setName("老刘");
System.out.println(dog.getPerson());
System.out.println(dog2.getPerson());
System.out.println(dog.getPerson().getName());
System.out.println(dog2.getPerson().getName());
}
}
结果如下:
浅拷贝-->打印出来的两个对象引用的Person地址是一致的,只要其中一个对象修改p的值,那么另一个也会变,最后名字都是老刘
深拷贝
- 方法一
在clone方法内,复制一份Person的对象
Dog类中
Person类也要重写clone方法,其他不变动
结果如下:
那么此时可以看出,打印出来的两个对象引用的Person地址是不一致的,只要其中一个对象修改p的值,那么另一个并不会变,所以最后主对象是老王克隆对象的是老刘
2.方法二
利用序列化实现深拷贝
Personl和Dog类实现Serializable接口,在Dog类中写一个深度拷贝自己的方法
public Object deepCopy() throws IOException, ClassNotFoundException {
//实现序列化
ByteArrayOutputStream b = new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(b);
o.writeObject(this);
//反序列化
ByteArrayInputStream i =new ByteArrayInputStream(b.toByteArray());
ObjectInputStream oi = new ObjectInputStream(i);
return oi.readObject();
}
测试方法中调用
Dog dog2 = (Dog) dog.deepCopy();
结果如上。
写一个通用的方法(深度拷贝工具类)
public class DeepCloneUtil {
public static<T extends Serializable> T deepCopy(T t) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(t);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return (T) oi.readObject();
}
}
Dog dog2 = DeepCloneUtil.deepCopy(dog);
同样相同的结果。
那么什么是序列化和反序列化呢?
Java序列化就是将Java对象转换成字节序列的过程,反序列化则相反,将字节序列转成Java对象的过程。
为什么要使用序列化和反序列呢?
网络上发送的数据我们都知道是以二进制的形式进行远程通讯。发送方只需要将Java对象序列化成字节序列就能在网络上进行传输通讯,接收方将接收到的字节序列发序列化成Java对象即可
实现序列化和反序列化的好处?
好处就是实现了数据的持久化,通过序列化可以将数据永久的保存到磁盘中
还有就是上述说的实现远程通信传输对象
序列化的步骤
1.创建一个对象输出流(序列化),可以包装一个其他类型的目标输出流,例如文件输出流FileOutputStream,或者字节输出流ByteArrayOutputStream
2.通过对象输出流将Java对象输出成二进制字节序列,writeObject()方法,参数就是Java对象
反序列化的步骤
1.创建一个对象输入流(反序列化),可以包装一个其他类型的目标输入流,例如文件输入流FileInputStream
2.通过对象输出流readObject()读取成一个Java对象
需要注意的是
1.如果一个对象引用另一个对象,两个类都需要进行序列化(例如上述例子的Person类)
2.如果序列化对象时,不希望某个属性被序列化,可以在该属性前添加transient关键字(临时变量)或者static,因为序列化保存的是对象状态,而静态变量属于类的状态
为什么序列化可以实现深拷贝?
序列化流中本质上没有保存任何引用数据,都是字面量数据。因此当进行反序列化时,其实本质是重新创建一个空对象进行赋值,引用变量的话则递归进行创建空对象赋值
上述例子中,使用到字节流,关于ByteArrayInputStream与ByteArrayOutputStream
就ByteArrayOutputStream来说,该类继承自OutputStream输出流,使用该输出流时,输出到的是一个byte数组,缓冲区会随着数据 的不断写入
而自动增长。对于ByteArrayInputStream也就是输入的是一个Byte数组
可以使用toByteArray()和toString()方法获取ByteArrayOutputStream输出流的数据
那么解读该工具类的深度拷贝方法:
创建一个字节输出流ByteArrayOutputSream bo,利用对象输出流,将序列化的(T类对象t)数据缓存到bo中,
接下来创建字节输入流(参数是byte数组,将byte数组的数据输入到输入流),对象输入流(字节序列--> T类)将oi输入到对象输入流,使用oi.readObject方法转换成Java对象T类