Java内存模型 详解

Java内存模型概念 Java平台自动集成线程以及多处理技术。内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,这点没有错,但是编译器、运行库、处理器或者系统缓存可以有特权在变量指定内存位置存储或者取出变量的值。JMM(Java Memory Model)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用 final 或 synchronized明确请求了某些可见性

(1) JSR133 JSR33的目的是修复JMM的一些缺陷而提出的,其本身的目标有以下几个:     <1> 保留目前JVM的安全保证,以进行类型的安全检查: 提供( out-of-thin-airsafety) 无中生有安全性,这样“正确同步的”应该被正式 而且直观地定义     <2> 程序员要有信心开发多线程程序,当然没有其他办法使得并发程序变得很容易 开发,但是该规范的发布主要目标是为减轻程序员理解内存模型中的一些细 节负担 提供大范围的流行硬件体系结构上的高性能 JVM实现,现在的处理器在它们的 内存模型上有着很大的不同, JMM应该能够适合于实际的尽可能多的体系结构 ,而不以性能为代价,这也是Java跨平台型设计的基础     <3> 提供一个同步的习惯用法,以允许发布一个对象使他不用同步就可见,这种情 况又称为初始化安全( initialization safety) 的新的安全保证 对现有代码应该只有最小限度的影

(2) 同步 同步:同步就是在发出一个功能调用的时候,在没有得到响应之前,该调用就不返回          如:有一个线程A,在A执行的过程中,可 能需要B提供一些相关的执行数据,当然触发B响应的就是A向B发送一个 请求或者说对B进行一个调用操作,如果A在执行该操作的时候是同步的方 式,那么A就会停留在这个位置等待B给一个响应消息,在B没有任何响应消 息回来的时候, A不能做其他事情,只能等待,那么这样的情况, A的操作就 是一个同步的简单说

异步: 异步就是在发出一个功能调用的时候, 不需要等待响应,继续进行 它该做的事情,一旦得到响应了过后给予一定的处理          如:有一个线程A,在A执行的过程中,同样需要B提供一 些相关数据或者操作,当A向B发送一个请求或者对B进行调用操作过后, A 不需要继续等待,而是执行A自己应该做的事情,一旦B有了响应过后会通知A, A接受到该异步请求的响应的时候会进行相关的处理,这种情况下 A的操 作就是一个简单的异步操

(3) 可见性、可排序性 synchronized关键字:强制实施一个线 程之间的互斥锁( 相互排斥) ,该互斥锁防止每次有多个线程进入一个给定监 控器所保护的同步语句块,也就是说在该情况下,执行程序代码所独有的某些 内存是独占模式, 其他的线程是不能针对它执行过程所独占的内存进行访问的,这种情况称为该内存
不可见

JVM 在处理该强制实施的时候可以提供一些内存的
可见规则,在该规则里面,它确保当存在一个同步块时,缓存被更新,当输入一个同步块时,缓存失效

JVM 内部提供给定监控器保护的同步块之中, 一个线程所写入的值对于其余所有的执行由同一个监控器保护的同步块线程来 说是可见的,这就是一个简单的可见性的描述。这种机器保证编译器不会把指 令从一个同步块的内部移到外部,虽然有时候它会把指令由外部移动到内部。 JMM 在缺省情况下不做这样的保证:只要有多个线程访问相同变量时必须使 用同步

可见性就是在多核或者多线程运行过程中内存的一种共享模式,在 JMM  模型里面,通过并发线程修改变量值的时候,必须将线程变量同步回主存过 后,其他线程才可能访问
注:换句话说,内存的可见性使内存资源可以共享,当一个线程执行的时候 它所占有的内存,如果它占有的内存资源是可见的,那么这时候其他线程在一 定规则内是可以访问该内存资源的,这种规则是由 JMM 内部定义的,这种情 况下内存的该特性称为其可见

可排序性提供了内存内部的访问顺序,在不同的程序针对不同的内存块进 行访问的时候,其访问不是无序的             如:有一个内存块, A 和 B 需要访问的时 候, JMM会提供一定的内存分配策略有序地分配它们使用的内存,而在内存的 调用过程也会变得有序地进行,内存的折中性质可以简单理解为有序性。而在 Java多线程程序里面, JMM 通过 Java关键字 volatile来保证内存的有序访问

Java内存模型结构 JVM中存在一个主存区(Main Memory 或 Java Heap Memory),Java中所有变量都是存在主存中的,对于所有线程进行共享,而每个线程又存在自己的工作内存(Working Memory),工作内存中保 存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而 是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传 递,是依赖主存来完成的。而在多核处理器下,大部分数据存储在高速缓存中,如果高速缓存不经过内存的时候,也是不可见的一种表现
《Java内存模型 详解》

Java 通过 synchronized 和 volatile关键字 提供的内存中模型的可见性保证 程序使用一个特殊的、存储关卡( memorybarrier) 的指令,来刷新缓存,使 缓存无效,刷新硬件的写缓存并且延迟执行的传递过程,无疑该机制会对 Java 程序的性能产生一定的影响
《Java内存模型 详解》

工作内存储了Java的一 些值,该模型保证了Java里面的属性、方法、字段存在一定的数学特性,按照 该特性,该模型存储了对应的一些内容,并且针对这些内容进行了一定的序列 化以及存储排序操作,这样使得 Java对象 在工作内存里面被 JVM顺利调用

内存的管理策略 1> 原子性(Atomicity): 这一点说明了该模型定义的规则针对原子级别的内容存在独立的影响,对 于模型设计最初,这些规则需要说明的仅仅是最简单的读取和存储单元写入的 一些操作,这种原子级别的包括——实例、静态变量、数组元素,只是在该 规则中不包括方法中的局部变量      获得或者初始化该值或 某一些值的时候,这些值是由其他线程写入,而且不是从两个或者多个线程产 生的数据在同一时间戳混合写入的时候,该字段的原子性在JVM内部是必须得 到保证的。也就是说JMM在定义JVM原子性的时候,只要在该规则不违反的 条件下, JVM本身不去理睬该数据的值是来自于什么线程,

2> 可见性(Visibility):在该规则的约束下,定义了一个线程在哪种情况下可以访问另外一个线程 或者影响另外一个线程,从JVM的操作上讲包括了从另外一个线程的可见区域 读取相关数据以及将数据写入到另外一个线程内      当一个线程需要修改另外线程的可见单元的时候必须遵循以下原则: 一个写入线程释放的同步锁和紧随其后进行读取的读线程的同步锁是同一个 从本质上讲,释放锁操作强迫它的隶属线程【 释放锁的线程】 从工作内存中的 写入缓存里面刷新(专业上讲这里不应该是刷新,可以理解为提供)数据 ( flush操作),然后获取锁操作使得另外一个线程【 获得锁的线程】 直接读取 前一个线程可访问域(也就是可见区域) 的字段的值。因为该锁内部提供了一 个同步方法或者同步块,该同步内容具有线程排他性, 这样就使得上边两个操 作只能针对单一线程在同步内容内部进行操作,这样就使得所有操作该内容的 单一线程具有该同步内容(加锁的同步方法或者同步块) 内的线程排他性,这 种情况的交替也可以理解为具有“短暂记忆效应”      一旦某个字段被申明为 volatile,在任何一个写入线程在工作内存中刷新缓存的 之前需要进行进一步的内存操作, 也就是说针对这样的字段进行立即刷新,可 以理解为这种volatile不会出现一般变量的缓存操作,而读取线程每次必须根据 前一个线程的可见域里面重新读取该变量的值,而不是直接读取。 当某个线程第一次去访问某个对象的域的时候,它要么初始化该对象的值,要 么从其他写入线程可见域里面去读取该对象的值; 这里结合上边理解,在满足 某种条件下,该线程对某对象域的值的读取是直接读取,有些时候却需要重新 读取    
注:如果在同一个线程里面通过方法调用去传一个对象的引用是绝对不 会出现上边提及到的可见性问题的。 JMM保证所有上边的规定以及关于内存可 见性特性的描述——一个特殊的更新、一个特定字段的修改都是某个线程针对 其他线程的一个“可见性”的概念,最终它发生的场所在内存模型中Java线程和 线程之间,至于这个发生时间可以是一个任意长的时间,但是最终会发生,也 就是说, Java内存模型中的可见性的特性主要是针对线程和线程之间使用内存 的一种规则和约定,该约定由JMM定义

3> 可排序性( Ordering):该规则将会约束任何一个违背了规则调用的线程在操作过程中的一些顺序,排序问题主要围绕了读取、写入和赋值语句有关的序列
注:内存模型本身是存在排序性的

