深入理解Java虚拟机二:垃圾收集与内存分配

垃圾收集:垃圾收集要完成三件事,包括哪些内存需要回收,什么时候回收及如何回收。

1、需要回收的内存判定:没有引用指向原先分配给某个对象的内存时,则该内存是需要回收的垃圾

        Java垃圾收集器在对内存进行回收之前,首先就是要确定这些对象哪些已经“死去”,对已经“死去”的对象进行内存回收。

目前,确定对象是否存活的主流算法有:

1)引用计数算法:

  所谓引用计数算法,即给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。引用计数算法的实现简单,判定效率也高,在大部分情况下它都是一个不错的算法。但是,Java语言没有采用引用计数算法来管理内存,主要原因是它很难解决对象之间的相互循环引用的问题。

  举个例子:对象objA和objB都有字段instance,赋值令objA.instance = objB及objB.instance = objA,除此之外,这两个对象再无任何引用;然后将objA和objB都置为null,这两个对象已经不可能再被访问,但是他们因为互相引用着对方,导致他们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

2)根搜索算法:定义一系列名为GC Roots的对象作为起点,从起点向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连,则说明该对象不可用,这时Java虚拟机可以对这些对象进行回收。

  Java虚拟机将以下对象定义为 GC Roots :

  • Java虚拟机栈中引用的对象:比如方法里面定义这种局部变量 User user= new User();
  • 静态属性引用的对象:比如 private static User user = new User();
  • 常量引用的对象:比如 private static final  User user = new User();
  • 本地方法栈中引用的对象

2、回收垃圾:根搜索算法中,不可达到的对象,也并非马上对该内存进行回收,会被第一次标记。一个对象的死亡,至少要经过两次标记:第一次标记,会对对象进行一次筛选,筛选的条件是对象是否有必要执行finalize方法,如果对象没有覆盖finalize方法或者已经被虚拟机调用过,则会被虚拟机视为“没有必要执行”。如果对象覆盖了finalize方法且没有被虚拟机调用过,那么该对象就会被放入到一个队列F-Queue,随后会有一个低优先级的Finalizer线程去执行。只要对象在finalize方法重新与引用链上的任何一个对象建立连接,那这个对象将会被移出“即将回收”的集合。但是,如果这个不可达到的对象再一次被标记,由于finalize方法被虚拟机调用过,则这个对象就真的离死不远了。不过,建议尽量避免使用finalize方法来拯救对象,因为使用try-finally或其他的方式可以做的更好、更及时。

垃圾回收算法(简单介绍下,想深入了解的可以网上查资料):

1)标记-清除算法:这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记出所有需要被回收的对象,然后回收被标记的对象所占用的空间。

2)复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

3)标记-整理算法:该算法标记阶段标记-清除算法一样,但在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

4)分代收集算法:核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域,一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

内存分配:引用尚学堂马士兵老师的J2SE课件并加上注释,直观明了,具体如下:

1)JVM自动寻找main方法,执行第一句代码,创建一个Test类的实例,在栈中分配一块内存,存放一个指向堆区对象的指针110925

2)创建一个int型的变量date,由于是基本类型,直接在栈中存放date对应的值9

3)创建两个BirthDate类的实例d1、d2,在栈中分别存放了对应的指针指向各自的对象。他们在实例化时调用了有参数的构造方法,因此对象中有自定义初始值

个人觉得,了解了这些后,就基本掌握了内存分配的思想了,无非就是两种类型的变量:基本类型和引用类型。二者作为局部变量,都放在栈中,基本类型直接在栈中保存值,引用类型只保存一个指向堆区的指针,真正的对象在堆里。作为参数时基本类型就直接传值,引用类型传指针。至于后面代码内存分配的问题了,这里就不一一讲解了。

小结:

1.分清什么是实例什么是对象。Class a = new Class();此时a叫实例,而不能说a是对象。实例在栈中,对象在堆中,操作实例实际上是通过实例的指针间接操作对象。多个实例可以指向同一个对象。

2.栈中的数据和堆中的数据销毁并不是同步的。方法一旦结束,栈中的局部变量立即销毁,但是堆中对象不一定销毁。因为可能有其他变量也指向了这个对象,直到栈中没有变量指向堆中的对象时,它才销毁,而且还不是马上销毁,要等垃圾回收扫描时才可以被销毁。

3.以上的栈、堆、代码段、数据段等等都是相对于应用程序而言的。每一个应用程序都对应唯一的一个JVM实例,每一个JVM实例都有自己的内存区域,互不影响。并且这些内存区域是所有线程共享的。这里提到的栈和堆都是整体上的概念,这些堆栈还可以细分。

4.类的成员变量在不同对象中各不相同,都有自己的存储空间(成员变量在堆中的对象中)。而类的方法却是该类的所有对象共享的,只有一套,对象使用方法的时候方法才被压入栈,方法不使用则不占用内存。

以上分析只涉及了栈和堆,还有一个非常重要的内存区域:常量池。这个地方往往出现一些莫名其妙的问题,鉴于篇幅以及个人水平问题,这里就不再啰嗦了,有兴趣了解的,推荐博客:http://www.cnblogs.com/wenfeng762/archive/2011/08/14/2137820.html,个人觉得解释的非常到位。

有问题欢迎留言。

 



 

posted @ 2017-11-17 09:05  wuzhiyuan  阅读(209)  评论(0编辑  收藏  举报