Java内存区域及对象的创建与回收

从第一天开始学习Java,就需要和JVM打交道,然而对其只知名称,不知内涵。此前一直觉得这块知识晦涩难懂,不想积极地去面对。但是JVM的知识又是每个通往高级Java程序员所必备的,因此正好趁这一阵辞职休息的时间,正面的和JVM刚一下。本文及近期后续JVM相关文章基本都是基于本人阅读的周志明所著《深入理解Java虚拟机-JVM高级特效与最佳实践》一书的读书笔记及心得体会,欢迎留言讨论。

本文主要内容是:Java内存区域相关概念,对象创建回收时的内存处理机制以及垃圾回收算法。

一、 Java内存区域

首先来看一张图。
Java内存区域及对象的创建与回收_第1张图片
如上图所示,Java虚拟机在运行Java程序时会把其所管理的内存划分为多个不同的数据区域,主要有程序计数器,虚拟机栈,本地方法栈,堆,方法区,运行时常量池。下面依次来说明各区域的作用。

程序计数器

当前线程执行的字节码文件的行号指示器。占用很小的内存空间。

由于cpu是不断的随机分配给各个线程使用,因此当一个线程执行其内的字节码文件到某一位置时,cpu切换分配给其他线程使用,这时程序计数器就会记录字节码文件执行到的位置,当下一次cpu分配给该线程时,就可以根据程序计数器记录的位置接着执行。由于每个线程自己都会维护这样的程序计数器,彼此互不干扰,因此程序计数器这样的内存区域就是“线程私有”的内存。

虚拟机栈

我们通常会把java内存区分为堆内存和栈内存,这里的栈内存就是指的虚拟机栈或者说是虚拟机栈中局部变量表部分。

局部变量表中存放了各种基本数据类型,引用数据类型和returnAddress类型。局部变量表所需的内存空间在编译期间完全分配,在进入方法后,方法运行期间,局部变量表的大小是不会改变的。

本地方法栈

与虚拟机栈的作用相类似。区别在于虚拟机栈为虚拟机执行java方法服务,本地方法栈为虚拟机使用到的native方法服务。

虚拟机管理的最大一块内存,被所有线程共享,在虚拟机启动时创建。堆内存唯一的作用是存放对象实例,所有的对象实例和数组都在此分配内存。但是随着JIT编译器的发展,所有对象在堆上分配内存也变得不是那么绝对。由于堆中存放的都是对象实例,因此堆内存也是垃圾收集器主要管理的内存区域。

方法区

方法区与堆内存一样,被所有线程所共享,用于存储已经被虚拟机加载的类的信息,常量,静态变量等。

运行时常量池

常量池属于方法区的一部分,存放的是编译期间生成的各种字面量和符号的引用,例如经常说的字符串的常量池,其实指的就是运行时常量池。为了减少字符串冗余的创建,JVM在内存中维护了一个字符串常量池。当通过字面量形式创建一个字符串时,JVM会首先根据创建字符串的内容去字符串常量池中查找是否已存在相同内容的字符串对象引用,如果有则将该引用返回,如果没有,则会创建一个字符串对象,并将其引用放入字符串常量池中,然后将该引用返回。这样一来就减少了很多不必要的字符串创建操作。

二、对象创建回收

对象创建

对象创建时的内存分配机制:

  • 指针碰撞:如果Java堆内存是绝对规整的,那么将会按照指针碰撞的方式进行分配。
  • 空闲列表:如果Java堆内存不是规整的,那么将会按照空闲列表方式进行分配。

两种分配机制是由堆内存是否规整决定的,那么Java堆内存是否规整,则是由垃圾收集器是否带有压缩整理功能所决定的。带有压缩整理功能的垃圾收集器,将会在对象回收后对堆内存进行压缩整理,因此得到规整的内存结构。

用下图来说明两种分配机制更为清晰。
Java内存区域及对象的创建与回收_第2张图片

对象回收

首先来看一个问题,如何判断一个对象是否存活?

