Java学习笔记-《Java程序员面试宝典》-第四章基础知识-4.8 Java平台与内存管理(4.8.4-4.8.6)

本文详细介绍了Java中的垃圾回收机制,包括垃圾回收的概念、常见的垃圾回收算法及其优缺点,并探讨了Java中的内存泄漏问题以及如何避免内存泄漏。此外,还对比了Java中堆和栈的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

4.8.4 什么是GC

在Java语言中,垃圾回收(Garbage Collection,GC)的主要作用是回收程序中不再使用的内存。为了减轻开发人员的工作,同时增加系统的安全性和稳定性,Java语言提供了垃圾回收器来自动检测对象的作用域,可自动地把不再被使用的存储空间释放掉。具体而言,垃圾回收器要负责完成3项任务:分配内存、确保被引用对象的内存不被错误地回收以及回收不再被引用的对象的内存空间。
垃圾回收器的存在一方面把开发人员从释放内存的复杂工作中解脱出来,提高了开发人员的生产效率;另一方面,对开发人员屏蔽了内存的方法,可以避免因开发人员错误地操作内存而导致程序的奔溃,保证了程序的稳定性。但是,垃圾回收也带来了问题,为了实现垃圾回收,垃圾回收器必须跟踪内存的使用情况,释放没用的对象,在完成内存的释放后还需要处理堆中的碎片,这些操作必定会增加JVM的负担,从而降低程序的执行效率。
对对象而言,如果没有任何变量去引用它,那么该对象将不可能被程序访问,因此可以认为它是垃圾信息,可以被回收。只要有一个以上的变量引用该对象,该对象就不会被垃圾回收。
对于垃圾回收器来说,它使用有向图来记录和管理内存中的所有对象,通过这个有向图就可以识别那些对象是”可达的”(有引用变量引用他就是”可达的”),那些对象是”不可达的”(没有引用变量引用就是不可达的),所有”不可达”的对象都是可被垃圾回收的,示例如下:

public class Test{
    public static void main(String[] args){
        Integer i1 = new Integer(1);
        Integer i2 = new Integer(2);
        i2 = i1;
    }
}

上述代码在执行到i2 = i1后,内存的引用关系如下图:
这里写图片描述
此时,如果垃圾回收器正在进行垃圾回收操作,在遍历上述有向图时,资源2所占的内存是不可达的,垃圾回收器就会认为这块内存已经不会再被使用了,因此就会回收该块的内存。
垃圾回收都是依据一定的算法进行的,下面介绍其中几种常用的垃圾回收算法:
1>引用计数算法(Reference Counting Collector)
引用计数作为一种简单但是效率较低的方法,其主要原理如下:在堆中对每个对象都有一个引用计数器,当对象被引用时,引用计数器加1;当引用被置为空或离开作用域时,引用计数减1,由于这种方法无法解决相互引用的问题,因此JVM没有采用这个算法。
2>追踪回收算法(Tracing Collector)
追踪回收算法利用JVM维护的对象引用图,从根节点开始遍历对象的引用图,同时标记遍历到的对象。当遍历结束后,未被标记的对象就是目前已经不再被使用的对象,可以被回收了。
3>压缩回收算法(Compacting Collector)
压缩回收算法的主要思路如下:把堆中活动的对象移动到堆中一端,这样就会在堆中另外一端留出很大的一块空闲区域,相当于对堆中的碎片进行了处理。虽然这种方法可以大大简化消除堆碎片的工作,但是每次处理都会带来性能的损失。
4>复制回收算法(Coping Collector)
复制回收算法的主要思路如下:把堆分为两个大小相同的区域,在任何时刻,只有其中的一个区域被使用,直到这个区域被消耗完为止,此时垃圾回收器会中断程序的执行,通过遍历的方式把所有活动的对象复制到另外一个区域中,在复制的过程中它们是紧挨着布置的,从而可以消除内存碎片。当复制过程结束后程序会接着运行,知道这块区域被使用完,然后再采用上面的方法继续进行垃圾回收。
这个算法的优点是在进行垃圾回收的同时对对象的布置也进行了安排,从而消除了内存碎片。但是这也付出了很高的代价:对指定大小的堆来说,需要两倍大小的内存空间;同时由于在内存调整过程中要中断当前执行的程序,从而降低了程序的执行效率。
5>按代回收算法(Generational Collector)
复制回收算法的主要缺点如下:每次算法执行时,所有处于活动状态的对象都要被复制,这样效率很低。由于程序有“程序创建的大部分对象的生命周期都很短,只有部分对象有较长的生命周期”的特点,因此可以根据这个特点对算法进行优化,按代回收算法的主要思路如下:把堆分为两个或者多个子堆,每个子堆被视为一代。算法在运行的过程中优先收集那些”年幼”的对象,如果一个对象经过多次收集仍然”存活”,那么就可以把这个对象转移到高一级的堆里,减少对其的扫描次数。

引申:
参考网址: http://www.cnblogs.com/qiyu/p/3349667.html

Java垃圾收集–对象的finalize()方法

根搜索算法:这个算法的基本思路是通过一系列名为“GC roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用连,当一个对象到GC roots没有任何引用链,则证明对象是不可用的。将被判定为可回收对象。不可达对象要被垃圾回收,至少要经历两次标记过程。第一次标记时执行finalize()方法,并做记号,第二次标记则不会再执行finalize()方法了。执行finalize()方法后,对象可能又变为可达对象,逃脱被垃圾回收的命运。

4.8.5Java是否存在内存泄漏问题

