java内存管理和垃圾回收机制

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010223904/article/details/48038815

我们知道,java所谓的跨平台特性(hardware- and operating system-independence)是依赖于JVM的。也就是,JAVA代码写好了之后会被编译成java的字节,一般存放为class文件,之后由JVM负责解释执行。

The Java Virtual Machine knows nothing of the Java programming language, only of a particular binary format, the class file format. A class file contains Java Virtual Machine instructions (or bytecodes) and a symbol table, as well as other ancillary information. (引用自JVMS)

java代码的执行依赖于JVM,这里我们了解一下JVM的内存管理机制和垃圾回收机制。

JAVA中的内存分区

编程语言会将内存分为不同的区域,具有不同的特性。学C语言的时候,一般是说分成堆和栈两个区域。
在方法中定义的基本类型,比如int, double这些,是存放在栈上的。方法的调用层次的组织也是用栈实现的。这里的栈和数据结构里的栈类似,因为方法的调用也是后进先出。方法的调用返回通过栈的伸缩可以很容易实现。所以这些栈上的变量会随着方法栈的收缩消亡。因为栈有大小限制,一般不放很大的内容,所以使用malloc函数申请大块内存,这时候这些内存是在堆上的。
当然,C中的内分区也并不是那么简单。比如还有常量区等等。

JAVA中则分为更多块。根据java® Virtual Machine Specification的说明,有这几个部分:

The pc Register
Java Virtual Machine Stacks
Heap
Method Area
Run-Time Constant Pool
Native Method Stacks

以下内容参考Java Virtual Machine Specification

JAVA虚拟机栈

虚拟机栈类似C中的方法栈,用来管理方法的调用,每个被调用的方法在在其中表示为一个栈帧。栈帧中保存了本地变量表,返回地址等等。
每个线程有自己独立的虚拟机栈。它的生命周期和线程同步,当线程创建的时候被创建,线程销毁的时候被销毁。
java中的对象需要通过new关键字得到,所有的对象实例都存放在堆上。当然,引用和基本类型是存放在栈中的。

Object o = new Object();

此时o这个引用是存放在栈中的,而真正的Object对象是存放在堆上的。
栈的特点是访问快,而且栈帧的大小是在编译期就确定了的。

本地方法栈

因为java可以调用其他语言比如C语言的代码,当执行本地方法的时候,栈帧就存在于本地方法栈中而不是虚拟机栈中。此时程序计数区里的数据为undefined

堆是JAVA很重要的区域,因为所有的new出来的instance和array都在堆上。堆又可以分为几个部分。
堆被所有线程共享。生命周期和JVM同步。

Method Area

It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

属于堆的一部分。可能会有垃圾回收可能没有。jvm初始化的时候既初始化。
用于存放已被虚拟机加载的类信息,常量,静态变量,即时编译期编译过后的代码等数据。
在上面进行的垃圾回收主要是针对常量池的回收和对类型的卸载。

Run-Time Constant Pool

常量区存放一些编译期就确定的常量。
其实是Method Area的一部分。

程序计数区

标示当前程序运行到的位置。JVM相当于一台抽象的计算机,感觉这个区域类似于真正的CPU中的PC寄存器。
当程序当前运行到正常的java方法的时候,这里标识的是the address of the Java Virtual Machine instruction currently being executed,当当前运行到本地方法的时候,这里的值是undefined

内存回收机制

和C/C++比起来,java的一个优势是,有自动的垃圾回收机制(garbage collection)。在C/C++中,任何malloc出来的值都要在合适的时候free,任何new出来的值都要在合适的时候delete,不然就容易发生内存泄露。而在JAVA里面则不同,JVM有一个垃圾管理机制,也就是有一个垃圾处理的线程,在某些时候会被执行,把不用的东西从内存中清理掉,避免了内存泄露。一般情况下,java程序员不需要去手动地释放内存,不用的内存不管它就是了。

垃圾收集的几种方法

  • mark & swap
  • 引用计数
  • 分代收集
  • stop & copy

垃圾回收要解决的问题大概可以归为以下三个:

  • 判断什么是垃圾(需要被回收)?
    一般来说,把没有引用指向的对象当做是可被回收的垃圾。因为没有对象指向它,也就无法对它进行操作,这个对象对于我们来说是没用的了,也就是所谓的垃圾。
  • 什么时候回收?
    内存不足或者当前空闲的时候。一般来说,gc线程的优先级都不太高。
  • 如何进行回收?
    有多种实现方案。

mark & sweep