在JVM中,有两种常见的算法来确定对象是否存活,引用计数器算法和可达性分析算法。

  • 引用计数器算法:为每个对象添加一个引用计数器,当有地方引用该对象时,引用计数器加1,当引用失效后,引用计数器减1。当引用计数器为0时,表示该对象没有被任何引用。但是主流的java虚拟机并没有采用这个算法,原因是它没有办法解决对象之间互相循环引用的问题。
  • 可达性分析算法:从一系列称之为GC Roots的对象开始向下搜索,搜索的路径称之为引用链,当一个对象和GC Roots之间没有任何引用链相连接时,则证明此对象是不可用的。

下面是可达性分析算法示意图。
Java内存区域及对象的创建与回收_第3张图片

两种算法都与引用相关,由此可见引用的重要性,那么这里再介绍一下Java中的引用类型,强引用,软引用,弱引用,虚引用。它们的引用强度依次递减,即强引用>软引用>弱引用>虚引用。

  • 强引用,即类似Object obj = new Object()的引用,只要强引用还存在,垃圾回收器就永远不会回收被引用的对象。
  • 软引用,描述的是还有用但是非必需的对象。软引用关联的对象,在内存即将溢出前,虚拟机会将这些对象列入回收范围进行第二次回收,如果这次回收还没有足够的内存,则抛出内存溢出异常。
  • 弱引用,也是描述的非必需的对象,只是引用的强度比软引用更弱。弱引用关联的对象只能生存到下一次垃圾回收之前,当垃圾回收时,无论内存是否足够,都将会回收对象。
  • 虚引用,也称之为幽灵引用或幻影引用,最弱的引用关系。无法通过虚引用来获取一个对象实例。

通过可达性分析算法分析后,那些没有引用的对象就会被列入到回收范围内,但是这些对象是否是一定会被回收呢?我们接着往下看。

即使是可达性分析算法不可达的对象,也并不是一定会被垃圾回收,这里将与finalize()方法有关系。

先说下finalize()方法的用处。当垃圾回收器回收对象时,会先调用对象的finalize()方法,用户可以在finalize()方法中做一些对象回收前最后的操作。但是基本上不推荐在finalize()中做一些类似于资源回收的操作,使用try-finally更为合适。

当被可达性分析算法分析,一个对象没有与GC Roots之间有任何引用链关系后,这个对象将被第一次标记并进行筛选,看它是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用了,虚拟机则将其视为没有必要执行finalize()方法。反之,如果有必要执行finalize()方法,那么虚拟机将会把该对象放入F-Queue的队列中,稍后垃圾回收器将会对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中重新与引用链上的任意一个对象建立关联,那么第二次标记时就会被移出即将回收的对象集合,从而逃逸成功。如果第二次标记时对象没有逃逸成功,那么它将会被垃圾回收器永久回收。

三、垃圾收集算法

  • 标记-清除算法
    • 标记出要回收的对象,在标记完成后统一回收所有被标记的对象。
    • 该算法有两个不足之处,一个是效率问题,因为标记和清除的过程效率都不高。另一个是空间问题,标记清除后会产生大量不连续的内存碎片,内存碎片过多可能导致后续需要分配较大内存时无法找到连续的足够大的内存,从而会提前触发一次垃圾回收操作。
  • 复制算法
    • 将整个内存分为大小相同的两块,然后每次使用一块内存。当这块内存不够使用时,将内存中还存活的对象复制到另一块内存区域上,然后将本内存上的空间全部清理掉。
    • 这样做的好处是实现简单且运行高效。但是内存只能使用一半却有点得不偿失。
  • 标记-整理算法
    • 与标记-清除算法类似,标记出要回收的对象,然后将依然存活的对象向内存的一端移动,最后将边界以外的内存清理干净。
  • 分代收集算法
    • 分代收集算法并没有什么新的思想,只是根据对象存活周期将内存划为新生代和老年代,再根据各个年代的特点采用最合适的收集算法。

你可能感兴趣的:(JVM,java虚拟机,java内存,java对象创建与回收,学习总结,Java虚拟机)