内存泄漏是指一个不再被程序使用的对象或变量还在内存中占有存储空间。
Java语言中引进了垃圾回收机制,由垃圾回收器负责回收不再使用的对象,既然有垃圾回收器来负责回收垃圾,那么是否还会存在内存泄漏的问题呢?
其实,在Java语言中,判断一个内存空间是否符合垃圾回收的标准有两个:第一、给对象赋予了空值null,以后再没有使用过;第二、给对象赋予了新值,重新分配了内存空间。一般来讲,内存泄漏主要有两种情况:一是堆中申请的空间没有被释放;二是对象已不再被使用,但是还仍然在内存中保留着。垃圾回收机制的引入可以有效的解决第一种情况;而对于第二种情况,垃圾回收机制则无法保证不再使用的对象会被释放。因此,Java语言中的内存泄漏主要指的是第二种情况。示例如下:

Vector v = new Vector(10);
for(int i=1;i < 10;i++){
    Object o = new Object();
    v.add(o);
}

在上述例子的循环中,不断创建新的对象加到Vector对象中,当退出循环后,o的作用域将会结束,但是由于v在使用这些对象,因此垃圾回收器无法将其回收,此时就造成了内存泄漏。只有将这些对象从Vector中删除才能释放创建的这些对象。
在Java语言中,容易引起内存泄露的原因很多,主要有以下几个方面的内容:
1>静态集合类。例如HashMap和Vector。如果这些容器为静态的,由于它们的生命周期与程序一致,那么容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏,如上例所示。
2>各种连接,例如数据库连接、网络连接以及IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显式地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
3>监听器。在Java语言中,往往会使用到监听器。通常一个应用中会用到多个监听器,但是在释放对象的同时往往没有相应的删除监听器,这也可能导致内存泄露。
4>变量不合理的作用域。一般而言,如果一个变量定义的作用范围大于其使用范围,很有可能会造成内存泄漏,另一方面,如果没有及时地把对象设置为null,很有可能会导致内存泄漏的发生,示例如下:

class Server{
    private String msg;
    public void receiverMsg(){
        readFromNet();  //从网络接收数据保存到msg中
        saveDB();       //把msg保存到数据库中
    }
}

在上述伪代码中,通过readFromNet()方法接受的消息保存在变量msg中,然后调用seveDB()方法把msg的内容保存到数据库中,此时msg已经没用了,但是由于msg的生命周期与对象的生命周期相同,此时的msg还不能被回收,因此造成了内存泄漏。对于这个问题,有如下两种解决方法:第一种,由于msg的作用范围只在receieveMsg()方法内,因此可以把msg定义为这个方法的局部变量,当方法结束后,msg的生命周期就会结束。此时垃圾回收器就可以回收msg的内存了;第二种方法,在使用完msg后,就把msg设置为null,这样垃圾回收器也会自动回收msg内容所占的内存空间。
5>单例模式可能会造成内存泄漏。单例模式的实现方法有很多种,下例所使用的单例模式就可能会造成内存泄漏:

class BigClass{
    //class body
}
class Singleton{
    private BigClass bc;
    private statuc Singletom instance = new Singleton(new BigClass());
    private Singleton(BigClass bc){this.bc = bc;}
    public Singleton getInstance(){
        return instance;
    }
}

在上述实现的单例模式中,Singleton存在一个对对象BigClass的引用,由于单例对象以静态变量的方式存储,因此它在JVM的整个生命周期中都存在,同时由于单例对象有一个对对象BigClass的引用,这样会导致BigClass类的对象不能被回收。

4.8.6 Java中的堆和栈有什么区别

在Java语言中,堆和栈都是内存中存放数据的地方。变量分为基本数据类型和引用类型,基本数据类型的变量(例如byte,boolean,short,char,int,long,float,double)以及对象的引用变量,都分配在栈上,变量出了作用域就会自动释放;而引用类型的变量,其内存分配在堆上或者常量池(例如字符常量池和基本类型常量)中,需要通过new等方式进行创建。
具体而言,栈内存主要用来存放基本数据类型与引用变量。栈内存的管理是通过压栈和弹栈操作来完成的,以栈帧为基本单位来管理程序的调用关系,每当有函数调用时,都会通过压栈方式来创建新的栈帧,每当函数调用结束后都会通过弹栈的方式释放栈帧。
堆内存用来存放运行时创建的对象。一般来讲,通过new关键字创建出来的对象都存放在堆内存中。由于JVM是基于堆栈的虚拟机,而每个Java程序都运行在一个单独的JVM实例上,每一个实例唯一对应一个堆,一个Java程序内的多个线程也就运行在同一个JVM实例上,因此这些线程之间会共享堆内存,鉴于此,多线程在访问堆中的数据时需要对数据进行同步。
在Java语言中,堆内存的释放工作由垃圾回收器来负责执行,开发人员只需要申请所需的对空间而不需要考虑释放的问题。
在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。这就是Java中引用的用法。
从堆和栈的功能及作用来比较,堆主要用来存放对象的,栈主要用来执行程序的。相比较于堆,栈的存取速度更快,但栈的大小个生存期必须是确定的,因此缺乏一定的灵活性。而堆却可以在运行时动态的分配内存,生存期不用提前告诉编译器,但也导致了其存取速度的缓慢。
实例如下:

class Rectangle{
    private int width;
    private int length;
    public Rectangle(int width,int length){
        this.width = width;
        this.length = length;
    }
    public class Test{
        public static void main(String[] args){
            int i=1;
            Rectangle r = new Rectangle(3,5)
        }
    }
}

在上述程序进入main()方法后,数据的存储关系如图所示:
这里写图片描述
由于i为基本数据类型的局部变量,因此它存储在栈空间中,而r为对象的引用变量,因此也被存储在栈空间中;实际的对象存储在对空间中,当main()方法退出后,存储在栈中的i和r通过压栈和弹栈操作将会在栈中被收回,而存储在堆中的对象将会由垃圾回收器来自动回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值