原始JMM缺陷 JMM最初设计的时候存在一定的缺陷,这种缺陷虽然现有的JVM平台已 经修复,但是为了读者更加了解JMM的设计思路,因此还是提及 1> 不可变对象不是不可变:不可变对象似乎可以改变它们的值(这种对象的不可变指通过使用final关 键字来得到保证) ,( PublisServiceReminder:让一个对象的所有字段都为 final 并不一定使得这个对象不可变——
所有类型还必须是原始类型而不能是对 象的引用,而不可变对象被认为不要求同步的。但是,因为在将内存写方面的 更改从一个线程传播到另外一个线程的时候存在潜在的延迟,这样就使得有可 能存在一种竞态条件,即允许一个线程首先看到不可变对象的一个值,一段时 间之后看到的是一个不同的值

2> 重新排序的易失性和非易失性存储 :与 volatile字段的内存操作重新排序有关,这个领域中现 有的JMM引起了一些比较混乱的结果。现有的 JMM表明易失性的读和写是直 接和主存打交道的,这样避免了把值存储到寄存器或者绕过处理器特定的缓 存,这使得多个线程一般能看见一个给定变量最新的值。可是,结果是这种  volatile 定义并没有最初想象中那样如愿以偿,并且导致了 volatile的重大混 乱。为了在缺乏同步的情况下提供较好的性能,编译器、运行时和缓存通常是 允许进行内存的重新排序操作的,只要当前执行的线程分辨不出它们的区别。 (这就是within-thread as-if-serial semantics[线程内似乎是串行]的解释)但 是,易失性的读和写是完全跨线程安排的,编译器或缓存不能在彼此之间重新 排序易失性的读和写。使用易失性变量 initialized担任守卫来表明一套别的操作已经 完成了,这是一个很好的思想,但是不能在 JMM下工作,因为旧的 JMM允许 非易失性的写(比如写到 configOptions字段,以及写到由configOptions引用 Map的字段中)与易失性的写一起重新排序,因此另外一个线程可能会看到 initialized为 true,但是对于 configOptions字段或它所引用的对象还没有一个一 致的或者说当前的针对内存的视图变量, volatile的旧语义只承诺在读和写的变 量的可见性,而不承诺其他变量,虽然这种方法更加有效的实现,但是结果会 和我们设计之初大相径庭

Java内存管理简介 内存管理在Java语言中是JVM自动操作的,当JVM发现某些对象不再需 要的时候,就会对该对象占用的内存进行重分配(释放)操作,而且使得分配 出来的内存能够提供给所需要的对象。在一些编程语言里面,内存管理是一个 程序的职责,但是书写过C++的程序员很清楚,如果该程序需要自己来书写很 有可能引起很严重的错误或者说不可预料的程序行为,最终大部分开发时间都 花在了调试这种程序以及修复相关错误上。一般情况下在Java程序开发过程把 手动内存管理称为
显示内存管理,而显示内存管理经常发生的一个情况就是引 用悬挂——也就是说有可能在重新分配过程释放掉了一个被某个对象引用正在 使用的内存空间,释放掉该空间过后,该引用就处于悬挂状态。如果这个被悬 挂引用指向的对象试图进行原来对象(因为这个时候该对象有可能已经不存在 了) 进行操作的时候,由于该对象本身的内存空间已经被手动释放掉了,这个 结果是不可预知的。显示内存管理另外一个常见的情况是内存泄漏, 当某些引 用不再引用该内存对象的时候,而该对象原本占用的内存并没有被释放,这种 情况简言为内存泄漏。比如,如果针对某个链表进行了内存分配,而因为手动 分配不当,仅仅让引用指向了某个元素所处的内存空间,这样就使得其他链表 中的元素不能再被引用而且使得这些元素所处的内存让应用程序处于不可达状 态而且这些对象所占有的内存也不能够被再使用,这个时候就发生了内存泄 漏。而这种情况一旦在程序中发生,就会
一直消耗系统的可用内存直到可用内
存耗尽,而针对计算机而言内存泄漏的严重程度大了会使得本来正常运行的程 序直接因为内存不足而中断,并不是Java程序里面出现Exception那么轻量 级

堆和栈 1> 通用介绍:程序运行时有三种内存分配策略: 静态的、栈式的、堆式的             静态存储:是指在编译时就能够确定每个数据目标在运行时的存储空间 需求,因而在编译时就可以给它们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出 现,因为它们都会导致编译程序无法计算准确的存储空间             栈式存储:该分配可成为动态存储分配,是由一个类似于堆栈的运行栈 来实现的,和静态存储的分配方式相反,在栈式存储方案中,程序对数据区的 需求在编译时是完全未知的,只有到了运行的时候才能知道,但是规定在运行 中进入一个程序模块的时候,必须知道该程序模块所需要的数据区的大小才能 分配其内存。和我们在数据结构中所熟知的栈一样,栈式存储分配按照先进后 出的原则进行分配             堆式存储:堆式存储分配则专门负责在编译时或运行时模块入口处都无 法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例,堆由大 片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放

2> JVM结构【堆、栈解析】:在  Java虚拟机规范中,一个虚拟机实例的行为主要描述为: 子系统、 内存 区域、 数据类型和指令,这些组件在描述了抽象的JVM内部的一个抽象结构。 与其说这些组成部分的目的是进行JVM内部结构的一种支配,更多的是提供一 种严格定义实现的外部行为,该规范定义了这些抽象组成部分以及相互作用的 任何 Java虚拟机 执行所需要的行为。下图描述了JVM内部的一个结构,其中 主要包括主要的子系统、内存区域,Java虚拟机 有一个类加载器作为JVM的子系统,类加载器针对 Class进行检测 以鉴定完全合格的类接口,而JVM内部也有一个执行引擎
《Java内存模型 详解》

当 JVM 运行一个程序的时候,它的内存需要用来存储很多内容,包括字节 码、以及从类文件中提取出来的一些附加信息、以及程序中实例化的对象、方 法参数、返回值、局部变量以及计算的中间结果。 JVM 的内存组织需要在不同 的运行时数据区进行以上的几个操作,下边针对上图里面出现的几个运行时数 据区进行详细解析:一些运行时数据区共享了所有应用程序线程和其他特有的 单个线程,每个JVM实例有一个方法区和一个内存堆,这些是共同在虚拟机内 运行的线程。在Java程序里面,每个新的线程启动过后,它就会被 JVM 在内 部分配自己的PC寄存器[PCregisters]( 程序计数器器)和Java堆栈( Java  stacks)。若该线程正在执行一个非本地 Java方法,在PC寄存器的值指示下 一条指令执行,该线程在 Java内存栈 中保存了非本地Java方法调用状态,其状 态包括局部变量、被调用的参数、它的返回值、以及中间计算结果。而本地方 法调用的状态则是存储在独立的本地方法内存栈里面(nativemethod  stacks),这种情况下使得这些本地方法和其他内存运行时数据区的内容尽可 能保证和其他内存运行时数据区独立,而且该方法的调用更靠近操作系统,这 些方法执行的字节码有可能根据操作系统环境的不同使得其编译出来的本地字 节码的结构也有一定的差异。 JVM中的内存栈是一个栈帧的组合,一个栈帧包 含了某个Java方法调用的状态,当某个线程调用方法的时候, JVM就会将一个 新的帧压入到Java内存栈,当方法调用完成过后, JVM将会从内存栈中移除该 栈帧。 JVM里面不存在一个可以存放中间计算数据结果值的寄存器,其内部指令集使用 Java栈 空间来存储中间计算的数据结果值,这种做法的设计是为了保 持 Java虚拟机 的指令集紧凑,使得与寄存器原理能够紧密结合并且进行操作
《Java内存模型 详解》

    (1) 方法区( Method Area) 在 JVM实例中,对装载的类型信息是存储在一个逻辑方法内存区中, 当  Java虚拟机 加载了一个类型的时候,它会跟着这个Class的类型去路径里面查 找对应的Class文件,类加载器读取类文件(线性二进制数据),然后将该文 件传递给Java虚拟机, JVM从二进制数据中提取信息并且将这些信息存储在方 法区,而类中声明(静态)变量就是来自于方法区中存储的信息。在 JVM 里面 用什么样的方式存储该信息是由 JVM 设计的时候决定的,例如:当数据进入方 法的时候,多类文件字节的存储量以 Big-Endian(第一次最重要的字节) 的顺 序存储,尽管如此,一个虚拟机可以用任何方式针对这些数据进行存储操作, 若它存储在一个 Little-Endian处理器上,设计的时候就有可能将多文件字节的 值按照 Little-Endian顺寻存储

        <1>类型信息:JVM虚拟机 将搜索和使用类型的一些信息也存储在方法区中以方便应用程 序加载读取该数据。设计者在设计过程也考虑到要方便 JVM 进行 Java应用程 序的快速执行,而这种取舍主要是为了程序在运行过程中内存不足的情况能够 通过一定的取舍去弥补内存不足的情况。在 JVM内部, 所有的线程共享相同的 方法区,因此,访问方法区的数据结构必须是线程安全的,如果两个线程都试 图去调用去找一个名为 Lava的类,比如Lava还没有被加载, 只有一个线程可 以加载该类而另外的线程只能够等待。方法区的大小在分配过程中是不固定 的,随着Java应用程序的运行, JVM可以调整其大小,需要注意一点,方法区 的内存不需要是连续的,因为方法区内存可以分配在内存堆中,即使是虚拟机  JVM实例对象自己所在的内存堆也是可行的,而在实现过程是允许程序员自身 来指定方法区的初始化大小的

由于 Java 本身的自动内存管理,方法区也会被垃圾回收的, Java 程序 可以通过类扩展动态加载器对象,类可以成为“未引用”向垃圾回收器进行 申请,如果一个类是“未引用”的,则该类就可能被卸载, 而方法区针对具体的语言特性有几种信息是存储在方法区内的:在 JVM 和 类文件名 的内部,类型名一般都是完全限定名 ( java.lang.String)格式,在Java源文件里面,完全限定名必须加入包前缀, 而不是在开发过程写的简单类名,而在方法上,只要是符合 Java语言规范 的类的完全限定名都可以,而 JVM 可能直接进行解析,比如: ( java.lang.String)在JVM内部名称为java/lang/String,这就是我们在异常捕捉 的时候经常看到的 ClassNotFoundException 的异常里面类信息的名称格式。 除此之外,还必须为每一种加载过的类型在JVM内进行存储,下边的信息 不存储在方法区内

        <2>常量池:针对类型加载的类型信息,JVM 将这些存储在常量池里,常量池是一个根 据类型定义的常量的有序常量集,包括字面量( String、 Integer、 Float常量) 以及符号引用(类型、字段、方法),整个长量池会被 JVM 的一个索引引用, 如同数组里面的元素集合按照索引访问一样, JVM针对这些常量池里面存储的 信息也是按照索引方式进行。实际上长量池在Java程序的动态链接过程起到了 一个至关重要的作用

        <3>字段信息:针对字段的类型信息,下边的信息是存储在方法区里面的: 字段名、 字段类型、 字段修饰符( public , private , protected , static , final , volatile , transient )

        <4>方法信息:针对方法信息,下边信息存储在方法区上: 方法名、 方法的返回类型(包括void) 、方法参数的类型、 数目以及顺序、 方法修饰符( public , private , protected , static , final , synchronized , native , abstract)                                针对非本地方法,还有些附加方法信息需要存储在方法区内: 方法字节码、 方法中局部变量区的大小、方法栈帧、 异常表

        <5>类变量:类变量在一个类的多个实例之间共享,这些变量直接和类相关,而不是和 类的实例相关, (定义过程简单理解为类里面定义的static类型的变量) ,针 对类变量,其逻辑部分就是存储在方法区内的。在 JVM 使用这些类之前, JVM 先要在方法区里面为定义的non-final变量分配内存空间;常量(定义为final) 则在 JVM内部 则不是以同样的方式来进行存储的,尽管针对常量而言,一个  final 的类变量是拥有它自己的常量池,作为常量池里面的存储某部分, 类常量 是存储在方法区内的,而其逻辑部分则不是按照上边的类变量的方式来进行内 存分配的。虽然non-final类变量是作为这些类型声明中存储数据的某一部分, final变量存储为任何使用它类型的一部分的数据格式进行简单存储

        <6>类Class的引:JVM 在加载了任何一个类型过后会创建一个 java.lang.Class的实例,虚拟 机必须通过一定的途径来引用该类型对应的一个 Class 的实例,并且将其存储 在方法区内

        <7>方法表:为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除 了以上讨论的结构, JVM 的实现者还添加一些其他的数据结构,如方法表

    (2) 内存栈(Stack):当一个新线程启动的时候, JVM 会为 Java线程创建每个线程的独立内存 栈,如前所言 Java 的内存栈是由栈帧构成, 栈帧本身处于游离状态,在JVM 里面,栈帧的操作只有两种: 出栈和入栈。正在被线程执行的方法一般称为当 前线程方法,而该方法的栈帧就称为当前帧,而在该方法内定义的类称为当前 类,常量池也称为当前常量池。当执行一个方法如此的时候, JVM保留当前类 和当前常量池的跟踪,当虚拟机遇到了存储在栈帧中的数据上的操作指令的时 候,它就执行当前帧的操作。当一个线程调用某个Java方法时,虚拟机创建并 且将一个新帧压入到内存堆栈中,而这个压入到内存栈中的帧成为当前栈帧, 当该方法执行的时候, JVM 使用内存栈来存储参数、局部变量、中间计算结果 以及其他相关数据。方法在执行过程有可能因为两种方式而结束: 如果一个方 法返回完成就属于方法执行的正常结束,如果在这个过程抛出异常而结束,可 以称为非正常结束,不论是正常结束还是异常结束, JVM都会弹出或者丢弃该 栈帧,则上一帧的方法就成为了当前帧。在JVM中, Java线程的栈数据是属于某个线程独有的,其他的线程不能够 修改或者通过其他方式来访问该线程的栈帧,正因为如此这种情况不用担心多 线程同步访问Java的局部变量,当一个线程调用某个方法的时候,方法的局部 变量是在方法内部进行的Java栈帧的存储,只有当前线程可以访问该局部变量,而其他线程不能随便访问该内存栈里面存储的数据。内存栈内的栈帧数据 和方法区以及内存堆一样, Java栈的栈帧不需要分配在连续的堆栈内,或者说 它们可能是在堆,或者两者组合分配,实际数据用于表示Java堆栈和栈帧结构 是JVM本身的设计结构决定的,而且在编程过程可以允许程序员指定一个用于 Java堆栈的初始大小以及最大、最小尺寸

内存栈: 这里的内存栈和物理结构内存堆栈有点点区别,是内存里面数据存储 的一种抽象数据结构。从操作系统上讲,在程序执行过程对内存的使用本身常 用的数据结构就是内存堆栈,而这里的内存堆栈指代的就是 JVM 在使用内存过 程整个内存的存储结构, 多指内存的物理结构,而Java内存栈不是指代的一个 物理结构,更多的时候指代的是一个抽象结构,就是符合 JVM 语言规范的内存 栈的一个抽象结构。 因为物理内存堆栈结构和Java内存栈的抽象模型结构本身 比较相似,所以在学习过程就正常把这两种结构放在一起考虑了,而且二 者除了概念上有一点点小的区别,理解成为一种结构对于初学者也未尝不可, 所以实际上也可以觉得二者没有太大的本质区别。但是在学习的时候最好分清 楚内存堆栈和Java内存栈的一小点细微的差距,前者是物理概念和本身模型, 后者是抽象概念和本身模型的一个共同体。而内存堆栈更多的说法可以理解为 一个内存块,因为内存块可以通过索引和指针进行数据结构的组合, 内存栈就 是内存块针对数据结构的一种表示,而内存堆则是内存块的另外一种数据结构 的表示,这样理解更容易区分内存栈和内存堆栈(内存块) 的概念

栈帧: 栈帧是内存栈里面的最小单位,指的是内存栈里面每一个最小内存存储 单元,它针对内存栈仅仅做了两个操作: 入栈和出栈,一般情况下:所说的堆 栈帧和栈帧倒是一个概念,所以在理解上记得加以区分

内存堆: 这里的内存堆和内存栈是相对应的,其实内存堆里面的数据也是存储 在系统内存堆栈里面的,只是它使用了另外一种方式来进行堆里面内存的管 理,Java语言本身的内存堆和内存栈,而这两个概念 都是抽象的概念模型,而且是相对的

栈帧: 栈帧主要包括三个部分: 局部变量、 操作数栈帧(操作帧) 和帧数 据(数据帧) 。本地变量和操作数帧的大小取决于需要,这些大小是在编译时 就决定的,并且在每个方法的类文件数据中进行分配,帧的数据大小则不一 样,它虽然也是在编译时就决定的但是它的大小和本身代码实现有关。当JVM  调用一个Java方法的时候,它会检查类的数据来确定在本地变量和操作方法要 求的栈大小,它计算该方法所需要的内存大小,然后将这些数据分配好内存空 间压入到内存堆栈中

栈帧 —— 局部变量: 局部变量是以 Java栈帧 组合成为的一个以零为基的数 组,使用局部变量的时候使用的实际上是一个包含了0的一个基于索引的数组结 构。 int类型、 float、 引用以及返回值都占据了一个数组中的局部变量的条目, 而 byte、 short、 char则在存储到局部变量的时候是先转化成为 int再进行操作的,则 long 和 double 则是在这样一个数组里面使用了两个元素的空间大小,在 局部变量里面存储基本数据类型的时候使用的就是这样的结构

栈帧——操作帧: 和局部变量一样,操作帧也是一组有组织的数组的存储 结构,但是和局部变量不一样的是这个不是通过数组的索引访问的,而是直接 进行的入栈和出栈的操作,当操作指令直接压入了操作栈帧过后,从栈帧里面 出来的数据会直接在出栈的时候被读取和使用。除了程序计数器以外,操作帧 也是可以直接被指令访问到的, JVM里面没有寄存器。处理操作帧的时候Java 虚拟机是基于内存栈的而不是基于寄存器的,因为它在操作过程是直接对内存 栈进行操作而不是针对寄存器进行操作。而 JVM内部的指令也可以来源于其他 地方比如紧接着操作符以及操作数的字节码流或者直接从常量池里面进行操 作。 JVM指令其实真正在操作过程的焦点是集中在内存栈栈帧的操作帧上的。 JVM指令将操作帧作为一个工作空间,有许多指令都是从操作帧里面出栈读取的,对指令进行操作过后将操作帧的计算结果重新压入内存堆栈内。比如 iadd 指令将两个整数压入到操作帧里面,然后将两个操作数进行相加,相加的时候 从内存栈里面读取两个操作数的值,然后进行运算,最后将运算结果重新存入 到内存堆栈里面

栈帧——数据帧:除了局部变量和操作帧以外, Java栈帧还包括了数据 帧,用于支持常量池、普通的方法返回以及异常抛出等,这些数据都是存储在 Java内存栈帧的数据帧中的。很多 JVM的指令集实际上使用的都是常量池里面 的一些条目,一些指令,只是把int、 long、 float、 double或者String从常量池 里面压入到Java栈帧的操作帧上边,一些指令使用常量池来管理类或者数组的 实例化操作、字段的访问控制、或者方法的调用,其他的指令就用来决定常量 池条目中记录的某一特定对象是否某一类或者常量池项中指定的接口。常量池 会判断类型、字段、方法、类、接口、类字段以及引用是如何在 JVM进行符号 化描述,而这个过程由JVM本身进行对应的判断。这里就可以理解JVM如何 来判断我们通常说的: “原始变量存储在内存栈上,而引用的对象存储在内存堆 上边。 ”除了常量池判断帧数据符号化描述特性以外,这些数据帧必须在 JVM 正常执行或者异常执行过程辅助它进行处理操作。如果一个方法是正常结束 的, JVM必须恢复栈帧调用方法的数据帧,而且必须设置 PC寄存器指向调用 方法后边等待的指令完成该调用方法的位置。如果该方法存在返回值, JVM也 必须将这个值压入到操作帧里面以提供给需要这些数据的方法进行调用。不仅如此,数据帧也必须提供一个方法调用的异常表,当JVM在方法中抛出异常 而非正常结束的时候,该异常表就用来存放异常信息

    (3)内存堆( Heap): 当一个Java应用程序在运行的时候在程序中创建一个对象或者一个数组的 时候, JVM会针对该对象和数组分配一个新的内存堆空间。但是在JVM实例 内部,只存在一个内存堆实例,所有的依赖该JVM的Java应用程序都需要共 享该堆实例,而Java应用程序本身在运行的时候它自己包含了一个由JVM虚 拟机实例分配的自己的堆空间,而在应用程序启动的时候,任何一个 Java应用 程序都会得到JVM分配的堆空间,而且针对每一个Java应用程序,这些运行 Java应用程序的堆空间都是相互独立的。这里所提及到的共享堆实例是指 JVM 在初始化运行的时候整体堆空间只有一个,这个是Java语言平台直接从操作系 统上能够拿到的整体堆空间,所以的依赖该JVM的程序都可以得到这些内存空 间,但是针对每一个独立的 Java应用程序而言,这些堆空间是相互独立的,每 一个Java应用程序在运行最初都是依靠JVM来进行堆空间的分配的。即使是 两个相同的Java应用程序,一旦在运行的时候处于不同的操作系统进程(一般 为java.exe)中,它们各自分配的堆空间都是独立的,不能相互访问,只是两个 Java应用进程初始化拿到的堆空间来自JVM的分配,而JVM是从最初的内存 堆实例里面分配出来的。在同一个Java应用程序里面如果出现了不同的线程, 则是可以共享每一个Java应用程序拿到的内存堆空间的,这也是为什么在开发 多线程程序的时候,针对同一个Java应用程序必须考虑线程安全问题,因为在 一个Java进程里面所有的线程是可以共享这个进程拿到的堆空间的数据的。但 是Java内存堆有一个特性,就是JVM拥有针对新的对象分配内存的指令,但 是它却不包含释放该内存空间的指令,当然开发过程可以在Java源代码中显示 释放内存或者说在JVM字节码中进行显示的内存释放,但是JVM仅仅只是检 测堆空间中是否有引用不可达(不可以引用)的对象,然后将接下来的操作交 给垃圾回收器来处理

JVM规范里面并没有提及到 Java对象如何在堆空间中表示和描述,对象 表示可以理解为设计JVM的工程师在最初考虑到对象调用以及垃圾回收器针对 对象的判断而独立的一种Java对象在内存中的存储结构,该结构是由设计最初 考虑的。针对一个创建的类实例而言,它内部定义的实例变量以及它的超类以 及一些相关的核心数据,是必须通过一定的途径进行该对象内部存储以及表示 的。当开发过程给定了一个对象引用的时候, JVM必须能够通过这个引用快速 从对象堆空间中去拿到该对象能够访问的数据内容。也就是说,堆空间内对象 的存储结构必须为外围对象引用提供一种可以访问该对象以及控制该对象的接 口使得引用能够顺利地调用该对象以及相关操作。因此,针对堆空间的对象, 分配的内存中往往也包含了一些指向方法区的指针,因为从整体存储结构上 讲, 方法区似乎存储了很多原子级别的内容,包括方法区内最原始最单一的一 些变量:比如类字段、字段数据、类型数据等等

    (4)内存栈和内存堆的实现原理探:实际上不论是内存栈结构、方法区还是内存堆结构,归根到底使用的是操 作系统的内存,操作系统的内存结构可以理解为内存块,常用的抽象方式就是 一个内存堆栈,而JVM在OS上边安装了过后,就在启动Java程序的时候按照 配置文件里面的内容向操作系统申请内存空间,该内存空间会按照JVM内部的 方法提供相应的结构调整。 内存栈应该是很容易理解的结构实现, 一般情况下,内存栈是保持连续 的,但是不绝对,内存栈申请到的地址实际上很多情况下都是连续的,而每个 地址的最小单位是按照计算机位来算的,该计算机位里面只有两种状态1和0, 而内存栈的使用过程就是典型的类似 C++里面的普通指针结构的使用过程,直 接针对指针进行++或者–操作就修改了该指针针对内存的偏移量,而这些偏移 量就使得该指针可以调用不同的内存栈中的数据。至于针对内存栈发送的指令 就是常见的计算机指令,而这些指令就使得该指针针对内存栈的栈帧进行指令 发送,比如发送操作指令、变量读取等等,直接就使得内存栈的调用变得更加 简单,而且栈帧在接受了该数据过后就知道到底针对栈帧内部的哪一个部分进 行调用,是操作帧、数据帧还是局部变量。 内存堆实际上在操作系统里面使用了双向链表的数据结构,双向链表的结 构使得即使内存堆不具有连续性,每一个堆空间里面的链表也可以进入下一个 堆空间,而操作系统本身在整理内存堆的时候会做一些简单的操作,然后通过 每一个内存堆的双向链表就使得内存堆更加方便。而且堆空间不需要有序,甚 至说有序不影响堆空间的存储结构,因为它归根到底是在内存块上边进行实现 的,内存块本身是一个堆栈结构,只是该内存堆栈里面的块如何分配不由 JVM 决定,是由操作系统已经最开始分配好了,也就是最小存储单位。然后 JVM拿 到从操作系统申请的堆空间过后,先进行初始化操作,然后就可以直接使用 了。 常见的对程序有影响的内存问题主要是两种: 溢出和内存泄漏,上边已经 讲过了内存泄漏,其实从内存的结构分析,泄漏这种情况很难甚至说不可能发 生在栈空间里面,其主要原因是栈空间本身很难出现悬停的内存,因为栈空间 的存储结构有可能是内存的一个地址数组,所以在访问栈空间的时候使用的都是索引或者下标或者就是最原始的出栈和入栈的操作,这些操作使得栈里面很 难出现像堆空间一样的内存悬停(也就是引用悬挂) 问题。堆空间悬停的内存 是因为栈中存放的引用的变化,其实引用可以理解为从栈到堆的一个指针,当 该指针发生变化的时候,堆内存碎片就有可能产生,而这种情况下在原始语言 里面就经常发生内存泄漏的情况,因为这些悬停的堆空间在系统里面是不能够 被任何本地指针引用到,就使得这些对象在未被回收的时候脱离了可操作区域 并且占用了系统资源。 栈溢出问题一直都是计算机领域里面的一个安全性问题,这里不做深入讨 论,说多了就偏离主题了,而内存泄漏是程序员最容易理解的内存问题,还有 一个问题来自于我一个黑客朋友就是: 堆溢出现象,这种现象可能更加复杂。 其实Java里面的内存结构,最初看来就是堆和栈的结合,实际上可以这样 理解,实际上对象的实际内容才存在对象池里面,而有关对象的其他东西有可 能会存储于方法区,而平时使用的时候的引用是存在内存栈上的,这样就更加 容易理解它内部的结构,不仅仅如此,有时候还需要考虑到 Java里面的一些字 段和属性到底是对象域的还是类域的,这个也是一个比较复杂的问题。 二者的区别简单总结一下: 管理方式: JVM自己可以针对内存栈进行管理操作,而且该内存空间的释放是 编译器就可以操作的内容,而堆空间在Java中JVM本身执行引擎不会对其进 行释放操作,而是让垃圾回收器进行自动回收 空间大小: 一般情况下栈空间相对于堆空间而言比较小,这是由栈空间里面存 储的数据以及本身需要的数据特性决定的,而堆空间在JVM堆实例进行分配的 时候一般大小都比较大,因为堆空间在一个Java程序中需要存储太多的Java对 象数据 碎片相关: 针对堆空间而言,即使垃圾回收器能够进行自动堆内存回收,但是 堆空间的活动量相对栈空间而言比较大,很有可能存在长期的堆空间分配和释 放操作,而且垃圾回收器不是实时的,它有可能使得堆空间的内存碎片主键累 积起来。针对栈空间而言,因为它本身就是一个堆栈的数据结构,它的操作都 是一一对应的,而且每一个最小单位的结构栈帧和堆空间内复杂的内存结构不 一样,所以它一般在使用过程很少出现内存碎片。 分配方式: 一般情况下,栈空间有两种分配方式:静态分配和动态分配,静态 分配是本身由编译器分配好了,而动态分配可能根据情况有所不同,而堆空间 却是完全的动态分配的,是一个运行时级别的内存分配。而栈空间分配的内存 不需要我们考虑释放问题,而堆空间即使在有垃圾回收器的前提下还是要考虑 其释放问题。 效率: 因为内存块本身的排列就是一个典型的堆栈结构,所以栈空间的效率自 然比起堆空间要高很多,而且计算机底层内存空间本身就使用了最基础的堆栈 结构使得栈空间和底层结构更加符合,它的操作也变得简单就是最简单的两个 指令:入栈和出栈;栈空间针对堆空间而言的弱点是灵活程度不够,特别是在 动态管理的时候。而堆空间最大的优势在于动态分配,因为它在计算机底层实 现可能是一个双向链表结构,所以它在管理的时候操作比栈空间复杂很多,自 然它的灵活度就高了,但是这样的设计也使得堆空间的效率不如栈空间,而且 低很多

本机内存 Java堆空间是在编写Java程序中被我们使用得最频繁的内存空间,平时开 发过程,开发人员一定遇到过OutOfMemoryError,这种结果有可能来源于Java 堆空间的内存泄漏,也可能是因为堆的大小不够而导致的, 有时候这些错误是 可以依靠开发人员修复的,但是随着Java程序需要处理越来越多的并发程序, 可能有些错误就不是那么容易处理了。有些时候即使Java堆空间没有满也可能 抛出错误,这种情况下需要了解的就是JRE( JavaRuntimeEnvironment)内部 到底发生了什么。 Java本身的运行宿主环境并不是操作系统,而是Java虚拟 机, Java虚拟机本身是用C编写的本机程序,自然它会调用到本机资源,最常 见的就是针对本机内存的调用。本机内存是可以用于运行时进程的,它和Java 应用程序使用的 Java堆内存不一样,每一种虚拟化资源都必须存储在本机内存里面,包括虚拟机本身运行的数据,这样也意味着主机的硬件和操作系统在本机内存的限制将直接影响到Java应用程序的性能

    (1) 堆空间和垃圾回收 :Java运行时是一个操作系统进程( Windows下一般为java.exe) ,该环境 提供的功能会受一些位置的用户代码驱动,这虽然提高了运行时在处理资源的 灵活性,但是无法预测每种情况下运行时环境需要何种资源,这一点Java堆空 间讲解中已经提到过了。在Java命令行可以使用 -Xmx 和 -Xms 来控制堆空间初 始配置, mx表示堆空间的最大大小, ms表示初始化大小,这也是上提到的启 动Java的配置文件可以配置的内容。尽管逻辑内存堆可以根据堆上的对象数量 和在 GC 上花费的时间增加或者减少,但是使用本机内存的大小是保持不变 的,而且由 -Xms 的值指定,大部分 GC 算法都是依赖被分配的连续内存块的堆 空间,因此不能在堆需要扩大的时候分配更多的本机内存,所有的堆内存必须 保留下来,请注意这里说的不是 Java 堆内存空间是本机内存。 本机内存保留和本机内存分配不一样,本机内存被保留的时候,无法使用 物理内存或者其他存储器作为备用内存,尽管保留地址空间块不会耗尽物理资 源,但是会阻止内存用于其他用途, 由保留从未使用过的内存导致的泄漏和泄 漏分配的内存造成的问题其严重程度差不多,但使用的堆区域缩小时,一些垃 圾回收器会回收堆空间的一部分内容,从而减少物理内存的使用。对于维护  Java堆 的内存管理系统,需要更多的本机内存来维护它的状态,进行垃圾收集 的时候,必须分配数据结构来跟踪空闲存储空间和进度记录,这些数据结构的 确切大小和性质因实现的不同而有所差异

    (2) JIT :JIT编译器在运行时编译Java字节码来优化本机可执行代码,这样极大提 高了Java运行时的速度,并且支持Java应用程序与本地代码相当的速度运行。 字节码编译使用本机内存,而且JIT编译器的输入(字节码)和输出(可执行 代码) 也必须存储在本机内存里面,包含了多个经过JIT编译的方法的Java程 序会比一些小型应用程序使用更多的本机内存

    (3) 类和类加载器: Java 应用程序由一些类组成,这些类定义对象结构和方法逻辑。 Java 应用 程序也使用 Java 运行时类库(比如 java.lang.String) 中的类,也可以使用第 三方库。这些类需要存储在内存中以备使用。存储类的方式取决于具体实现。 SunJDK 使用永久生成( permanentgeneration,PermGen)堆区域,从最基 本的层面来看,使用更多的类将需要使用更多内存。 (这可能意味着您的本机 内存使用量会增加,或者您必须明确地重新设置 PermGen 或共享类缓存等区 域的大小,以装入所有类) 。记住,不仅您的应用程序需要加载到内存中,框 架、应用服务器、第三方库以及包含类的 Java 运行时也会按需加载并占用空 间。 Java 运行时可以卸载类来回收空间,但是只有在非常严酷的条件下才会这 样做,不能卸载单个类,而是卸载类加载器,随其加载的所有类都会被卸载。 只有在以下情况下才能卸载类加载器。Java 堆不包含对表示该类加载器的 java.lang.ClassLoader 对象的引用。 Java 堆不包含对表示类加载器加载的类的任何 java.lang.Class 对象的引用。 在 Java 堆上,该类加载器加载的任何类的所有对象都不再存活(被引用)

    (4) Java 运行时为所有 Java 应用程序创建的 3 个默认类加载 器(bootstrap、 extension 和 application ) 都不可能满足这些条件,因此,任 何系统类(比如 java.lang.String)或通过应用程序类加载器加载的任何应用程 序类都不能在运行时释放。即使类加载器适合进行收集,运行时也只会将收集 类加载器作为GC 周期的一部分。一些实现只会在某些 GC 周期中卸载类加 载器,也可能在运行时生成类,而不去释放它。许多 JavaEE 应用程序使用 JavaServerPages(JSP) 技术来生成 Web 页面。使用 JSP 会为执行的每个 .jsp  页面生成一个类,并且这些类会在加载它们的类加载器的整个生存期中一直存 在  —— 这个生存期通常是 Web 应用程序的生存期。另一种生成类的常见方 法是使用 Java 反射。反射的工作方式因 Java 实现的不同而不同,当使用  java.lang.reflectAPI 时, Java 运行时必须将一个反射对象(比如  java.lang.reflect.Field)的方法连接到被反射到的对象或类。这可以通过使用  Java 本机接口( JavaNativeInterface, JNI) 访问器来完成,这种方法需要的 设置很少,但是速度缓慢,也可以在运行时为您想要反射到的每种对象类型动 态构建一个类。后一种方法在设置上更慢,但运行速度更快,非常适合于经常 反射到一个特定类的应用程序。 Java 运行时在最初几次反射到一个类时使用 JNI 方法,但当使用了若干次 JNI 方法之后,访问器会膨胀为字节码访问器, 这涉及到构建类并通过新的类加载器进行加载。执行多次反射可能导致创建了 许多访问器类和类加载器,保持对反射对象的引用会导致这些类一直存活,并 继续占用空间,因为创建字节码访问器非常缓慢,所以 Java 运行时可以缓存 这些访问器以备以后使用, 一些应用程序和框架还会缓存反射对象,这进一步 增加了它们的本机内存占用

    (4) JNI: JNI支持本机代码调用Java方法,反之亦然, Java运行时本身极大依赖于  JNI代码 来实现类库功能,比如文件和网络I/O, JNI应用程序可以通过三种方 式增加Java运行时对本机内存的使用: JNI应用程序的本机代码被编译到共享库中,或编译为加载到进程地址空间中 的可执行文件,大型本机应用程序可能仅仅加载就会占用大量进程地址空间 本机代码必须与Java运行时共享地址空间,任何本机代码分配或本机代码执行 的内存映射都会耗用Java运行时内存 某些JNI函数可能在它们的常规操作中使用本机内存, GetTypeArrayElements  和 GetTypeArrayRegion函数可以将Java堆复制到本机内存缓冲区中,提供给 本地代码使用,是否复制数据依赖于运行时实现,通过这种方式访问大量 Java  堆数据就可能使用大量的本机内存堆空间

    (5) NIO: JDK1.4开始添加了新的I/O类,引入了一种基于通道和缓冲区执行I/O的 新方式,就像Java堆上的内存支持I/O缓冲区一样, NIO添加了对直接 ByteBuffer的支持, ByteBuffer受本机内存而不是 Java堆的支持,直接 ByteBuffer可以直接传递到本机操作系统库函数,以执行 I/O,这种情况虽然提 高了Java程序在I/O的执行效率,但是会对本机内存进行直接的内存开销。 ByteBuffer直接操作和非直接操作的区别如下:
《Java内存模型 详解》

对于在何处存储直接 ByteBuffer 数据,很容易产生混淆。应用程序仍然在 Java 堆上使用一个对象来编排 I/O 操作,但持有该数据的缓冲区将保存在本 机内存中, Java 堆对象仅包含对本机堆缓冲区的引用。非直接 ByteBuffer 将 其数据保存在 Java 堆上的 byte[] 数组中。直接ByteBuffer对象会自动清理本 机缓冲区,但这个过程只能作为 Java堆GC的一部分执行,它不会自动影响施 加在本机上的压力。 GC仅在Java堆被填满,以至于无法为堆分配请求提供服 务的时候,或者在 Java应用程序中显示请求它发生

    (6) 线程: 应用程序中的每个线程都需要内存来存储器堆栈(用于在调用函数时持有 局部变量并维护状态的内存区域)。每个 Java 线程都需要堆栈空间来运行。 根据实现的不同, Java 线程可以分为本机线程和 Java 堆栈。除了堆栈空间, 每个线程还需要为线程本地存储( thread-local storage) 和内部数据结构提供 一些本机内存。尽管每个线程使用的内存量非常小,但对于拥有数百个线程的 应用程序来说,线程堆栈的总内存使用量可能非常大。如果运行的应用程序的 线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致糟 糕的性能和更高的内存占用

本机内存耗尽 Java运行时善于以不同的方式来处理Java堆空间的耗尽和本机堆空间的 耗尽,但是这两种情形具有类似症状,当Java堆空间耗尽的时候, Java应用程 序很难正常运行,因为Java应用程序必须通过分配对象来完成工作,只要Java 堆被填满,就会出现糟糕的GC性能,并且抛出OutOfMemoryError。相反,一 旦 Java 运行时开始运行并且应用程序处于稳定状态,它可以在本机堆完全耗 尽之后继续正常运行, 不一定会发生奇怪的行为,因为需要分配本机内存的操 作比需要分配 Java 堆的操作少得多。尽管需要本机内存的操作因 JVM 实现 不同而异,但也有一些操作很常见: 启动线程、 加载类以及执行某种类型的网 络和文件 I/O。 本机内存不足行为与 Java 堆内存不足行为也不太一样,因为 无法对本机堆分配进行控制,尽管所有 Java 堆分配都在 Java 内存管理系统 控制之下,但任何本机代码(无论其位于 JVM、 Java 类库还是应用程序代 码中) 都可能执行本机内存分配,而且会失败。尝试进行分配的代码然后会处 理这种情况,无论设计人员的意图是什么:它可能通过 JNI 接口抛出一个 OutOfMemoryError,在屏幕上输出一条消息,发生无提示失败并在稍后再试 一次,或者执行其他操作

内存共享 在Java语言里面, 没有共享内存的概念,但是在某些引用中,共享内存却 很受用,例如Java语言的分布式系统,存着大量的Java分布式共享对象,很多 时候需要查询这些对象的状态,以查看系统是否运行正常或者了解这些对象目 前的一些统计数据和状态。 如果使用的是网络通信的方式,显然会增加应用的 额外开销,也增加了不必要的应用编程,如果是共享内存方式,则可以直接通 过共享内存查看到所需要的对象的数据和统计数据,从而减少一些不必要的麻 烦

    (1) 共享内存特点: 可以被多个进程打开访问, 读写操作的进程在执行读写操作的时候其他进程不能进行写操作, 多个进程可以交替对某一个共享内存执行写操作 ,一个进程执行了内存写操作过后,不影响其他进程对该内存的访问,同时其他 进程对更新后的内存具有可见性 在进程执行写操作时如果异常退出,对其他进程的写操作禁止自动解除 相对共享文件,数据访问的方便性和效率

    (2) 出现情况: 独占的写操作,相应有独占的写操作等待队列。独占的写操作本身不会发生数 据的一致性问题; 共享的写操作,相应有共享的写操作等待队列。共享的写操作则要注意防止发 生数据的一致性问题; 独占的读操作,相应有共享的读操作等待队列; 共享的读操作,相应有共享的读操作等待队列

    (3) Java中共享内存的实现: JDK1.4 里面的 MappedByteBuffer 为开发人员在Java中实现共享内存提供 了良好的方法,该缓冲区实际上是一个磁盘文件的内存映象,二者的变化会保 持同步,即内存数据发生变化过后会立即反应到磁盘文件中,这样会有效地保 证共享内存的实现,将共享文件和磁盘文件简历联系的是文件通道类: FileChannel,该类的加入是JDK为了统一外围设备的访问方法,并且加强了多 线程对同一文件进行存取的安全性,这里可以使用它来建立共享内存用,它建 立了共享内存和磁盘文件之间的一个通道。打开一个文件可使用  RandomAccessFile类 的 getChannel方法,该方法直接返回一个文件通道,该文 件通道由于对应的文件设为随机存取, 一方面可以进行读写两种操作,另外一 个方面使用它不会破坏映象文件的内容。这里,如果使用 FileOutputStream 和 FileInputStream则不能理想地实现共享内存的要求,因为这两个类同时实现自 由读写很困难

    (4) 共享内存的应用: 在  Java 中,共享内存一般有两种应用:         <1> 永久对象配置:在 java服务器应用中,用户可能会在运行过程中配置 一些参数,而这些参数需要永久有效,当服务器应用重新启动后,这些配置参 数仍然可以对应用起作用。这就可以用到该文 中的共享内存。该共享内存中保 存了服务器的运行参数和一些对象运行特性。可以在应用启动时读入以启用以 前配置的参数         <2> 查询共享数据:一个应用(例 sys.java)是系统的服务进程,其系 统的运行状态记录在共享内存中,其中运行状态可能是不断变化的。为了随时 了解系统的运行状态,启动另一个应用(例 mon.java),该应用查询该共享 内存,汇报系统的运行状态

防止内存泄漏 1> Java中的内存泄漏 在 Java语言 中, 内存泄漏就是存在一些被分配的对象,这些对象有两个特 点: 这些对象可达,即在对象内存的有向图中存在通路可以与其相连;其次, 这些对象是无用的,即程序以后不会再使用这些对象了。 如果对象满足这两个 条件,该对象就可以判定为Java中的内存泄漏,这些对象不会被GC回收,然 而它却占用内存,这就是 Java语言中的内存泄漏。 Java 中的内存泄漏和C++ 中的内存泄漏还存在一定的区别,在C++里面,内存泄漏的范围更大一些,有 些对象被分配了内存空间,但是却不可达,由于C++中没有GC,这些内存将 会永远收不回来,在Java中这些不可达对象则是被GC负责回收的,因此程序 员不需要考虑这一部分的内存泄漏
《Java内存模型 详解》

Java语言中也是存在内存泄漏的,但是其内存泄漏 范围比C++要小很多,因为Java里面有个特殊程序回收所有的不可达对象: 垃 圾回收器。对于程序员来说, GC基本是透明的,不可见的。虽然,我们只有 几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言 规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM 实现者可能使用不同的算法管理GC。通常, GC的线程的优先级别较低, JVM 调用GC的策略也有很多种,有的是内存使用到达一定程度时, GC才开始工作,也有定时执行的,有的是平缓执行 GC,有的是中断式执行 GC。但通常来 说,我们不需要关心这些。除非在一些特定的场合, GC的执行影响应用程序 的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然 中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能 够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行, Sun 提供的HotSpotJVM就支持这一特性
注:Java语言因为提供了垃圾回收器,照理说是不会出现内存泄漏的, Java里面导致内存泄漏的主要原因就是,先前申请了内存空间而忘记了释放。 如果程序中存在对无用对象的引用,这些对象就会驻留在内存中消耗内存,因 为无法让GC判断这些对象是否可达。如果存在对象的引用,这个对象就被定 义为“有效的活动状态”,同时不会被释放,要确定对象所占内存被回收,必须 要确认该对象不再被使用。典型的做法就是把对象数据成员设置成为null或者 从集合中移除,当局部变量不需要的情况则不需要显示声明为null

2> 常见的Java内存泄漏     (1) 全局集合: 在大型应用程序中存在各种各样的全局数据仓库是很普遍的,比如一个  JNDI树 或者一个Sessiontable(会话表),在这些情况下,必须注意管理存储 库的大小,必须有某种机制从存储库中移除不再需要的数据          解决: [1] 常用的解决方法是周期运作清除作业,该作业会验证仓库中的数据然后 清楚一切不需要的数据                    [2] 另外一种方式是反向链接计数,集合负责统计集合中每个入口的反向链 接数据,这要求反向链接告诉集合合适会退出入口,当反向链接数目为零的时 候,该元素就可以移除了

    (2) 缓存: 缓存一种用来快速查找已经执行过的操作结果的数据结构。因此,如果一 个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据 的操作结果进行缓存,以便在下次调用该操作时使用缓存的数据。缓存通常都 是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话则会出现内存溢 出的后果,因此需要将所使用的内存容量与检索数据的速度加以平衡         解决: [1] 常用的解决途径是使用 java.lang.ref.SoftReference类 坚持将对象放入缓 存,这个方法可以保证当虚拟机用完内存或者需要更多堆的时候,可以释放这 些对象的引用

    (3) 类加载器: Java类装载器的使用为内存泄漏提供了许多可乘之机。一般来说类装载器 都具有复杂结构,因为类装载器不仅仅是只与”常规”对象引用有关,同时也和 对象内部的引用有关。比如数据变量, 方法和各种类。这意味着只要存在对数 据变量,方法,各种类和对象的类装载器,那么类装载器将驻留在JVM中。既 然类装载器可以同很多的类关联,同时也可以和静态数据变量关联,那么相当 多的内存就可能发生泄漏

3> Java引用 Java中的对象引用主要有以下几种类型:     (1) 强可及对象( strongly reachable): 可以通过强引用访问的对象,一般来说,平时写代码的方式都是使用 的强引用对象,比如下边的代码段: StringBuilder builder = new StringBuilder(); 上边代码部分引用obj这个引用将引用内存堆中的一个对象,这种情况 下,只要 obj 的引用存在,垃圾回收器就永远不会释放该对象的存储空间。这 种对象我们又成为强引用( Strong references) ,这种强引用方式就是Java语 言的原生的Java引用,几乎每天编程的时候都用到。上边代码JVM存储 了一个StringBuilder 类型的对象的强引用在变量builder。强引用和GC的交 互是这样的, 如果一个对象通过强引用可达或者通过强引用链可达的话这种对 象就成为强可及对象,这种情况下的对象垃圾回收器不予理睬。如果开发 过程不需要垃圾回器回收该对象,就直接将该对象赋为强引用,也是普通的编 程方法     (2) 软可及对象( softly reachable): 不通过强引用访问的对象,即不是强可及对象,但是可以通过软引用访问 的对象就成为软可及对象,软可及对象就需要使用类SoftReference ( java.lang.ref.SoftReference)。此种类型的引用主要用于内存比较敏感的高速 缓存,而且此种引用还是具有较强的引用功能,当内存不够的时候 GC 会回收 这类内存,因此如果内存充足的时候,这种引用通常不会被回收的。不仅仅如 此,这种引用对象在 JVM 里面保证在抛 OutOfMemory 异常之前,设置成为 null。通俗地讲,这种类型的引用保证在 JVM 内存不足的时候全部被清除,但 是有个关键在于: 垃圾收集器在运行时是否释放软可及对象是不确定的,而且 使用垃圾回收算法并不能保证一次性寻找到所有的软可及对象。当垃圾回收器 每次运行的时候都可以随意释放不是强可及对象占用的内存,如果垃圾回收器 找到了软可及对象过后,可能会进行以下操作: 将 SoftReference对象 的 referent域设置成为null,从而使该对象不再引用heap 对象。 SoftReference引用过的内存堆上的对象一律被生命为finalizable。 当内存堆上的对象finalize()方法被运行而且该对象占用的内存被释放, SoftReference对象就会被添加到它的 ReferenceQueue,前提条件是 ReferenceQueue本身是存在的     (3) 弱可及对象( weakly reachable): 不是强可及对象同样也不是软可及对象,仅仅通过弱引用 WeakReference ( java.lang.ref.WeakReference)访问的对象,这种对象的用途在于规范化映射 ( canonicalized mapping) ,对于生存周期相对比较长而且重新创建的时候开 销少的对象,弱引用也比较有用,和软引用对象不同的是,垃圾回收器如果碰 到了弱可及对象,将释放 WeakReference对象的内存,但是垃圾回收器需要运 行很多次才能够找到弱可及对象。弱引用对象在使用的时候,可以配合  ReferenceQueue类 使用,如果弱引用被回收, JVM就会把这个弱引用加入到相 关的引用队列中去。最简单的弱引用方法如以下代码: WeakReference weakWidget = new WeakReference(classA); 在上边代码里面,当使用 weakWidget.get()来获取 classA的时候, 由 于弱引用本身是无法阻止垃圾回收的,所以也许会拿到一个 null为返回
注:这里提供一个小技巧,如果我们希望取得某个对象的信息,但是又不影响 该对象的垃圾回收过程,就可以使用 WeakReference来记住该对象,一般
在开发调试器和优化器的时候使用这个是很好的一个手段 通过 weakWidget.get() 返回的是 null就证明该对 象已经被垃圾回收器回收了,而这种情况下弱引用对象就失去了使用价值, GC 就会定义为需要进行清除工作。这种情况下弱引用无法引用任何对象,所以在 JVM里面就成为了一个死引用,这就是为什么有时候需要通过  ReferenceQueue类来配合使用的原因,使用了 ReferenceQueue过后,就使得更加容易监视该引用的对象,如果通过一 ReferenceQueue类来构造一个 弱引用,当弱引用的对象已经被回收的时候,系统将自动使用对象引用队列来 代替对象引用,而且可以通过 ReferenceQueue类的运行来决定是否真正要 从垃圾回收器里面将该死引用( Dead Reference)清除。 弱引用代码段:
// 创建普通引用对象 MyObject object = new MyObject();
// 创建一个引用队列 ReferenceQueue rq = new ReferenceQueue();
// 使用引用队列创建 MyObject的弱引用 WeakReference wr= new WeakReference(object,rq); 这里提供两个实在的场景来描述弱引用的相关用法:         [1] 你想给对象附加一些信息,于是你用一个 Hashtable 把对象和附加信息 关联起来。你不停的把对象和附加信息放入 Hashtable 中,但是当对象用完的 时候,你不得不把对象再从 Hashtable 中移除,否则它占用的内存变不会释 放。万一你忘记了,那么没有从 Hashtable 中移除的对象也可以算作是内存泄 漏。理想的状况应该是当对象用完时, Hashtable 中的对象会自动被垃圾收集器 回收,不然你就是在做垃圾回收的工作         [2] 你想实现一个图片缓存,因为加载图片的开销比较大。你将图片对象的 引用放入这个缓存,以便以后能够重新使用这个对象。但是你必须决定缓存中 的哪些图片不再需要了,从而将引用从缓存中移除。不管你使用什么管理缓存 的算法,你实际上都在处理垃圾收集的工作,更简单的办法(除非你有特殊的 需求,这也应该是最好的办法)是让垃圾收集器来处理,由它来决定回收哪个 对象。 当Java回收器遇到了弱引用的时候有可能会执行以下操作: 将WeakReference 对象的 referent域设置成为null,从而使该对象不再引用heap 对象。WeakReference引用过的内存堆上的对象一律被生命为 finalizable。 当内存堆上的对象 finalize()方法被运行而且该对象占用的内存被释放, WeakReference对象 就会被添加到它的ReferenceQueue,前提条件是  ReferenceQueue 本身是存在的     (4) 清除:当引用对象的referent域设置为null,并且引用类在内存堆中引用的对象声 明为可结束的时候,该对象就可以清除,清除不做过多的讲述     (5) 虚可及对象( phantomlyreachable): 不是强可及对象,也不是软可及对象,同样不是弱可及对象,之所以把虚 可及对象放到最后来讲,主要也是因为它的特殊性,有时候我们又称之为“幽灵 对象”,已经结束的,可以通过虚引用来访问该对象。我们使用类 PhantomReference(java.lang.ref.PhantomReference)来访问,这个类只能用于跟 踪被引用对象进行的收集,同样的,可以用于执行 per-mortern清除操作。 PhantomReference必须与ReferenceQueue类一起使用。需要使用 ReferenceQueue是因为它能够充当通知机制,当垃圾收集器确定了某个对象是 虚可及对象的时候, PhantomReference对象就被放在了它的ReferenceQueue 上,这就是一个通知,表明PhantomReference引用的对象已经结束,可以收集 了,一般情况下我们刚好在对象内存在回收之前采取该行为。这种引用不同于 弱引用和软引用,这种方式通过get()获取到的对象总是返回null,仅仅当这些 对象在ReferenceQueue队列里面的时候,我们可以知道它所引用的哪些对对象 是死引用( DeadReference)。而这种引用和弱引用的区别在于:              弱引用( WeakReference) 是在对象不可达的时候尽快进入  ReferenceQueue队列的,在finalization方法执行和垃圾回收之前是确实会发生 的,理论上这类对象是不正确的对象,但是 WeakReference对象可以继续保持 Dead状态             虚引用( PhantomReference) 是在对象确实已经从物理内存中移除过后才 进入的ReferenceQueue队列,而且get()方法会一直返回null 当垃圾回收器遇到了虚引用的时候将有可能执行以下操作: PhantomReference引用过的heap对象声明为finalizable; 虚引用在堆对象释放之前就添加到了它的ReferenceQueue里面,这种情况使得 可以在堆对象被回收之前采取操作
注:再次提醒, PhantomReference对象 必须经过关联的ReferenceQueue来创建,就是说必须和ReferenceQueue类配合 操作 引用避过了finalize()方法,因为对于此方法的执行而言,虚引用真正引用到 的对象是异常对象,若在该方法内要使用对象只能重建。一般情况垃圾回收器 会轮询两次,一次标记为finalization,第二次进行真实的回收,而往往标记工 作不能实时进行,或者垃圾回收其会等待一个对象去标记 finalization。这种情 况很有可能引起MemoryOut,而使用虚引用这种情况就会完全避免。 因为虚引 用在引用对象的过程不会去使得这个对象由 Dead复活,而且这种对象是可以 在回收周期进行回收的。 在JVM内部,虚引用比起使用finalize()方法更加安全一点而且更加有效。 而finaliaze()方法回收在虚拟机里面实现起来相对简单,而且也可以处理大部分工作,所以我们仍然使用这种方式来进行对象回收的扫尾操作,但是有了虚引 用过后我们可以选择是否手动操作该对象使得程序更加高效完美

4> 防止内存泄漏     (1) 使用软引用阻止泄漏: 在 Java语言中有一种形式的内存泄漏称为对象游离( Object  Loitering)     (2)  垃圾回收对引用的处理: 弱引用和软引用都扩展了抽象的 Reference 类虚引用( phantom  references), 引用对象被垃圾收集器特殊地看待。垃圾收集器在跟踪堆期间遇 到一个 Reference 时,不会标记或跟踪该引用对象,而是在已知活跃的 Reference 对象的队列上放置一个 Reference。在跟踪之后,垃圾收集器就识别 软可及的对象——这些对象上除了软引用外,没有任何强引用。垃圾收集器然后根据当前收集所回收的内存总量和其他策略考虑因素,判断软引用此时是否 需要被清除。将被清除的软引用如果具有相应的引用队列,就会进入队列。 其 余的软可及对象(没有清除的对象) 然后被看作一个根集( root set) ,堆跟踪 继续使用这些新的根,以便通过活跃的软引用而可及的对象能够被标记。 处理 软引用之后,弱可及对象的集合被识别 —— 这样的对象上不存在强引用或软 引用。这些对象被清除和加入队列。所有 Reference 类型在加入队列之前被 清除,所以处理事后检查( post-mortem)清除的线程永远不会具有 referent 对象的访问权,而只具有Reference 对象的访问权。因此,当 References 与引用队列一起使用时,通常需要细分适当的引用类型,并将它 直接用于您的设计中(与 WeakHashMap 一样,它的 Map.Entry 扩展了 WeakReference)或者存储对需要清除的实体的引用     (3) 使用弱引用堵住内存泄漏: [1] 全局 Map造成的内存泄漏: 无意识对象保留最常见的原因是使用 Map 将元数据与临时对象 ( transient object) 相关联。假定一个对象具有中等生命周期,比分配它的那 个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字 连接。需要将一些元数据与这个套接字关联,如生成连接的用户的标识。在创 建 Socket 时是不知道这些信息的,并且不能将数据添加到 Socket 对象上,因 为不能控制 Socket 类或者它的子类。这时,典型的方法就是在一个全局 Map 中存储这些信息                                                 [2] 弱引用内存泄漏代码: 程序有内存泄漏的第一个迹象通常是它抛出一个 OutOfMemoryError,或 者因为频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集可以提供能 够用来诊断内存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 选项调用 JVM,那么每次 GC 运行时在控制台上或者日志文件中会打印出一个诊断信 息,包括它所花费的时间、当前堆使用情况以及恢复了多少内存。记录 GC 使 用情况并不具有干扰性,因此如果需要分析内存问题或者调优垃圾收集器,在 生产环境中默认启用 GC 日志是值得的。 有工具可以利用 GC 日志输出并以 图形方式将它显示出来, JTune 就是这样的一种工具。观察 GC 之后堆大小 的图,可以看到程序内存使用的趋势。对于大多数程序来说,可以将内存使用 分为两部分: baseline 使用和 current load 使用。对于服务器应用程序, baseline 使用就是应用程序在没有任何负荷、但是已经准备好接受请求时的 内存使用, current load 使用是在处理请求过程中使用的、但是在请求处理 完成后会释放的内存。只要负荷大体上是恒定的,应用程序通常会很快达到一 个稳定的内存使用水平。如果在应用程序已经完成了其初始化并且负荷没有增 加的情况下,内存使用持续增加,那么程序就可能在处理前面的请求时保留了 生成的对象                                                 [3] 使用弱引用堵住内存泄漏: SocketManager 的问题是 Socket-User 映射的生命周期应当与 Socket 的生 命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序 不得不使用人工内存管理的老技术。幸运的是,从 JDK1.2 开始,垃圾收集器 提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助 我们防止这种内存泄漏——利用弱引用。弱引用是对一个对象(称为 referent) 的引用的持有者。 使用弱引用后,可以维持对 referent 的引用,而不 会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只 有弱引用,那么这个 referent 就会成为垃圾收集的候选对象,就像没有任何剩 余的引用一样,而且所有剩余的弱引用都被清除。 (只有弱引用的对象称为弱 可及( weaklyreachable)) WeakReference 的 referent 是在构造时设置的,在 没有被清除之前,可以用 get() 获取它的值。如果弱引用被清除了(不管是 referent 已经被垃圾收集了,还是有人调用了 WeakReference.clear()) , get() 会返回 null。相应地,在使用其结果之前,应当总是检查get() 是否返回一个 非 null 值,因为 referent 最终总是会被垃圾收集的。用一个普通的(强)引 用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命 周期一样长。如果不小心,那么它可能就与程序的生命周期一样——如果将一 个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时, 完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种 到达它的方法。弱引用对于构造弱集合最有用,如那些在应用程序的其余部分 使用对象期间存储关于这些对象的元数据的集合——这就是 SocketManager 类 所要做的工作。因为这是弱引用最常见的用法, WeakHashMap 也被添加到 JDK1.2 的类库中,它对键(而不是对值)使用弱引用。如果在一个普通 HashMap 中用一个对象作为键,那么这个对象在映射从 Map 中删除之前不能 被回收, WeakHashMap 使您可以用一个对象作为 Map 键,同时不会阻止这个 对象被垃圾收集。下边的代码给出了 WeakHashMap 的 get() 方法的一种可能 实现,它展示了弱引用的使用 调用 WeakReference.get() 时, 它返回一个对 referent 的强引用( 如果它 仍然存活的话) ,因此不需要担心映射在 while 循环体中消失,因为强引用会 防止它被垃圾收集。 WeakHashMap 的实现展示了弱引用的一种常见用法—— 一些内部对象扩展 WeakReference。其原因在下面一节讨论引用队列时会得到 解释。在向 WeakHashMap 中添加映射时,请记住映射可能会在以后“脱离”, 因为键被垃圾收集了。在这种情况下, get() 返回 null,这使得测试 get() 的返 回值是否为 null 变得比平时更重要了                                                 [4] 使用 WeakHashMap堵住泄漏:在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如下边代码所示。 (如果 SocketManager 需要线程安全, 那么可以用 Collections.synchronizedMap() 包装 WeakHashMap) 。当映射的 生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当 小心不滥用这种技术,大多数时候还是应当使用普通的 HashMap 作为 Map 的实

引用队列: WeakHashMap 用弱引用承载映射键,这使得应用程序不再使用键对象时 它们可以被垃圾收集, get() 实现可以根据 WeakReference.get() 是否返回 null 来区分死的映射和活的映射。 但是这只是防止 Map 的内存消耗在应用程序的 生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象 被收集后从 Map 中删除死项。 否则, Map 会充满对应于死键的项。虽然这对 于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被 收集了, Map.Entry 和值对象也不会被收集。可以通过周期性地扫描 Map,对 每一个弱引用调用 get(),并在 get() 返回 null 时删除那个映射而消除死映 射。但是如果 Map 有许多活的项,那么这种方法的效率很低。如果有一种方 法可以在弱引用的 referent 被垃圾收集时发出通知就好了,这就是引用队列的 作用。引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要 方法。弱引用有两个构造函数:一个只取 referent 作为参数,另一个还取引用 队列作为参数。如果用关联的引用队列创建弱引用,在 referent 成为 GC 候选 对象时,这个引用对象(不是referent) 就在引用清除后加入 到引用队列中。 之后,应用程序从引用队列提取引用并了解到它的 referent 已被收集,因此可 以进行相应的清理活动,如去掉已不在弱集合中的对象的项。(引用队列提供 了与 BlockingQueue 同样的出列模式 ——polled、 timedblocking 和 untimed  blocking。) WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大 多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联 的映射
《Java内存模型 详解》

SoftReference:假定垃圾回收器确定在某一时间点某个对象是软可到达对 象。这时,它可以选择自动清除针对该对象的所有软引用,以及通过强引用 链,从其可以到达该对象的针对任何其他软可到达对象的所有软引用。在同一 时间或晚些时候,它会将那些已经向引用队列注册的新清除的软引用加入队 列。 软可到达对象的所有软引用都要保证在虚拟机抛出 OutOfMemoryError 之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用 的顺序将不受任何约束。 然而,虚拟机实现不鼓励清除最近访问或使用过的软 引用。 此类的直接实例可用于实现简单缓存;该类或其派生的子类还可用于更 大型的数据结构,以实现更复杂的缓存。只要软引用的指示对象是强可到达对 象,即正在实际使用的对象,就不会清除软引用。例如,通过保持最近使用的 项的强指示对象,并由垃圾回收器决定是否放弃剩余的项,复杂的缓存可以防 止放弃最近使用的项。一般来说, WeakReference我们用来防止内存泄漏,保 证内存对象被VM回收

WeakReference:弱引用对象,它们并不禁止其指示对象变得可终结,并 被终结,然后被回收。弱引用最常用于实现规范化的映射。假定垃圾回收器确 定在某一时间点上某个对象是弱可到达对象。这时,它将自动清除针对此对象 的所有弱引用,以及通过强引用链和软引用,可以从其到达该对象的针对任何 其他弱可到达对象的所有弱引用。同时它将声明所有以前的弱可到达对象为可 终结的。在同一时间或晚些时候,它将那些已经向引用队列注册的新清除的弱 引用加入队列。 SoftReference多用作来实现cache机制,保证cache的有效 性

PhantomReference:虚引用对象,在回收器确定其指示对象可另外回收之 后,被加入队列。虚引用最常见的用法是以某种可能比使用 Java 终结机制更 灵活的方式来指派 pre-mortem 清除操作。如果垃圾回收器确定在某一特定时 间点上虚引用的指示对象是虚可到达对象,那么在那时或者在以后的某一时 间,它会将该引用加入队列。为了确保可回收的对象仍然保持原状,虚引用的 指示对象不能被检索:虚引用的 get 方法总是返回 null。与软引用和弱引用不 同,虚引用在加入队列时并没有通过垃圾回收器自动清除。通过虚引用可到达 的对象将仍然保持原状,直到所有这类引用都被清除,或者它们都变得不可到 达

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/chenshun123/article/details/50935326
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