Java的内存回收

Java虚拟机内存原型

寄存器:我们在程序无法控制
:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是堆中
存取速度比堆块,仅次于寄存器,栈数据可以共享,栈的数据大小与生存期必须是确定的,缺乏灵活性。
:存放new产生的数据
可以动态分配内存大小,生存期也不必事先告诉编译器,因为它在运行时动态分配内存,Java的垃圾收集器会自动收走这些不再使用的数据,但缺点是,由于在运行时分配内存,存取速度较慢
静态域:存放在对象中用static定义的静态成员
常量池:存放常量(利用final关键字修饰的)
非RAM存储:硬盘等永久存储空间

Java引用的种类

>对象在内存中状态

对于JVM的垃圾回收机制来说,如果一个对象,没有一个引用指向它,那么它就被认为是一个垃圾。那该对象就会回收。可以把JVM内存中对象引用理解成一种有向图,把引用变量、对象都当成有向图的顶点,将引用关系当成图的有向边(注意:有向边总是从引用变量指向被引用的Java对象)
1、可达状态
当一个对象被创建后,有一个或一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可以通过引用变量来调用该对象的方法和属性。
2、可恢复状态
如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态,在这个状态下,系统的垃圾回收机制准备回收该对象所占用的内存空间,在回收该对象之前,系统会调用所有对象的finalize方法进行资源的清理,如果系统在调用finalize方法重新让一个引用变量引用该对象,则这个对象会再次变为激活状态,否则该 对象状进入不可达状态。
3、不可达状态
当对象与所有引用变量的关联都被切继,且系统已经调用所有对象的finalize方法依然没有该对象变成可达状态,那这个对象将永久性地失去引用,最后变成不可达状态,只有当一个对象处于不可达状态时统才会真正回收该对象所占有的资源。

*对象的状态转换图如下:
这里写图片描述

>强引用

强引用是Java编程中使用广泛的引用类型,被强引用所引用的Java对象绝不会被垃圾回收,当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题,因此强引用是造成Java内存泄漏的主要原因之一
*代码示例

class Person
{
    String name;
    int age;
    public Person(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
public class ReferenceTest {
     public static void main(String[] args) {
         //创建一个长度为10000的强引用数组,来保存10000个Person对象
         Person[] person=new Person[10000];
        //依次初始化
        for(int i=0;i<person.length;i++)
        {
            person[i]=new Person("名字"+i,(i+1)*2%100);
        }
        System.out.println(person[1]);
        System.out.println(person[3]);
        //通知系统进行垃圾回收
        System.gc();
        System.runFinalization();

        System.out.println(person[1]);
        System.out.println(person[3]);

    }
}

运行结果(结果说明内存还是足够的)
这里写图片描述
我们来把修改java虚拟机内存,把堆内存减少到2m
(操作:程序右键选属性->run/debug settings->选中应用程序->编辑->Arguments->VM arguments输入框输入 -Xmx2m -Xms2m )
这里写图片描述
再运行(程序因为内存不足而中止)
这里写图片描述

>软引用

软引用通过SoftReference类来实现,当系统内存空间足够,软引用的对象不会被系统回收,程序也可以使用该对象,当系统内存不足时,系统将会回收
*代码示例

        // 创建一个长度为10000的弱引用数组,来保存10000个Person对象
        SoftReference<Person>[] person = new SoftReference[10000];

        // 依次初始化
        for (int i = 0; i < person.length; i++) {
            person[i] = new SoftReference<Person>(new Person("名字" + i, (i + 1) * 2 % 100));
        }   

运行结果(结果说明内存足够的)
这里写图片描述
修改java虚拟机内存,把堆内存减少到2m,再运行
这里写图片描述
从运行结果可以看出,内存不足时,垃圾回收机制会回收SoftReference引用的对象

>弱引用

弱引用与软引用有点相似,区别是弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
*代码示例

    //创建一个字符串对象
        String str=new String("疯狂Java讲义");
        //创建一个弱引用,让该弱引用引用到“疯狂Java讲义”字符串对象
        WeakReference<String> wr=new WeakReference<String>(str);
        //切断str引用变量和“疯狂Java讲义”字符串对象之间的引用关系
        str=null;
        //取出弱引用所引用的对象
        System.out.println(wr.get());

        //强制垃圾回收
        System.gc();
        System.runFinalization();

        //再次取出弱引用所引用的对象
        System.out.println(wr.get());

内存分配图
这里写图片描述
运行结果
这里写图片描述
从运行结果看出,当系统垃圾回收机制启动后,弱引用的对象就会被清除掉。null表面该对象已经被清除了

>虚引用

虚引用通过PhantomReference类实现,类似没有引用,主要作用是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用的对象是否即将被回收,虚引用不能单独使用,必须要和引用队列(ReferenceQueue)联合使用
*代码示例

    //创建一个字符串对象
         String str=new String("这是虚引用");
         //创建一个引用队列
         ReferenceQueue<String> rq=new ReferenceQueue<String>();
         //创建一个虚引用,让该虚引用引用到“这是虚引用”字符串对象
         PhantomReference<String> pr=new PhantomReference<String>(str,rq);
         //切断str引用与"这是虚引用"字符串之间的引用
         str=null;
         //取出虚引用所引用的对象
         System.out.println(pr.get());

         //强制垃圾回收
         System.gc();
         System.runFinalization();

         //取出引用队列中最先进入队列中引用与pr进行比较
         System.out.println(rq.poll()==pr);

运行结果
这里写图片描述
从运行结果可以看出,在未强制进行垃圾回收,程序输出null,说明系统无法通过虚引用访问被引用的对象,当程序强制回收垃圾后,虚引用引用的对象被回收,然后该引用会添加到关联的引用队列中,所以输出true,所以说程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收


Java的内存泄漏

无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费,就是内存泄漏

1、静态变量引起内存泄露:
根据分代回收机制(后面有讲),JVM会将程序中obj引用变量存在Permanent代里,这导致Object对象一直有效,从而使obj引用的Object得不到回收
例:

 class Person
 {
      static Object obj=new Object();
 }

2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
例:

public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孙悟空","pwd2",26);
Person p3 = new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:"+set.size()+" 个元素!"); 
//结果:总共有:3 个元素!
p3.setAge(2); 
//修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); 
//此时remove不掉,造成内存泄漏

set.add(p3); 
//重新添加,居然添加成功
System.out.println("总共有:"+set.size()+" 个元素!"); 
//结果:总共有:4 个元素!
for (Person person : set)
{
   System.out.println(person);
}
}

3、监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

4、各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。

5、内部类和外部模块等的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:
public void registerMsg(Object b);
这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。
6、单例模式
单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,考虑下面的例子:

class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B类采用单例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
}