先标记出哪些不是垃圾,回收的时候把没有被标记到的认为是垃圾,进行回收。
标记的方法是,从一个rootSet出发,能被里面某个引用指向的对象就认为是可达的,也就是不是垃圾。可达性是可以传递的,被认为是可达的对象指向的对象也被认为是可达的。
一般来说,rootset里的初始的引用可以是栈中的临时变量,static类型的常量,寄存器中内容等等,因为这些内容一定是可达的。

这种方法可能会导致后面内存中有很多碎片。
CSAPP中讲到这种方法的问题还在于判断某些内存中的数据到底是一个引用还是单纯的数值。java具备能够分辨出引用还是不是引用的能力,所以java的GC叫做准确式GC。

引用计数

针对每个对象对象维护一个表示当前有多少个对象指向这个引用的值。实时更新这个值。也就是另某个引用指向它时引用加一,反之减一。值为0也就表示这个对象不可达。
这种的弊端是可能会因为循环引用导致某些有应该被回收的对象的引用计数不为0,所以无法被回收。
比如说A持有B的引用,B也持有A的引用,但是除此之外没有指向A和B的引用,程序根本就无法操作A和B,所以他们应该被回收的,但是并没有。

stop & copy

这种方法和前面不同的主要是清理的方式。
把堆分成两部分,每个时刻其实只有一个部分起到充当堆内存的作用,进行garbage colloction的时候,把当前作为堆内存部分的不是垃圾的内容copy到另一个部分,然后再把这个部分作为堆来使用就行、

这种方法的缺点是,对内存的利用不够,有一部分是用不到的。而且copy的时候,之前的引用的要进行修正。

java中的实现

因为大部分对象的生命周期并不长久,而少部分的对象又可以活得很久,所以可以把堆分为不同的区域,分别存放存活时间短的和存活时间长的对象,分别执行不同的策略。
把堆分为年轻代,老年代。年轻代上存放经历garbage collection次数较少的对象,老年代上存放经历过较多次garbage collection的对象。
年轻代分为三个部分,分别是eden,toSurvival,fromSurvival。每个时刻eden加上toSurvival,fromSurvival中的一个区域作为用作分配的区域,另一个Survival区域则是用于发生gc的时候使用。

当发生垃圾回收的时候,根据上面的说法,年轻代中只有少数对象是还存活着的。那么这时候采用stop-copy算法就比较高效,因为只需要比较少的复制操作。年轻代上的garbage collectio叫做minor gc。

当需要在老年代上进行gc操作的时候,采取的是标记算法。老年代上的gc叫做major gc。一般比新生代上的gc费时得多。

rootset包括虚拟机栈中引用的对象,方法区中类静态属性引用的对象,本地区中常量引用的对象,本地方法栈中JNI引用的对象
java使用了一组叫做OopMap的数据结构来存储rootser,但是执行每条指令都更新这个数据,那就太麻烦了。不在每次执行指令的时候更新,为了避免在枚举可达性的过程中可达性不同步,在某个特定的点更新,然后gc只会在等待所有线程都进行到这个点的时候执行。

finalize()

当gc决定要回收一个对象的内存的时候,就会去调用这个对象的finalize()函数(如果这个类重写了finalize函数并且没有被执行过),然后在下一次gc过程中再去真正回收这个对象。
finalize函数被用来做一些清理的工作,java几乎所有对象都是通过new方法在堆上分配的,这些内存可以正确地被jvm回收,但是如果调用了本地方法,比如说用到了C里面的malloc,那么就需要在finilize函数中正确地释放这部分内存了。
考虑下面的代码(摘自《深入理解java虚拟机》)

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive(){
        System.out.println("yes,i am still alive!");
    }

    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead!");
        }
        // 下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead!");
        }
    }
}

这段代码验证了finalize函数只会被执行一次

各种引用

  • 强引用
    我们平时使用的类似Object o = new Object(),这里o就是一个强引用。前面提到的判断是否应该被回收的问题,被强引用引用的对象就被认为是可达的,就算发生OutOfMemonyError也不会回收它。
  • 软引用
    如果有一些对象我们可能会用到,但是又不想因为它们发生OutOfMemonyError,那么就把指向它们的引用设为软引用。当内存不足要进行gc的时候,只被软引用引用的对象是可以被回收的。
    比如SoftReference<Object> weakRef = SoftReference<Object>(new Object())
    在设计缓存的时候,会经常会有用到软应用的需求。
  • 弱引用
    类似软引用,但是只被弱引用指向的对象更有可能被回收。
    只被弱引用引用对象一旦被垃圾处理器线程发现就会被回收。

猜你喜欢

转载自blog.csdn.net/u010223904/article/details/48038815
今日推荐