Java内存模型

来源:互联网 发布:舰娘 知乎 编辑:程序博客网 时间:2024/06/14 10:51

0.前言

本篇文章是Thinking in Java和深入理解JAVA虚拟机的读书笔记,除了原文笔记的整理之外还会加上其他自己的见解。

1.Java中存储数据的五个地方

先了解Java中数据可以在哪些地方存在,之后我们便能够更好地理解Java的内存模型。

1.(非内存)寄存器

它位于处理器内部,因此是最快的存储区。寄存器的数量极其有限,需要根据需求进行分配,程序员不能直接控制,可以说,寄存器对程序员而言是透明的。

2.堆栈

位于通用RAM(随机访问存储器)中,但通过堆栈指针可以从处理器那里获得直接支持。堆栈指针向下移动则分配新的内存,堆栈指针向上移动则释放那些内存。这是仅次于寄存器的快速有效的分配存储方法。创建程序时,Java系统必须知道存储在堆栈内所有项确切的生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些Java数据(如对象引用)存储于堆栈中,但Java对象并不存储于其中。

3.堆

也是通用的内存池(也位于RAM区),用于存放所有的Java对象。堆与堆栈不同之处在于,编译器不需要知道存储的数据在堆里存活多少时间,因此在堆里分配存储有很大的灵活性。需要对象时只需用new写段代码,当执行这行代码时,会自动在堆里进行存储的分配。这种灵活性带来的缺点就是,用堆进行存储分配和清理可能比堆栈进行存储分配需要更多的时间。

4.常量存储

常量值通常放在程序代码内部。在嵌入式系统中,常量本身会和其他部分隔离开,所以在这种情况下可以选择将其存放在ROM(只读存储器)中。

5.非RAM存储

如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。例如:流对象、持久化对象。在流对象中,对象转化为字节流被发送给另一台机器。而在持久化对象中,对象被存放于磁盘上,即使程序终止它们仍可以保持自己的状态。

对于基本类型,不用new来创建变量,而是创建一个并非是引用的“自动”变量,它直接在堆栈中存储“值”,使得更加高效。

2.Java内存区域划分

Java的虚拟机有着自动内存管理机制,在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

这里写图片描述

1.程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程锁执行的字节码的行号指示器。在虚拟机的概念模型中,字节码解释器通过改变计数器的值来选取下一条要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器来完成。每条线程都需要一个独立的程序计数器,这是“线程私有”的内存区域。

2.Java虚拟机栈

Java虚拟机栈是线程私有的,生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法开始调用时入栈,返回时出栈。局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)

3.本地方法栈

本地方法栈与虚拟机栈十分类似,区别在于虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

4.Java堆

对大多数应用而言Java堆是Java虚拟机所管理的内存中的最大的一块,同时被所有线程共享。几乎所有的对象实例以及数组都在这里分配内存。同时,Java堆也是垃圾收集器管理的主要区域(因此被称为GC堆),大部分收集器都用分代收集算法。Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可

5.方法区

和Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(在Java虚拟机规范中把方法区描述为堆的一个逻辑部分,但有一个叫做Non-Heap的别名,应该是为了与Java堆区分开来),垃圾收集的行为在这里很少出现
-运行时常量池:方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用来存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量值相对于Class文件常量池的另一个重要特征是具备动态性。

3.垃圾回收机制

1.finalize()方法

工作原理(假定):一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize(),并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
需要注意:
①对象可能不被垃圾回收器回收
②垃圾回收不等于“析构”
③垃圾回收只与内存有关
finalize()可以用于对象终结条件的验证

2.垃圾回收器如何工作

1.如何判断对象已死?

①引用计数算法(几乎没有在用的)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能再被使用的。
优点:简单、判定效率高
缺点:很难解决对象之间相互循环引用的问题
②可达性分析算法(Java C# Lisp)
通过一系列的,称为”GC Roots“的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到GC Roots没有任何引用链相连(用图论来说就是从GC Roots到这个对象不可达),则此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
a.虚拟机栈(栈帧中的本地变量表)中引用的对象。
b.方法区中类静态属性引用的对象。
c.方法区中常量引用的对象。
d.本地方法栈中JNI(即Native方法)引用的对象

2.垃圾收集算法

①标记-清除算法(Mark-Sweep)
最基础的收集算法。算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
主要不足:效率太低;空间问题:标记清除后会产生大量不连续内存碎片,可能导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
②复制算法(Copying)
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完就将还活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点:实现简单,运行高效
缺点:内存缩小为原来的一半(配合内存区域分代来实行)
③标记-整理算法(Mark-Compact)
其中的标记过程和“标记-清楚”算法一样,但后续步骤是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
④分代收集算法(Generational Collection)
当前商业虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存划分为几块。一般将Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高、没有额外空间进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

  • 新生代中的回收:据研究表明,新生代中98%的对象是“朝生夕死”的,因此将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将这两块区域中还活着的对象一次性地复制到另一块Survivor空间上,最后清理掉自身。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1.如果Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保
  • 分配担保:如果另一块Survivor空间没有足够空间存放上一次新生代手机下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

3.引用类型与垃圾回收

JDK1.2之后,Java对引用的概念进行了扩充,将引用分为四类:
1.强引用:在程序代码之中普遍存在的,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
2.软引用(SoftReference):还有用、但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
目的:实现内存敏感的高速缓存
3.弱引用(WeakReference):同样是描述非必需的对象,但是强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉纸杯弱引用关联的对象。
目的:为了”规范映射“(canonicalizing mappings),不妨碍垃圾回收期回收映射的键或值,规范映射中的对象的实例可以在程序的多处被同时使用,以节省存储空间。
4.虚引用(PhantomReference):最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
目的:能在这个对象被收集器回收时收到一个系统通知,可以调度回收前的清理工作,比Java终止机制更灵活。
如果想继续持有对某个对象的引用,既希望能够访问到它,也希望能够被垃圾回收器回收,这时就应该使用Reference对象。

0 0
原创粉丝点击