显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收。想象下如果A是个比较复杂的对象或者集合类型会发生什么情况


垃圾回收机制

垃圾回收的基本算法

标记压缩法

先从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清除未标记的对象,而是将所有的存活对象压缩到内存的一端之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

标记回收法

从“GC Roots”(GC Roots指垃圾收集器的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象)集合开始,将内存整个遍历一次,保留所有被GC Roots直接或者间接引用到的对象,而剩下的对象都当作垃圾对待并回收,这个算法需要中断进程内其他组件的执行并且可能产生碎片化

复制回收法

将内存分为大小相等的两部分(假设A、B两部分),每次呢只使用其中的一部分(这里我们假设为A区),等这部分用完了,这时候就将这里面还能活下来的对象复制到另一部分内存(这里设为B区)中,然后把A区中的剩下部分全部清理掉。这样内存碎片的问题就解决了
这里写图片描述

分代回收法

根据对象的生命周期将内存划分,然后进行分区管理,在Java虚拟机分代垃圾回收机制中,应用程序可用的堆空间可以分为年轻代老年代,年轻代有被分为Eden区,From区与To区
这里写图片描述
分代回收法更详细链接http://blog.csdn.net/sinat_36246371/article/details/52998505

>堆内存的分代回收

>与垃圾回收的附加选项

下面两个选项用于设置java虚拟机内存大小
-Xms :设置java虚拟机堆内存的最大容量如java -Xmx256m XxxClass
-Xms :设置java虚拟机堆内存的初始容量,如java -Xms128m XxxClass

下面选项都是关于java垃圾回收的附加选项
-xx:MinHeapFreeRatio =40 :设置java堆内存最小的空闲百分比,默认为40,如java -xx:MinHeapFreeRadio = 40 XxxClass

-xx:MaxHeapFreeRatio=70 :设置Java堆内存最大的空闲百分比,默认为70,如java -XX:MaxHeapFreeRatio =70 XxxClass

-xx:NewRatio=2 ;设置Yonng/Old内存的比例,如java -XX:NewRatio=1 XxxClass
-xx:NewSize=size:设置Yonng代内存的默认容量,如java -XX:Newsize=64m XxxClass

-xx:SurvivorRatio = 8;设置Yonng代中eden/survivor的比例,如java -xx:MaxNewSize=128m XxxClass

注意 当设置Young代的内存超过了-Xmx设置的大小时,Young设置的内存大小将不会起作用,JVM会自动将Young代内存设置为与-Xmx设置的大小相等。

-XX:PermSIze=size;设置Permnanent代内存的默认容量,如java –XX:PermSize=128m XxxClass

-XX:MaxPermSize=64m;设置Permanent代内存的最大容量,如java -XX:MaxPermSize=128m XxxClass

>常见垃圾回收器

  1. 串行回收器(Serial Garbage Collector)
    Serial Garbage Collector通过暂停所有应用的线程来工作。它是为单线程工作环境而设计的。它中使用一个线程来进行垃圾回收。这种暂停应用线程来进行垃圾回收的方式可能不太适应服务器环境。它最适合简单的命令行程序。
    通过 -XX:+UseSerialGC 参数来选用Serial Garbage Collector。
  2. Parallel Garbage Collector

Parallel Garbage Collector也被称为吞吐量收集器(throughput collector)。它是Java虚拟机的默认垃圾收集器。与Serial Garbage Collector不同,Parallel Garbage Collector使用多个线程进行垃圾回收。与Serial Garbage Collector相似的地方时,它也是暂停所有的应用线程来进行垃圾回收。
3. CMS Garbage Collector

Concurrent Mark Sweep (CMS) Garbage Collector使用多个线程来扫描堆内存来标记需要回收的实例,然后再清除被标记的实例。CMS Garbage Collector只有在如下两种情景才会暂停所有的应用线程:

当标记永久代内存空间中的对象时;
当进行垃圾回收时,堆内存同步发生了一些变化。

相比Parallel Garbage Collector,CMS Garbage Collector使用更多的CPU资源来确保应用有一个更好的吞吐量。如果分配更多的CPU资源可以获得更好的性能,那么CMS Garbage Collector是一个更好的选择,相比Parallel Garbage Collector。

通过 XX:+USeParNewGC 参数来选用CMS Garbage Collector。


内存管理的小技巧

>尽量使用直接量

当需要使用字符串,还有Byte,Short、integer、Long、Float、Double、Boolean、Character包装类的实例时,不应该采用new的方式来创建对象,而应该使用直接量来创建它们
应该是String str="hello";而不是String str=new String("hello");
后者除了在创建一个缓存在字符串缓冲池的“hello”字符串,str所引用的String对象底层还包含一个存放了h、e、l、l、o的char[ ]数组

>使用StringBuilder和StringBuffer进行字符串连接

String代表字符序列不可变的字符串,StringBuilderheStringBuffer都代表字符序列可变的字符串
建议

StringBuilder st = new StringBuilder();
c = st.append(a).append(b);

不建议

String c = a+b;

因为这样会在运行的时候生成大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降

如果使用少量的字符串操作,使用 (+运算符)连接字符串;
如果频繁的对大量字符串进行操作,则使用
1:全局变量或者需要多线程支持则使用StringBuffer;
2:局部变量或者单线程不涉及线程安全则使有StringBuilder

>尽早释放无用对象的引用

>尽量少用静态变量

>避免在经常调用的方法、循环中创建Java对象

>缓存经常使用的对象

>尽量不要使用finalize方法

>考虑使用SoftReference

参考:《疯狂java 突破程序员基本功的16课》

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java内存回收是由Java虚拟机(JVM)自动管理的,一般情况下无需手动控制。JVM使用垃圾回收器(Garbage Collector)来识别和回收不再使用的对象,释放内存空间。 虽然无法直接控制内存回收的具体时机,但可以通过一些技术手段来优化内存使用和垃圾回收的性能,包括: 1. 避免创建过多的临时对象:频繁地创建临时对象会增加垃圾回收的负担。可以尽量重用对象或使用可变对象,避免不必要的对象创建。 2. 及时释放资源:对于使用了外部资源(如文件、数据库连接等)的对象,在使用完后应及时手动释放资源,确保及时回收。 3. 注意对象的生命周期:确保对象在不再使用时被及时置为null,这样垃圾回收器可以识别并回收这些无用的对象。 4. 使用合适的数据结构和算法:选择适合应用场景的数据结构和算法,可以减少内存占用和垃圾回收的压力。 5. 调整JVM参数:可以通过调整JVM的参数来优化垃圾回收器的行为,例如堆大小、新生代和老年代的比例、并行或并发的垃圾回收等。不同的应用场景可能需要不同的参数配置。 需要注意的是,手动控制内存回收可能会导致不稳定性和性能问题,一般情况下,建议依赖JVM的自动内存管理机制。只有在特殊情况下,例如需要管理大量的本地资源或使用了大对象时,才需要考虑手动进行内存管理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值