内存模型


1.JMM简介java

2.堆和栈c++

3.本机内存git

4.防止内存泄漏程序员

 

1.JMM简介算法

  i.内存模型概述编程

Java平台自动集成了线程以及多处理器技术,这种集成程度比Java之前诞生的计算机语言要厉害不少,该语言针对多种异构平台的平台独立性而使用的多线程技术支持也是具备开拓性的一面,有时候在开发Java同步和线程安全要求很严格的程序时,每每容易混淆的一个概念就是内存模型。究竟什么是内存模型?内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,这点没有错,可是编译器、运行库、处理器或者系统缓存能够有特权在变量指定内存位置存储或者取出变量的值。【JMM】(Java Memory Model的缩写)容许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了finalsynchronized明确请求了某些可见性的保证。bootstrap

1)JSR133数组

  在Java语言规范里面指出了JMM是一个比较开拓性的尝试,这种尝试视图定义一个一致的、跨平台的内存模型,可是它有一些比较细微并且很重要的缺点。其实Java语言里面比较容易混淆的关键字主要是synchronizedvolatile,也由于这样在开发过程当中每每开发者会忽略掉这些规则,这也使得编写同步代码比较困难。缓存

JSR133自己的目的是为了修复本来JMM的一些缺陷而提出的,其自己的制定目标有如下几个:安全

·            保留目前JVM的安全保证,以进行类型的安全检查

·            提供out-of-thin-air safety无中生有安全性,这样正确同步的应该被正式并且直观地定义

·            程序员要有信心开发多线程程序,固然没有其余办法使得并发程序变得很容易开发,可是该规范的发布主要目标是为了减轻程序员理解内存模型中的一些细节负担

·            提供大范围的流行硬件体系结构上的高性能JVM实现,如今的处理器在它们的内存模型上有着很大的不一样,JMM应该可以适合于实际的尽量多的体系结构而不以性能为代价,这也是Java跨平台型设计的基础

·            提供一个同步的习惯用法,以容许发布一个对象使他不用同步就可见,这种状况又称为初始化安全(initialization safety的新的安全保证

·            对现有代码应该只有最小限度的影响

2)同步、异步【这里仅仅指概念上的理解,不牵涉到计算机底层基础的一些操做】

  在系统开发过程,常常会遇到这几个基本概念,不管是网络通信、对象之间的消息通信仍是Web开发人员经常使用的Http请求都会遇到这样几个概念,常常有人提到Ajax是异步通信方式,那么究竟怎样的方式是这样的概念描述呢?

  同步:同步就是在发出一个功能调用的时候,在没有获得响应以前,该调用就不返回,按照这样的定义,其实大部分程序的执行都是同步调用的,通常状况下,在描述同步和异步操做的时候,主要是指代须要其余部件协做处理或者须要协做响应的一些任务处理。好比有一个线程A,在A执行的过程当中,可能须要B提供一些相关的执行数据,固然触发B响应的就是AB发送一个请求或者说对B进行一个调用操做,若是A在执行该操做的时候是同步的方式,那么A就会停留在这个位置等待B给一个响应消息,在B没有任何响应消息回来的时候,A不能作其余事情,只能等待,那么这样的状况,A的操做就是一个同步的简单说明。

  异步:异步就是在发出一个功能调用的时候,不须要等待响应,继续进行它该作的事情,一旦获得响应了事后给予必定的处理,可是不影响正常的处理过程的一种方式。好比有一个线程A,在A执行的过程当中,一样须要B提供一些相关数据或者操做,当AB发送一个请求或者对B进行调用操做事后,A不须要继续等待,而是执行A本身应该作的事情,一旦B有了响应事后会通知AA接受到该异步请求的响应的时候会进行相关的处理,这种状况下A的操做就是一个简单的异步操做。

3)可见性、可排序性

Java内存模型的两个关键概念:可见性(Visibility可排序性(Ordering

  开发过多线程程序的程序员都明白,synchronized关键字强制实施一个线程之间的互斥锁(相互排斥,该互斥锁防止每次有多个线程进入一个给定监控器所保护的同步语句块,也就是说在该状况下,执行程序代码所独有的某些内存是独占模式其余的线程是不能针对它执行过程所独占的内存进行访问的,这种状况称为该内存不可见。可是在该模型的同步模式中,还有另一个方面:JMM中指出了,JVM在处理该强制实施的时候能够提供一些内存的可见规则,在该规则里面,它确保当存在一个同步块时,缓存被更新,当输入一个同步块时,缓存失效。所以在JVM内部提供给定监控器保护的同步块之中,一个线程所写入的值对于其他全部的执行由同一个监控器保护的同步块线程来讲是可见的,这就是一个简单的可见性的描述。这种机器保证编译器不会把指令从一个同步块的内部移到外部,虽然有时候它会把指令由外部移动到内部JMM在缺省状况下不作这样的保证——只要有多个线程访问相同变量时必须使用同步。简单总结:

  可见性就是在多核或者多线程运行过程当中内存的一种共享模式,在JMM模型里面,经过并发线程修改变量值的时候,必须将线程变量同步回主存事后,其余线程才可能访问到。

*:简单讲,内存的可见性使内存资源能够共享,当一个线程执行的时候它所占有的内存,若是它占有的内存资源是可见的,那么这时候其余线程在必定规则内是能够访问该内存资源的,这种规则是由JMM内部定义的,这种状况下内存的该特性称为其可见性。】

  可排序性提供了内存内部的访问顺序,在不一样的程序针对不一样的内存块进行访问的时候,其访问不是无序的,好比有一个内存块,AB须要访问的时候,JMM会提供必定的内存分配策略有序地分配它们使用的内存,而在内存的调用过程也会变得有序地进行,内存的折中性质能够简单理解为有序性。而在Java多线程程序里面,JMM经过Java关键字volatile来保证内存的有序访问。

ii.JMM结构:

1)简单分析:

Java语言规范中提到过,JVM中存在一个主存区(Main MemoryJava Heap MemoryJava全部变量都是存在主存中的,对于全部线程进行共享,而每一个线程又存在本身的工做内存(Working Memory,工做内存中保存的是主存中某些变量的拷贝,线程对全部变量的操做并不是发生在主存区,而是发生在工做内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。而在多核处理器下,大部分数据存储在高速缓存中,若是高速缓存不通过内存的时候,也是不可见的一种表现。在Java程序中,内存自己是比较昂贵的资源,其实不只仅针对Java应用程序,对操做系统自己而言内存也属于昂贵资源,Java程序在性能开销过程当中有几个比较典型的可控制的来源。synchronizedvolatile关键字提供的内存中模型的可见性保证程序使用一个特殊的、存储关卡(memory barrier的指令,来刷新缓存,使缓存无效,刷新硬件的写缓存而且延迟执行的传递过程,无疑该机制会对Java程序的性能产生必定的影响。

JMM的最初目的,就是为了可以支持多线程程序设计的,每一个线程能够认为是和其余线程不一样的CPU上运行,或者对于多处理器的机器而言,该模型须要实现的就是使得每个线程就像运行在不一样的机器、不一样的CPU或者自己就不一样的线程上同样,这种状况实际上在项目开发中是常见的。对于CPU自己而言,不能直接访问其余CPU寄存器,模型必须经过某种定义规则来使得线程和线程在工做内存中进行相互调用而实现CPU自己对其余CPU、或者说线程对其余线程的内存中资源的访问,而表现这种规则的运行环境通常为运行该程序的运行宿主环境(操做系统、服务器、分布式系统等),而程序自己表现就依赖于编写该程序的语言特性,这里也就是说用Java编写的应用程序在内存管理中的实现就是遵循其部分原则,也就是前边说起到的JMM定义了Java语言针对内存的一些的相关规则。然而,虽然设计之初是为了可以更好支持多线程,可是该模型的应用和实现固然不局限于多处理器,而在JVM编译器编译Java编写的程序的时候以及运行期执行该程序的时候,对于CPU的系统而言,这种规则也是有效的,这就是是上边提到的线程和线程之间的内存策略。JMM自己在描述过程没有提过具体的内存地址以及在实现该策略中的实现方法是由JVM的哪个环节(编译器、处理器、缓存控制器、其余)提供的机制来实现的,甚至针对一个开发很是熟悉的程序员,也不必定可以了解它内部对于类、对象、方法以及相关内容的一些具体可见的物理结构。相反,JMM定义了一个线程与主存之间的抽象关系,其实从上边的图能够知道,每个线程能够抽象成为一个工做内存(抽象的高速缓存和寄存器),其中存储了Java的一些值,该模型保证了Java里面的属性、方法、字段存在必定的数学特性,按照该特性,该模型存储了对应的一些内容,而且针对这些内容进行了必定的序列化以及存储排序操做,这样使得Java对象在工做内存里面被JVM顺利调用,(固然这是比较抽象的一种解释)既然如此,大多数JMM的规则在实现的时候,必须使得主存和工做内存之间的通讯可以得以保证,并且不能违反内存模型自己的结构,这是语言在设计之处必须考虑到的针对内存的一种设计方法。这里须要知道的一点是,这一切的操做在Java语言里面都是依靠Java语言自身来操做的,由于Java针对开发人员而言,内存的管理在不须要手动操做的状况下自己存在内存的管理策略,这也是Java本身进行内存管理的一种优点。

[1]原子性(Atomicity):

  这一点说明了该模型定义的规则针对原子级别的内容存在独立的影响,对于模型设计最初,这些规则须要说明的仅仅是最简单的读取和存储单元写入的的一些操做,这种原子级别的包括——实例、静态变量、数组元素,只是在该规则中不包括方法中的局部变量。

[2]可见性(Visibility):

  在该规则的约束下,定义了一个线程在哪一种状况下能够访问另一个线程或者影响另一个线程,从JVM的操做上讲包括了从另一个线程的可见区域读取相关数据以及将数据写入到另一个线程内。

[3]可排序性(Ordering):

  该规则将会约束任何一个违背了规则调用的线程在操做过程当中的一些顺序,排序问题主要围绕了读取、写入和赋值语句有关的序列。

  若是在该模型内部使用了一致的同步性的时候,这些属性中的每个属性都遵循比较简单的原则:和全部同步的内存块同样,每一个同步块以内的任何变化都具有了原子性以及可见性,和其余同步方法以及同步块遵循一样一致的原则,并且在这样的一个模型内,每一个同步块不能使用同一个锁,在整个程序的调用过程是按照编写的程序指定指令运行的。即便某一个同步块内的处理可能会失效,可是该问题不会影响到其余线程的同步问题,也不会引发连环失效。简单讲:当程序运行的时候使用了一致的同步性的时候,每一个同步块有一个独立的空间以及独立的同步控制器和锁机制,而后对外按照JVM的执行指令进行数据的读写操做。这种状况使得使用内存的过程变得很是严谨!

  若是不使用同步或者说使用同步不一致这里能够理解为异步,但不必定是异步操做,该程序执行的答案就会变得极其复杂。并且在这样的状况下,该内存模型处理的结果比起大多数程序员所指望的结果而言就变得十分脆弱,甚至比起JVM提供的实现都脆弱不少。由于这样因此出现了Java针对该内存操做的最简单的语言规范来进行必定的习惯限制,排除该状况发生的作法在于:

JVM线程必须依靠自身来维持对象的可见性以及对象自身应该提供相对应的操做而实现整个内存操做的三个特性,而不是仅仅依靠特定的修改对象状态的线程来完成如此复杂的一个流程。

*:综上所属,JMMJVM内部实现的结构就变得相对复杂,固然通常的Java初学者能够不用了解得这么深刻。】

[4]三个特性的解析(针对JMM内部):

  原子性(Atomicity):

  访问存储单元内的任何类型的字段的值以及对其更新操做的时候,除开long类型和double类型,其余类型的字段是必需要保证其原子性的,这些字段也包括为对象服务的引用。此外,该原子性规则扩展能够延伸到基于longdouble的另外两种类型volatile longvolatile doublevolatilejava关键字),没有被volatile声明的long类型以及double类型的字段值虽然不保证其JMM中的原子性,可是是被容许的。针对non-long/non-double的字段在表达式中使用的时候,JMM的原子性有这样一种规则:若是你得到或者初始化该值或某一些值的时候,这些值是由其余线程写入,并且不是从两个或者多个线程产生的数据在同一时间戳混合写入的时候,该字段的原子性JVM内部是必须获得保证的。也就是说JMM在定义JVM原子性的时候,只要在该规则不违反的条件下,JVM自己不去理睬该数据的值是来自于什么线程,由于这样使得Java语言在并行运算的设计的过程当中针对多线程的原子性设计变得极其简单,并且即便开发人员没有考虑到最终的程序也没有太大的影响。再次解释一下:这里的原子性指的是原子级别的操做,好比最小的一块内存的读写操做,能够理解为Java语言最终编译事后最接近内存的最底层的操做单元,这种读写操做的数据单元不是变量的值,而是本机码,也就是前边在讲《Java基础知识》中提到的由运行器解释的时候生成的Native Code

  可见性(Visibility):

  当一个线程须要修改另外线程的可见单元的时候必须遵循如下原则:

·            一个写入线程释放的同步锁和紧随其后进行读取的读线程的同步锁是同一个
从本质上讲,释放锁操做强迫它的隶属线程释放锁的线程从工做内存中的写入缓存里面刷新(专业上讲这里不该该是刷新,能够理解为提供)数据(flush操做),而后获取锁操做使得另一个线程得到锁的线程直接读取前一个线程可访问域(也就是可见区域)的字段的值。由于该锁内部提供了一个同步方法或者同步块,该同步内容具备线程排他性这样就使得上边两个操做只能针对单一线程在同步内容内部进行操做,这样就使得全部操做该内容的单一线程具备该同步内容(加锁的同步方法或者同步块)内的线程排他性,这种状况的交替也能够理解为具备短暂记忆效应
这里须要理解的是同步双重含义使用锁机制容许基于高层同步协议进行处理操做,这是最基本的同步;同时系统内存(不少时候这里是指基于机器指令的底层存储关卡memory barrier,前边提到过)在处理同步的时候可以跨线程操做,使得线程和线程之间的数据是同步的这样的机制也折射出一点,并行编程相对于顺序编程而言,更加相似于分布式编程。后一种同步能够做为JMM机制中的方法在一个线程中运行的效果展现,注意这里不是多个线程运行的效果展现,由于它反应了该线程愿意发送或者接受的双重操做,而且使得它本身的可见区域能够提供给其余线程运行或者更新,从这个角度来看,使用消息传递能够视为相互之间的变量同步,由于相对其余线程而言,它的操做针对其余线程也是对等的。

·            一旦某个字段被申明为volatile,在任何一个写入线程在工做内存中刷新缓存的以前须要进行进一步的内存操做也就是说针对这样的字段进行当即刷新,能够理解为这种volatile不会出现通常变量的缓存操做,而读取线程每次必须根据前一个线程的可见域里面从新读取该变量的值,而不是直接读取

·            当某个线程第一次去访问某个对象的域的时候,它要么初始化该对象的值,要么从其余写入线程可见域里面去读取该对象的值这里结合上边理解,在知足某种条件下,该线程对某对象域的值的读取是直接读取,有些时候却须要从新读取。
这里须要当心一点的是,在并发编程里面,很差的一个实践就是使用一个合法引用去引用不彻底构造的对象这种状况在从其余写入线程可见域里面进行数据读取的时候发生频率比较高。从编程角度上讲,在构造函数里面开启一个新的线程是有必定的风险的,特别是该类是属于一个可子类化的类的时候。Thread.start由调用线程启动,而后由得到该启动的线程释放锁具备相同的短暂记忆效应,若是一个实现了Runnable接口的超类在子类构造子执行以前调用了Thread(this).start()方法,那么就可能使得该对象在线程方法run执行以前并没有被彻底初始化这样就使得一个指向该对象的合法引用去引用了不彻底构造的一个对象。一样的,若是建立一个新的线程T而且启动该线程,而后再使用线程T来建立对象X,这种状况就不能保证X对象里面全部的属性针对线程T都是可见的除非是在全部针对X对象的引用中进行同步处理,或者最好的方法是在T线程启动以前建立对象X

·            若一个线程终止,全部的变量值都必须从工做内存中刷到主存好比,若是一个同步线程由于另外一个使用Thread.join方法的线程而终止,那么该线程的可见域针对那个线程而言其发生的改变以及产生的一些影响是须要保证可知道的。

  注意:若是在同一个线程里面经过方法调用去传一个对象的引用是绝对不会出现上边说起到的可见性问题的。JMM保证全部上边的规定以及关于内存可见性特性的描述——一个特殊的更新、一个特定字段的修改都是某个线程针对其余线程的一个可见性的概念,最终它发生的场所在内存模型中Java线程和线程之间,至于这个发生时间能够是一个任意长的时间,可是最终会发生,也就是说,Java内存模型中的可见性的特性主要是针对线程和线程之间使用内存的一种规则和约定,该约定由JMM定义。

  不只仅如此,该模型还容许不一样步的状况下可见性特性。好比针对一个线程提供一个对象或者字段访问域的原始值进行操做,而针对另一个线程提供一个对象或者字段刷新事后的值进行操做。一样也有可能针对一个线程读取一个原始的值以及引用对象的对象内容,针对另一个线程读取一个刷新事后的值或者刷新事后的引用。

  尽管如此,上边的可见性特性分析的一些特征在跨线程操做的时候是有可能失败的,并且不可以避免这些故障发生。这是一个不争的事实,使用同步多线程的代码并不能绝对保证线程安全的行为,只是容许某种规则对其操做进行必定的限制,可是在最新的JVM实现以及最新的Java平台中,即便是多个处理器,经过一些工具进行可见性的测试发现实际上是不多发生故障的。跨线程共享CPU的共享缓存的使用,其缺陷就在于影响了编译器的优化操做,这也体现了强有力的缓存一致性使得硬件的价值有所提高,由于它们之间的关系在线程与线程之间的复杂度变得更高。这种方式使得可见度的自由测试显得更加不切实际,由于这些错误的发生极为罕见,或者说在平台上咱们开发过程当中根本碰不到。在并行程开发中,不使用同步致使失败的缘由也不只仅是对可见度的不良把握致使的,致使其程序失败的缘由是多方面的,包括缓存一致性、内存一致性问题等。

  可排序性(Ordering):

  可排序规则在线程与线程之间主要有下边两点:

·            从操做线程的角度看来,若是全部的指令执行都是按照普通顺序进行,那么对于一个顺序运行的程序而言,可排序性也是顺序的

·            从其余操做线程的角度看来,排序性如同在这个线程中运行在非同步方法中的一个间谍,因此任何事情都有可能发生。惟一有用的限制是同步方法和同步块的相对排序,就像操做volatile字段同样,老是保留下来使用

*:如何理解这里间谍的意思,能够这样理解,排序规则在本线程里面遵循了第一条法则,可是对其余线程而言,某个线程自身的排序特性可能使得它不定地访问执行线程的可见域,而使得该线程对自己在执行的线程产生必定的影响。举个例子,A线程须要作三件事情分别是A1A2A3,而B是另一个线程具备操做B1B2,若是把参考定位到B线程,那么对A线程而言,B的操做B1B2有可能随时会访问到A的可见区域,好比A有一个可见区域aA1就是把a修改称为1,可是B线程在A线程调用了A1事后,却访问了a而且使用B1或者B2操做使得a发生了改变,变成了2,那么当A按照排序性进行A2操做读取到a的值的时候,读取到的是2而不是1,这样就使得程序最初设计的时候A线程的初衷发生了改变,就是排序被打乱了,那么B线程对A线程而言,其身份就是间谍,并且须要注意到一点,B线程的这些操做不会和A之间存在等待关系,那么B线程的这些操做就是异步操做,因此针对执行线程A而言,B的身份就是非同步方法中的间谍。】

  一样的,这仅仅是一个最低限度的保障性质,在任何给定的程序或者平台,开发中有可能发现更加严格的排序,可是开发人员在设计程序的时候不能依赖这种排序,若是依赖它们会发现测试难度会成指数级递增,并且在复合规定的时候会由于不一样的特性使得JVM的实现由于不符合设计初衷而失败。

注意:第一点在JLSJava Language Specification)的全部讨论中也是被采用的,例如算数表达式通常状况都是从上到下、从左到右的顺序,可是这一点须要理解的是,从其余操做线程的角度看来这一点又具备不肯定性,对线程内部而言,其内存模型自己是存在排序性的。*:这里讨论的排序是最底层的内存里面执行的时候的NativeCode的排序,不是说按照顺序执行的Java代码具备的有序性质,本文主要分析的是JVM的内存模型,因此但愿读者明白这里指代的讨论单元是内存区。】

iii.原始JMM缺陷:

JMM最初设计的时候存在必定的缺陷,这种缺陷虽然现有的JVM平台已经修复,可是这里不得不说起,也是为了读者更加了解JMM的设计思路,这一个小节的概念可能会牵涉到不少更加深刻的知识,若是读者不能读懂没有关系先看了文章后边的章节再返回来看也能够。

1)问题1:不可变对象不是不可变的

  学过Java的朋友都应该知道Java中的不可变对象,这一点在本文最后讲解String类的时候也会说起,而JMM最初设计的时候,这个问题一直都存在,就是:不可变对象彷佛能够改变它们的值(这种对象的不可变指经过使用final关键字来获得保证),(Publis Service Reminder:让一个对象的全部字段都为final并不必定使得这个对象不可变——全部类型还必须是原始类型而不能是对象的引用。而不可变对象被认为不要求同步的。可是,由于在将内存写方面的更改从一个线程传播到另一个线程的时候存在潜在的延迟,这样就使得有可能存在一种竞态条件,即容许一个线程首先看到不可变对象的一个值,一段时间以后看到的是一个不一样的值。这种状况之前怎么发生的呢?在JDK 1.4中的String实现里,这儿基本有三个重要的决定性字段:对字符数组的引用、长度和描述字符串的开始数组的偏移量String就是以这样的方式在JDK 1.4中实现的,而不是只有字符数组,所以字符数组能够在多个StringStringBuffer对象之间共享,而不须要在每次建立一个String的时候都拷贝到一个新的字符数组里。假设有下边的代码:

String s1 = "/usr/tmp";

String s2 =s1.substring(4); // "/tmp"

  这种状况下,字符串s2将具备大小为4长度和偏移量,可是它将和s1共享“/usr/tmp”里面的同一字符数组,在String构造函数运行以前,Object的构造函数将用它们默认的值初始化全部的字段,包括决定性的长度和偏移字段。当String构造函数运行的时候,字符串长度和偏移量被设置成所须要的值。可是在旧的内存模型中,由于缺少同步,有可能另外一个线程会临时地看到偏移量字段具备初始默认值0,然后又看到正确的值4,结果是s2的值从“/usr”变成了“/tmp”,这并非咱们真正的初衷,这个问题就是原始JMM的第一个缺陷所在,由于在原始JMM模型里面这是合理并且合法的,JDK 1.4如下的版本都容许这样作。

2)问题2:从新排序的易失性和非易失性存储

  另外一个主要领域是与volatile字段的内存操做从新排序有关,这个领域中现有的JMM引发了一些比较混乱的结果。现有的JMM代表易失性的读和写是直接和主存打交道的,这样避免了把值存储到寄存器或者绕过处理器特定的缓存,这使得多个线程通常能看见一个给定变量最新的值。但是,结果是这种volatile定义并无最初想象中那样如愿以偿,而且致使了volatile的重大混乱。为了在缺少同步的状况下提供较好的性能,编译器、运行时和缓存一般是容许进行内存的从新排序操做的,只要当前执行的线程分辨不出它们的区别。(这就是within-threadas-if-serial semantics[线程内彷佛是串行]的解释)可是,易失性的读和写是彻底跨线程安排的,编译器或缓存不能在彼此之间从新排序易失性的读和写。遗憾的是,经过参考普通变量的读写,JMM容许易失性的读和写被重排序,这样觉得着开发人员不能使用易失性标志做为操做已经完成的标志。好比:

Map configOptions;

char[] configText;

volatile boolean initialized = false;

 

// 线程1

configOptions= new HashMap();

configText =readConfigFile(filename);

processConfigOptions(configText,configOptions);

initialized = true;

 

// 线程2

while(!initialized)

   sleep();

  这里的思想是使用易失性变量initialized担任守卫来代表一套别的操做已经完成了,这是一个很好的思想,可是不能在JMM下工做,由于旧的JMM容许非易失性的写(好比写到configOptions字段,以及写到由configOptions引用Map的字段中)与易失性的写一块儿从新排序,所以另一个线程可能会看到initializedtrue,可是对于configOptions字段或它所引用的对象尚未一个一致的或者说当前的针对内存的视图变量,volatile的旧语义只承诺在读和写的变量的可见性,而不承诺其余变量,虽然这种方法更加有效的实现,可是结果会和咱们设计之初截然不同。

 

2.堆和栈

i.Java内存管理简介:

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

  在之前的编程过程当中,手动内存管理带了计算机程序不可避免的错误,并且这种错误对计算机程序是毁灭性的,因此内存管理就成为了一个很重要的话题,可是针对大多数纯面向对象语言而言,好比Java,提供了语言自己具备的内存特性:自动化内存管理,这种语言提供了一个程序垃圾回收器(Garbage Collector[GC],自动内存管理提供了一个抽象的接口以及更加可靠的代码使得内存可以在程序里面进行合理的分配。最多见的状况就是垃圾回收器避免了悬挂引用的问题,由于一旦这些对象没有被任何引用可达的时候,也就是这些对象在JVM的内存池里面成为了避免可引用对象,该垃圾回收器会直接回收掉这些对象占用的内存,固然这些对象必须知足垃圾回收器回收的某些对象规则,而垃圾回收器在回收的时候会自动释放掉这些内存。不只仅如此,垃圾回收器一样会解决内存泄漏问题。

ii.详解堆和栈[图片以及部份内容来自《Inside JVM]

1)通用简介

[编译原理]学过编译原理的人都明白,程序运行时有三种内存分配策略:静态的、栈式的、堆式的

  静态存储——是指在编译时就可以肯定每一个数据目标在运行时的存储空间需求,于是在编译时就能够给它们分配固定的内存空间。这种分配策略要求程序代码中不容许有可变数据结构的存在,也不容许有嵌套或者递归的结构出现,由于它们都会致使编译程序没法计算准确的存储空间。

  栈式存储——该分配可成为动态存储分配,是由一个相似于堆栈的运行栈来实现的,和静态存储的分配方式相反,在栈式存储方案中,程序对数据区的需求在编译时是彻底未知的,只有到了运行的时候才能知道,可是规定在运行中进入一个程序模块的时候,必须知道该程序模块所须要的数据区的大小才能分配其内存。和咱们在数据结构中所熟知的栈同样,栈式存储分配按照先进后出的原则进行分配。

  堆式存储——堆式存储分配则专门负责在编译时或运行时模块入口处都没法肯定存储要求的数据结构的内存分配,好比可变长度串和对象实例,堆由大片的可利用块或空闲块组成,堆中的内存能够按照任意顺序分配和释放。

[C++语言]对比C++语言里面,程序占用的内存分为下边几个部分:

[1]栈区(Stack:由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操做方式相似于数据结构中的栈。咱们在程序中定义的局部变量就是存放在栈里,当局部变量的生命周期结束的时候,它所占的内存会被自动释放。

[2]堆区(Heap通常由程序员分配和释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式却是相似于链表。咱们在程序中使用c++中new或者c中的malloc申请的一块内存,就是在heap上申请的,在使用完毕后,是须要咱们本身动手释放的,不然就会产生“内存泄露”的问题。

  [3]全局区(静态区)(Static:全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另外一块区域。程序结束后由系统释放。

  [4]文字常量区:常量字符串就是放在这里的,程序结束后由系统释放。在Java中对应有一个字符串常量池。

  [5]程序代码区:存放函数体的二进制代码

2)JVM结构【堆、栈解析】:

  在Java虚拟机规范中,一个虚拟机实例的行为主要描述为:子系统内存区域数据类型指令,这些组件在描述了抽象的JVM内部的一个抽象结构。与其说这些组成部分的目的是进行JVM内部结构的一种支配,更多的是提供一种严格定义实现的外部行为,该规范定义了这些抽象组成部分以及相互做用的任何Java虚拟机执行所须要的行为。下图描述了JVM内部的一个结构,其中主要包括主要的子系统、内存区域,如同之前在《Java基础知识》中描述的:Java虚拟机有一个类加载器做为JVM的子系统,类加载器针对Class进行检测以鉴定彻底合格的类接口,而JVM内部也有一个执行引擎:

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

1)方法区(Method Area

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

——$Big-EndianLittle-Endian——

  程序存储数据过程当中,若是数据是跨越多个字节对象就必须有一种约定:

·            它的地址是多少:对于跨越多个字节的对象,通常它所占的字节都是连续的,它的地址等于它所占字节最低地址,这种状况链表可能存储的仅仅是表头

·            它的字节在内存中是如何组织的

  好比:int x,它的地址为0x100,那么它占据了内存中的0x1000x1010x1020x103四个字节,因此通常状况咱们以为int4个字节。上边只是内存组织的一种状况,多字节对象在内存中的组织有两种约定,还有一种状况:若一个整数为W位,它的表示以下:

  每一位表示为:[Xw-1,Xw-2,...,X1,X0]

  它的最高有效字节MSBMost Significant Byte为:[Xw-1,Xw-2,...,Xw-8]

  最低有效字节LSBLeast Significant Byte为:[X7,X6,...,X0]

  其他字节则位于LSBMSB之间

LSBMSB谁位于内存的最低地址,即表明了该对象的地址,这样就引出了Big-EndianLittle-Endian的问题,若是LSBMSB前,LSB是最低地址,则该机器是小端,反之则是大端。DESDigital Equipment Corporation,如今是Compaq公司的一部分)和Intel机器(x86平台)通常采用小端,IBMMotorolaPower PC)、Sun的机器通常采用大端。固然这种不能表明全部状况,有的CPU既能工做于小端、又能够工做于大端,好比ARMAlpha、摩托罗拉的PowerPC,这些状况根据具体的处理器型号有所不一样。可是大部分操做系统(WindowsFreeBSDLinux)通常都是Little Endian的,少部分系统(Mac OS)是Big Endian的,因此用什么方式存储还得依赖宿主操做系统环境。

  由上图能够看到,映射访问(32位地址的0”)主要是由寄存器到内存、由内存到寄存器的一种数据映射方式,Big-Endian在上图能够看出的原子内存单位(Atomic Unit在系统内存中的增加方向为从左到右,而Little-Endian的地址增加方向为从右到左。举个例子:

  若要存储数据0x0A0B0C0D

Big-Endian

  以8为一个存储单位,其存储的地址增加为:

  上图中能够看出MSB的值存储了0x0A,这种状况下数据的高位是从内存的低地址开始存储的,而后从左到右开始增加,第二位0x0B就是存储在第二位的,若是是按照16位为一个存储单位,其存储方式又为:

  则能够看到Big-Endian的映射地址方式为:

 

MSB在计算机中,最高有效位(MSB)是指位值的存储位置为转换为二进制数据后的最大值,MSB有时候在Big-Endian的架构中称为最左最大数据位,这种状况下再往左边的内存位则不是数据位了,而是有效位数位置的最高符号位,不只仅如此,MSB也能够对应一个二进制符号位的符号位补码标记:“1”的含义为负,“0”的含义为正。最高位表明了最重要字节,也就是说当某些多字节数据拥有了最大值的时候它就是存储的时候最高位数据的字节对应的内存位置:

Little-Endian

  与Big-Endian相对的就是Little-Endian的存储方式,一样按照8位为一个存储单位上边的数据0x0A0B0C0D存储格式为:

  能够看到LSB的值存储的0x0D,也就是数据的最低位是从内存的低地址开始存储的,它的高位是从右到左的顺序逐渐增长内存分配空间进行存储的,若是按照十六位为存储单位存储格式为:

  从上图能够看到最低的16位的存储单位里面存储的值为0x0C0D,接着才是0x0A0B,这样就能够看到按照数据从高位到低位在内存中存储的时候是从右到左进行递增存储的,实际上能够从写内存的顺序来理解,实际上数据存储在内存中无非在使用的时候是写内存读内存针对LSB的方式最好的书面解释就是向左增长来看待,若是真正在进行内存读写的时候使用这样的顺序,其意义就体现出来了:

  按照这种读写格式,0x0D存储在最低内存地址,而从右往左的增加就能够看到LSB存储的数据为0x0D,和初衷吻合,则十六位的存储就能够按照下边的格式来解释:

  实际上从上边的存储还会考虑到另一个问题,若是按照这种方式从右往左的方式进行存储,若是是遇到Unicode文字就和从左到右的语言显示方式相反。好比一个单词“XRAY”,使用Little-Endian的方式存储格式为:

  使用这种方式进行内存读写的时候就会发现计算机语言和语言自己的顺序会有冲突,这种冲突主要是以使用语言的人的习惯有关,而书面化的语言从左到右就能够知道其冲突是不可避免的。咱们通常使用语言的阅读方式都是从左到右,而低端存储(Little-Endian)的这种内存读写的方式使得咱们最终从计算机里面读取字符须要进行倒序,并且考虑另一个问题,若是是针对中文而言,一个字符是两个字节,就会出现总体顺序和每个位的顺序会进行两次倒序操做,这种方式真正在制做处理器的时候也存在一种计算上的冲突,而针对使用文字从左到右进行阅读的国家而言,从右到左的方式(Big-Endian)则会有这样的文字冲突,另一方面,尽管有不少国家使用语言是从右到左,可是仅仅和Big-Endian的方式存在冲突,这些国家毕竟占少数,因此能够理解的是,为何主流的系统都是使用的Little-Endian的方式

*:这里不解释Middle-Endian的方式以及Mixed-Endian的方式】

LSB在计算机中,最低有效位是一个二进制给予单位的整数,位的位置肯定了该数据是一个偶数仍是奇数LSB有时被称为最右位。在使用具体位二进制数以内,常见的存储方式就是每一位存储1或者0的方式,从0向上到1每一比特逢二进一的存储方式。LSB的这种特性用来指定单位位,而不是位的数字,而这种方式也有可能产生必定的混乱。

——以上是关于Big-EndianLittle-Endian的简单讲解——

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

  一样的,由于Java自己的自动内存管理,方法区也会被垃圾回收的,Java程序能够经过类扩展动态加载器对象,类能够成为未引用向垃圾回收器进行申请,若是一个类是未引用的,则该类就可能被卸载,

  而方法区针对具体的语言特性有几种信息是存储在方法区内的:

  【类型信息】

·            类型的彻底限定名(java.lang.String格式)

·            类型的彻底限定名的直接父类的彻底限定名(除非这个父类的类型是一个接口或者java.lang.Object

·            不论类型是一个类或者接口

·            类型的修饰符(例如publicabstractfinal

·            任何一个直接超类接口的彻底限定名的列表

  在JVM和类文件名的内部,类型名通常都是彻底限定名(java.lang.String)格式,在Java源文件里面,彻底限定名必须加入包前缀,而不是咱们在开发过程写的简单类名,而在方法上,只要是符合Java语言规范的类的彻底限定名均可以,而JVM可能直接进行解析,好比:java.lang.String)在JVM内部名称为java/lang/String,这就是咱们在异常捕捉的时候常常看到的ClassNotFoundException的异常里面类信息的名称格式

  除此以外,还必须为每一种加载过的类型在JVM内进行存储,下边的信息不存储在方法区内,下边的章节会一一说明

·            类型常量池

·            字段信息

·            方法信息

·            全部定义在Class内部的(静态)变量信息,除开常量

·            一个ClassLoader的引用

·            Class的引用

  【常量池】

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

  【字段信息】

  针对字段的类型信息,下边的信息是存储在方法区里面的:

·            字段名

·            字段类型

·            字段修饰符(public,private,protected,static,final,volatile,transient

  【方法信息】

  针对方法信息,下边信息存储在方法区上:

·            方法名

·            方法的返回类型(包括void

·            方法参数的类型数目以及顺序

·            方法修饰符(public,private,protected,static,final,synchronized,native,abstract

  针对非本地方法,还有些附加方法信息须要存储在方法区内:

·            方法字节码

·            方法中局部变量区的大小、方法栈帧

·            异常表

  【类变量】

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

  ClassLoader引用】

  对于每种类型的加载,JVM必须检测其类型是否符合了JVM的语言规范,对于经过类加载器加载的对象类型,JVM必须存储对类的引用,而这些针对类加载器的引用是做为了方法区里面的类型数据部分进行存储的。

  【类Class的引用】

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

  【方法表】

  为了提升访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,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引用以及返回值都占据了一个数组中的局部变量的条目,而byteshortchar则在存储到局部变量的时候是先转化成为int再进行操做的,则longdouble则是在这样一个数组里面使用了两个元素的空间大小,在局部变量里面存储基本数据类型的时候使用的就是这样的结构。举个例子:

class Example3a{

    public staticint runClassMethod(int i,long l,float f,double d,Object o,byte b)

   {

       return 0;

   }

    public int runInstanceMethod(char c,double d,short s,boolean b)

   {

       return 0;

   }

}

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

begin

iload_0 //将整数类型的局部变量0压入到内存栈里面

iload_1 //将整数类型的局部变量1压入到内存栈里面

iadd    //将两个变量出栈读取,而后进行相加操做,将结果从新压入栈中

istore_2 //将最终输出结果放在另一个局部变量里面

end

  综上所述,就是整个计算过程针对内存的一些操做内容,而总体的结构能够用下图来描述:

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

3)内存堆(Heap):

  当一个Java应用程序在运行的时候在程序中建立一个对象或者一个数组的时候,JVM会针对该对象和数组分配一个新的内存堆空间。可是在JVM实例内部,只存在一个内存堆实例,全部的依赖该JVMJava应用程序都须要共享该堆实例,而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必须可以经过这个引用快速从对象堆空间中去拿到该对象可以访问的数据内容。也就是说,堆空间内对象的存储结构必须为外围对象引用提供一种能够访问该对象以及控制该对象的接口使得引用可以顺利地调用该对象以及相关操做。所以,针对堆空间的对象,分配的内存中每每也包含了一些指向方法区的指针,由于从总体存储结构上讲,方法区彷佛存储了不少原子级别的内容,包括方法区内最原始最单一的一些变量:好比类字段、字段数据、类型数据等等。而JVM自己针对堆空间的管理存在两种设计结构:

  1】设计一:

  堆空间的设计能够划分为两个部分:一个处理池和一个对象池,一个对象的引用能够拿处处理池的一个本地指针,而处理池主要分为两个部分:一个指向对象池里面的指针以及一个指向方法区的指针。这种结构的优点在于JVM在处理对象的时候,更加可以方便地组合堆碎片以使得全部的数据被更加方便地进行调用。当JVM须要将一个对象移动到对象池的时候,它仅仅须要更新该对象的指针到一个新的对象池的内存地址中就能够完成了,而后在处理池中针对该对象的内部结构进行相对应的处理工做。不过这样的方法也会出现一个缺点就是在处理一个对象的时候针对对象的访问须要提供两个不一样的指针,这一点可能很差理解,其实能够这样讲,真正在对象处理过程存在一个根据时间戳有区别的对象状态,而对象在移动、更新以及建立的整个过程当中,它的处理池里面老是包含了两个指针,一个指针是指向对象内容自己,一个指针是指向了方法区,由于一个完整的对外的对象是依靠这两部分被引用指针引用到的,而咱们开发过程是不能够操做处理池的两个指针的,只有引用指针咱们能够经过外围编程拿到。若是Java是按照这种设计进行对象存储,这里的引用指针就是平时说起到的“Java的引用,只是JVM在引用指针还作了必定的封装,这种封装的规则是JVM自己设计的时候作的,它就经过这种结构在外围进行一次封装,好比Java引用不具有直接操做内存地址的能力就是该封装的一种限制规则。这种设计的结构图以下:

 

  2】设计二:

  另一种堆空间设计就是使用对象引用拿到的本地指针,将该指针直接指向绑定好的对象的实例数据,这些数据里面仅仅包含了一个指向方法区原子级别的数据去拿到该实例相关数据,这种状况下只须要引用一个指针来访问对象实例数据,可是这样的状况使得对象的移动以及对象的数据更新变得更加复杂。当JVM须要移动这些数据以及进行堆内存碎片的整理的时候,就必须直接更新该对象全部运行时的数据区,这种状况能够用下图进行表示:

JVM须要从一个对象引用来得到该引用可以引用的对象数据存在多个缘由,当一个程序试图将一个对象的引用转换成为另一个类型的时候,JVM就会检查两个引用指向的对象是否存在父子类关系,而且检查两个引用引用到的对象是否可以进行类型转换,并且全部这种类型的转换必须执行一样的一个操做:instanceof操做,在上边两种状况下,JVM都必需要去分析引用指向的对象内部的数据。当一个程序调用了一个实例方法的时候,JVM就必须进行动态绑定操做,它必须选择调用方法的引用类型,是一个基于类的方法调用仍是一个基于对象的方法调用,要作到这一点,它又要获取该对象的惟一引用才能够。无论对象的实现是使用什么方式来进行对象描述,都是在针对内存中关于该对象的方法表进行操做,由于使用这样的方式加快了实例针对方法的调用,并且在JVM内部实现的时候这样的机制使得其运行表现比较良好,因此方法表的设计在JVM总体结构中发挥了极其重要的做用。关于方法表的存在与否,在JVM规范里面没有严格说明,也有可能真正在实现过程只是一个抽象概念物理层它根本不存在,针对放发表实现对于一个建立的实例而言,它自己具备不过高的内存须要求,若是该实现里面使用了方法表,则对象的方法表应该是能够很快被外围引用访问到的。

  有一种办法就是经过对象引用链接到方法表的时候,以下图:

  该图代表,在每一个指针指向一个对象的时候,其实是使用的一个特殊的数据结构,这些特殊的结构包括几个部分:

·            一个指向该对象类全部数据的指针

·            该对象的方法表

  实际上从图中能够看出,方法表就是一个指针数组,它的每个元素包含了一个指针,针对每一个对象的方法均可以直接经过该指针在方法区中找到匹配的数据进行相关调用,而这些方法表须要包括的内容以下:

·            方法内存堆栈段空间中操做栈的大小以及局部变量

·            方法字节码

·            一个方法的异常表

  这些信息使得JVM足够针对该方法进行调用,在调用过程,这种结构也可以方便子类对象的方法直接经过指针引用到父类的一些方法定义,也就是说指针在内存空间以内经过JVM自己的调用使得父类的一些方法表也能够一样的方式被调用,固然这种调用过程避免不了两个对象之间的类型检查,可是这样的方式就使得继承的实现变得更加简单,并且方法表提供的这些数据足够引用对对象进行带有任何OO特征的对象操做。

  另一种数据在上边的途中没有显示出来,也是从逻辑上讲内存堆中的对象的真实数据结构——对象的。这一点可能须要关联到JMM模型中讲的进行理解。JVM中的每个对象都是和一个锁(互斥)相关联的,这种结构使得该对象能够很容易支持多线程访问,并且该对象的对象锁一次只能被一个线程访问。当一个线程在运行的时候具备某个对象的锁的时候,仅仅只有这个线程能够访问该对象的实例变量,其余线程若是须要访问该实例的实例变量就必须等待这个线程将它占有的对象锁释放事后才可以正常访问,若是一个线程请求了一个被其余线程占有的对象锁,这个请求线程也必须等到该锁被释放事后才可以拿到这个对象的对象锁。一旦这个线程拥有了一个对象锁事后,它本身能够屡次向同一个锁发送对象的锁请求,可是若是它要使得被该线程锁住的对象能够被其余锁访问到的话就须要一样的释放锁的次数,好比线程A请求了对象B的对象锁三次,那么A将会一直占有B对象的对象锁,直到它将该对象锁释放了三次。

  不少对象也可能在整个生命周期都没有被对象锁锁住过,在这样的状况下对象锁相关的数据是不须要对象内部实现的,除非有线程向该对象请求了对象锁,不然这个对象就没有该对象锁的存储结构。因此上边的实现图能够知道,不少实现不包括指向对象锁的锁数据,锁数据的实现必需要等待某个线程向该对象发送了对象锁请求事后,并且是在第一次锁请求事后才会被实现。这个结构中,JVM却可以间接地经过一些办法针对对象的锁进行管理,好比把对象锁放在基于对象地址的搜索树上边。实现了锁结构的对象中,每个Java对象逻辑上都在内存中成为了一个等待集,这样就使得全部的线程在锁结构里面针对对象内部数据能够独立操做,等待集就使得每一个线程可以独立于其余线程去完成一个共同的设计目标以及程序执行的最终结果,这样就使得多线程的线程独享数据以及线程共享数据机制很容易实现。

  不只仅如此,针对内存堆对象还必须存在一个对象的镜像,该镜像的主要目的是提供给垃圾回收器进行监控操做,垃圾回收器是经过对象的状态来判断该对象是否被应用,一样它须要针对堆内的对象进行监控。而当监控过程垃圾回收器收到对象回收的事件触发的时候,虽然使用了不一样的垃圾回收算法,不论使用什么算法都须要经过独有的机制来判断对象目前处于哪一种状态,而后根据对象状态进行操做。开发过程程序员每每不会去仔细分析当一个对象引用设置成为null了事后虚拟机内部的操做,但实际上Java里面的引用每每不像咱们想像中那么简单,Java引用中的虚引用、弱引用就是使得Java引用在显示提交可回收状态的状况下对内存堆中的对象进行的反向监控,这些引用能够监视到垃圾回收器回收该对象的过程。垃圾回收器自己的实现也是须要内存堆中的对象可以提供相对应的数据的。其实这个位置到底JVM里面是否使用了完整的Java对象的镜像仍是使用的一个镜像索引我没有去仔细分析过,总之是在堆结构里面存在着堆内对象的一个相似拷贝的镜像机制,使得垃圾回收器可以顺利回收再也不被引用的对象。

4)内存栈和内存堆的实现原理探测【该部分为不肯定概念】:

  实际上不管是内存栈结构、方法区仍是内存堆结构,归根到底使用的是操做系统的内存,操做系统的内存结构能够理解为内存块,经常使用的抽象方式就是一个内存堆栈,而JVMOS上边安装了事后,就在启动Java程序的时候按照配置文件里面的内容向操做系统申请内存空间,该内存空间会按照JVM内部的方法提供相应的结构调整。

  内存栈应该是很容易理解的结构实现,通常状况下,内存栈是保持连续的,可是不绝对,内存栈申请到的地址实际上不少状况下都是连续的,而每一个地址的最小单位是按照计算机位来算的,该计算机位里面只有两种状态10,而内存栈的使用过程就是典型的相似C++里面的普通指针结构的使用过程,直接针对指针进行++或者--操做就修改了该指针针对内存的偏移量,而这些偏移量就使得该指针能够调用不一样的内存栈中的数据。至于针对内存栈发送的指令就是常见的计算机指令,而这些指令就使得该指针针对内存栈的栈帧进行指令发送,好比发送操做指令、变量读取等等,直接就使得内存栈的调用变得更加简单,并且栈帧在接受了该数据事后就知道到底针对栈帧内部的哪个部分进行调用,是操做帧、数据帧仍是局部变量。

  内存堆实际上在操做系统里面使用了双向链表的数据结构,双向链表的结构使得即便内存堆不具备连续性,每个堆空间里面的链表也能够进入下一个堆空间,而操做系统自己在整理内存堆的时候会作一些简单的操做,而后经过每个内存堆的双向链表就使得内存堆更加方便。并且堆空间不须要有序,甚至说有序不影响堆空间的存储结构,由于它归根究竟是在内存块上边进行实现的,内存块自己是一个堆栈结构,只是该内存堆栈里面的块如何分配不禁JVM决定,是由操做系统已经最开始分配好了,也就是最小存储单位。而后JVM拿到从操做系统申请的堆空间事后,先进行初始化操做,而后就能够直接使用了。

  常见的对程序有影响的内存问题主要是两种:溢出和内存泄漏,上边已经讲过了内存泄漏,其实从内存的结构分析,泄漏这种状况很难甚至说不可能发生在栈空间里面,其主要缘由是栈空间自己很难出现悬停的内存,由于栈空间的存储结构有多是内存的一个地址数组,因此在访问栈空间的时候使用的都是索引或者下标或者就是最原始的出栈和入栈的操做,这些操做使得栈里面很难出现像堆空间同样的内存悬停(也就是引用悬挂问题。堆空间悬停的内存是由于栈中存放的引用的变化,其实引用能够理解为从栈到堆的一个指针,当该指针发生变化的时候,堆内存碎片就有可能产生,而这种状况下在原始语言里面就常常发生内存泄漏的状况,由于这些悬停的堆空间在系统里面是不可以被任何本地指针引用到,就使得这些对象在未被回收的时候脱离了可操做区域而且占用了系统资源。

  栈溢出问题一直都是计算机领域里面的一个安全性问题,这里不作深刻讨论,说多了就偏离主题了,而内存泄漏是程序员最容易理解的内存问题,还有一个问题来自于我一个黑客朋友就是:堆溢出现象,这种现象可能更加复杂。

  其实Java里面的内存结构,最初看来就是堆和栈的结合,实际上能够这样理解,实际上对象的实际内容才存在对象池里面,而有关对象的其余东西有可能会存储于方法区,而平时使用的时候的引用是存在内存栈上的,这样就更加容易理解它内部的结构,不只仅如此,有时候还须要考虑到Java里面的一些字段和属性究竟是对象域的仍是类域的,这个也是一个比较复杂的问题。

两者的区别简单总结一下:

·            管理方式:JVM本身能够针对内存栈进行管理操做,并且该内存空间的释放是编译器就能够操做的内容,而堆空间在JavaJVM自己执行引擎不会对其进行释放操做,而是让垃圾回收器进行自动回收

·            空间大小:通常状况下栈空间相对于堆空间而言比较小,这是由栈空间里面存储的数据以及自己须要的数据特性决定的,而堆空间在JVM堆实例进行分配的时候通常大小都比较大,由于堆空间在一个Java程序中须要存储太多的Java对象数据

·            碎片相关:针对堆空间而言,即便垃圾回收器可以进行自动堆内存回收,可是堆空间的活动量相对栈空间而言比较大,颇有可能存在长期的堆空间分配和释放操做,并且垃圾回收器不是实时的,它有可能使得堆空间的内存碎片主键累积起来。针对栈空间而言,由于它自己就是一个堆栈的数据结构,它的操做都是一一对应的,并且每个最小单位的结构栈帧和堆空间内复杂的内存结构不同,因此它通常在使用过程不多出现内存碎片。

·            分配方式:通常状况下,栈空间有两种分配方式:静态分配和动态分配,静态分配是自己由编译器分配好了,而动态分配可能根据状况有所不一样,而堆空间倒是彻底的动态分配的,是一个运行时级别的内存分配。而栈空间分配的内存不须要咱们考虑释放问题,而堆空间即便在有垃圾回收器的前提下仍是要考虑其释放问题。

·            效率:由于内存块自己的排列就是一个典型的堆栈结构,因此栈空间的效率天然比起堆空间要高不少,并且计算机底层内存空间自己就使用了最基础的堆栈结构使得栈空间和底层结构更加符合,它的操做也变得简单就是最简单的两个指令:入栈和出栈;栈空间针对堆空间而言的弱点是灵活程度不够,特别是在动态管理的时候。而堆空间最大的优点在于动态分配,由于它在计算机底层实现多是一个双向链表结构,因此它在管理的时候操做比栈空间复杂不少,天然它的灵活度就高了,可是这样的设计也使得堆空间的效率不如栈空间,并且低不少。

 

3.本机内存[部份内容来源于IBM开发中心]

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

i.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中的类,也能够使用第三方库。这些类须要存储在内存中以备使用。存储类的方式取决于具体实现。Sun JDK 使用永久生成(permanent generationPermGen)堆区域,从最基本的层面来看,使用更多的类将须要使用更多内存。(这可能意味着您的本机内存使用量会增长,或者您必须明确地从新设置 PermGen 或共享类缓存等区域的大小,以装入全部类)。记住,不只您的应用程序须要加载到内存中,框架、应用服务器、第三方库以及包含类的 Java 运行时也会按需加载并占用空间。Java 运行时能够卸载类来回收空间,可是只有在很是严酷的条件下才会这样作,不能卸载单个类,而是卸载类加载器,随其加载的全部类都会被卸载。只有在如下状况下才能卸载类加载器

·            Java 堆不包含对表示该类加载器的 java.lang.ClassLoader 对象的引用。

·            Java 堆不包含对表示类加载器加载的类的任何 java.lang.Class 对象的引用。

·             Java 堆上,该类加载器加载的任何类的全部对象都再也不存活(被引用)。

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

4)JNI

JNI支持本机代码调用Java方法,反之亦然,Java运行时自己极大依赖于JNI代码来实现类库功能,好比文件和网络I/OJNI应用程序能够经过三种方式增长Java运行时对本机内存的使用:

·            JNI应用程序的本机代码被编译到共享库中,或编译为加载到进程地址空间中的可执行文件,大型本机应用程序可能仅仅加载就会占用大量进程地址空间

·            本机代码必须与Java运行时共享地址空间,任何本机代码分配本机代码执行内存映射都会耗用Java运行时内存

·            某些JNI函数可能在它们的常规操做中使用本机内存,GetTypeArrayElementsGetTypeArrayRegion函数能够将Java堆复制到本机内存缓冲区中,提供给本地代码使用,是否复制数据依赖于运行时实现,经过这种方式访问大量Java堆数据就可能使用大量的本机内存堆空间

5)NIO

JDK 1.4开始添加了新的I/O类,引入了一种基于通道和缓冲区执行I/O的新方式,就像Java堆上的内存支持I/O缓冲区同样,NIO添加了对直接ByteBuffer的支持,ByteBuffer受本机内存而不是Java堆的支持,直接ByteBuffer能够直接传递到本机操做系统库函数,以执行I/O,这种状况虽然提升了Java程序在I/O的执行效率,可是会对本机内存进行直接的内存开销。ByteBuffer直接操做和非直接操做的区别以下:

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

6)线程:

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

ii.本机内存耗尽:

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

iii.例子:

  这篇文章一致都在讲概念,这里既然提到了ByteBuffer,先提供一个简单的例子演示该类的使用:

——[$]使用NIO读取txt文件——

package org.susan.java.io;

 

import java.io.FileInputStream;

import java.io.IOException;

import java.nio.ByteBuffer;

import java.nio.channels.FileChannel;

 

public class ExplicitChannelRead{

    public static void main(String args[]){

       FileInputStream fileInputStream;

       FileChannel fileChannel;

       long fileSize;

       ByteBuffer byteBuffer;

       try{

           fileInputStream = new FileInputStream("D:\\read.txt");

           fileChannel =fileInputStream.getChannel();

           fileSize = fileChannel.size();

           byteBuffer = ByteBuffer.allocate((int)fileSize);

           fileChannel.read(byteBuffer);

           byteBuffer.rewind();

           forint i = 0; i <fileSize; i++ )

               System.out.print((char)byteBuffer.get());

           fileChannel.close();

           fileInputStream.close();

       }catch(IOException ex){

           ex.printStackTrace();

       }

   }

}

  在读取文件的路径放上该txt文件里面写入:Hello World,上边这段代码就是使用NIO的方式读取文件系统上的文件,这段程序的输入就为:

Hello World

——[$]获取ByteBuffer上的字节转换为Byte数组——

package org.susan.java.io;

 

import java.nio.ByteBuffer;

 

public class ByteBufferToByteArray{

    public static void main(String args[]) throws Exception{

       // byte数组建立ByteBuffer

       byte[] bytes = new byte[10];

       ByteBuffer buffer = ByteBuffer.wrap(bytes);

 

       // positionlimit,也就是ByteBuffer缓冲区的首尾之间读取字节

       bytes = new byte[buffer.remaining()];

       buffer.get(bytes, 0, bytes.length);

 

       // 读取全部ByteBuffer内的字节

       buffer.clear();

       bytes = new byte[buffer.capacity()];

       buffer.get(bytes, 0, bytes.length);

   }

}

  上边代码就是从ByteBufferbyte数组转换过程,有了这个过程在开发过程当中可能更加方便,ByteBuffer的详细讲解我保留到IO部分,这里仅仅是涉及到了一些,因此提供两段实例代码。

iv.共享内存:

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

1)共享内存特色:

·            能够被多个进程打开访问

·            读写操做的进程在执行读写操做的时候其余进程不能进行写操做

·            多个进程能够交替对某一个共享内存执行写操做

·            一个进程执行了内存写操做事后,不影响其余进程对该内存的访问,同时其余进程对更新后的内存具备可见性

·            在进程执行写操做时若是异常退出,对其余进程的写操做禁止自动解除

·            相对共享文件,数据访问的方便性和效率  

2)出现状况:

·            独占的写操做,相应有独占的写操做等待队列。独占的写操做自己不会发生数据的一致性问题;

·            共享的写操做,相应有共享的写操做等待队列。共享的写操做则要注意防止发生数据的一致性问题;

·            独占的读操做,相应有共享的读操做等待队列;

·            共享的读操做,相应有共享的读操做等待队列;

3)Java中共享内存的实现:

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

  下边代码段实现了上边说起的共享内存功能

// 得到一个只读的随机存取文件对象

RandomAccessFileRAFile = new RandomAccessFile(filename,"r");

// 得到相应的文件通道

FileChannel fc =RAFile.getChannel();

// 取得文件的实际大小

int size = (int)fc.size();

// 得到共享内存缓冲区,该共享内存只读 
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);

// 得到一个可读写的随机存取文件对象 
RAFile = new RandomAccessFile(filename,"rw");

// 得到相应的文件通道 
fc = RAFile.getChannel();

// 取得文件的实际大小,以便映像到共享内存 
size = (int)fc.size();

// 得到共享内存缓冲区,该共享内存可读写 
mapBuf = fc.map(FileChannel.MAP_RW,0,size);

// 获取头部消息:存取权限 

mode = mapBuf.getInt(); 

  若是多个应用映象使用同一文件名的共享内存,则意味着这多个应用共享了同一内存数据,这些应用对于文件能够具备同等存取权限,一个应用对数据的刷新会更新到多个应用中。为了防止多个应用同时对共享内存进行写操做,能够在该共享内存的头部信息加入写操做标记,该共享文件的头部基本信息至少有:

·            共享内存长度

·            共享内存目前的存取模式

  共享文件的头部信息是私有信息,多个应用能够对同一个共享内存执行写操做,执行写操做和结束写操做的时候,能够使用以下方法:

publicboolean startWrite()

{

    if(mode == 0// 这里mode表明共享内存的存取模式,为0表明可写

   {

       mode = 1; // 意味着别的应用不可写

       mapBuf.flip();

       mapBuf.putInt(mode);    //写入共享内存的头部信息

       return true;

   }

    else{

       return false//代表已经有应用在写该共享内存了,本应用不可以针对共享内存再作写操做

   }
}

 

public boolean stopWrite()

{

   mode = 0// 释放写权限

   mapBuf.flip();

   mapBuf.putInt(mode);    //写入共享内存头部信息

    return true;
}

*:上边提供了对共享内存执行写操做过程的两个方法,这两个方法其实理解起来很简单,真正须要思考的是一个针对存取模式的设置,其实这种机制和最前面提到的内存的锁模式有点相似,一旦当mode(存取模式)设置称为可写的时候,startWrite才能返回true,不只仅如此,某个应用程序在向共享内存写入数据的时候还会修改其存取模式,由于若是不修改的话就会致使其余应用一样针对该内存是可写的,这样就使得共享内存的实现变得混乱,而在中止写操做stopWrite的时候,须要将mode设置称为1,也就是上边注释段提到的释放写权限。】

  关于锁的知识这里简单作个补充*:上边代码的这种模式能够理解为一种简单的锁模式】:通常状况下,计算机编程中会常常遇到锁模式,在整个锁模式过程当中能够将锁分为两类(这里只是辅助理解,不是严格的锁分类)——共享锁排他锁(也称为独占锁),锁的定位是定位于针对全部与计算机有关的资源好比内存、文件、存储空间等,针对这些资源均可能出现锁模式。在上边堆和栈一节讲到了Java对象锁,其实不只仅是对象,只要是计算机中会出现写入和读取共同操做的资源,都有可能出现锁模式。

  共享锁——当应用程序得到了资源的共享锁的时候,那么应用程序就能够直接访问该资源,资源的共享锁能够被多个应用程序拿到,在Java里面线程之间有时候也存在对象的共享锁,可是有一个很明显的特征,也就是内存共享锁只能读取数据,不可以写入数据,不管是什么资源,当应用程序仅仅只能拿到该资源的共享锁的时候,是不可以针对该资源进行写操做的。

  独占锁——当应用程序得到了资源的独占锁的时候,应用程序访问该资源在共享锁上边多了一个权限就是写权限,针对资源自己而言,一个资源只有一把独占锁,也就是说一个资源只能同时被一个应用或者一个执行代码程序容许写操做Java线程中的对象写操做也是这个道理,若某个应用拿到了独占锁的时候,不只仅能够读取资源里面的数据,并且能够向该资源进行数据写操做。

  数据一致性——当资源同时被应用进行读写访问的时候,有可能会出现数据一致性问题,好比A应用拿到了资源R1的独占锁,B应用拿到了资源R1的共享锁,A在针对R1进行写操做,而两个应用的操做——A的写操做和B的读操做出现了一个时间差,s1的时候B读取了R1的资源,s2的时候A写入了数据修改了R1的资源,s3的时候B又进行了第二次读,而两次读取相隔时间比较短暂并且初衷没有考虑到AB的读取过程修改了资源,这种状况下针对锁模式就须要考虑到数据一致性问题。独占锁的排他性在这里的意思是该锁只能被一个应用获取,获取过程只能由这个应用写入数据到资源内部,除非它释放该锁,不然其余拿不到锁的应用是没法对资源进行写入操做的。

  按照上边的思路去理解代码里面实现共享内存的过程就更加容易理解了。

  若是执行写操做的应用异常停止,那么映像文件的共享内存将再也不能执行写操做。为了在应用异常停止后,写操做禁止标志自动消除,必须让运行的应用获知退出的应用。在多线程应用中,能够用同步方法得到这样的效果,可是在多进程中,同步是不起做用的。方法能够采用的多种技巧,这里只是描述一可能的实现:采用文件锁的方式。写共享内存应用在得到对一个共享内存写权限的时候,除了判断头部信息的写权限标志外,还要判断一个临时的锁文件是否能够获得,若是能够获得,则即便头部信息的写权限标志为1(上述),也能够启动写权限,其实这已经代表写权限得到的应用已经异常退出,这段代码以下:

// 打开一个临时文件,注意统一共享内存,该文件名必须相同,能够在共享文件名后边添加“.lock”后缀

RandomAccessFilefiles = new RandomAccessFile("memory.lock","rw");

// 获取文件通道

FileChannellockFileChannel = files.getChannel();

// 获取文件的独占锁,该方法不产生任何阻塞直接返回

FileLock fileLock =lockFileChannel.tryLock();

// 若是为空表示已经有应用占有了

if( fileLock == null ){

    // ...不可写

}else{

    // ...能够执行写操做

}

4)共享内存的应用:

  在Java中,共享内存通常有两种应用:

[1]永久对象配置——java服务器应用中,用户可能会在运行过程当中配置一些参数,而这些参数须要永久有效,当服务器应用从新启动后,这些配置参数仍然能够对应用起做用。这就能够用到该文中的共享内存。该共享内存中保存了服务器的运行参数和一些对象运行特性。能够在应用启动时读入以启用之前配置的参数。

[2]查询共享数据——一个应用(例 sys.java)是系统的服务进程,其系统的运行状态记录在共享内存中,其中运行状态多是不断变化的。为了随时了解系统的运行状态,启动另外一个应用(例 mon.java),该应用查询该共享内存,汇报系统的运行状态。

v.小节:

提供本机内存以及共享内存的知识,主要是为了让读者可以更顺利地理解JVM内部内存模型的物理原理,包括JVM如何和操做系统在内存这个级别进行交互,理解了这些内容就让读者对Java内存模型的认识会更加深刻,并且不容易遗忘。其实Java的内存模型远不及咱们想象中那么简单,并且其结构极端复杂,看过《Inside JVM》的朋友应该就知道,结合JVM指令集去写点小代码测试.class文件的里层结构也不失为一种好玩的学习方法。

4.防止内存泄漏

Java中会有内存泄漏,听起来彷佛是很不正常的,由于Java提供了垃圾回收器针对内存进行自动回收,可是Java仍是会出现内存泄漏的。

i.什么是Java中的内存泄漏:

  在Java语言中,内存泄漏就是存在一些被分配的对象,这些对象有两个特色:这些对象可达,即在对象内存的有向图中存在通路能够与其相连;其次,这些对象是无用的,即程序之后不会再使用这些对象了。若是对象知足这两个条件,该对象就能够断定为Java中的内存泄漏,这些对象不会被GC回收,然而它却占用内存,这就是Java语言中的内存泄漏Java中的内存泄漏和C++中的内存泄漏还存在必定的区别,在C++里面,内存泄漏的范围更大一些,有些对象被分配了内存空间,可是却不可达,因为C++中没有GC,这些内存将会永远收不回来,在Java中这些不可达对象则是被GC负责回收的,所以程序员不须要考虑这一部分的内存泄漏。两者的图以下:

  所以按照上边的分析,Java语言中也是存在内存泄漏的,可是其内存泄漏范围比C++要小不少,由于Java里面有个特殊程序回收全部的不可达对象:垃圾回收器。对于程序员来讲,GC基本是透明的,不可见的。虽然,咱们只有几个函数能够访问GC,例如运行GC的函数System.gc(),可是根据Java语言规范定义,该函数不保证JVM的垃圾收集器必定会执行。由于,不一样的JVM实现者可能使用不一样的算法管理GC。一般,GC的线程的优先级别较低,JVM调用GC的策略也有不少种,有的是内存使用到达必定程度时,GC才开始工做,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但一般来讲,咱们不须要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不但愿GC忽然中断应用程序执行而进行垃圾回收,那么咱们须要调整GC的参数,让GC可以经过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。

  举个例子:

——[$]内存泄漏的例子——

package org.susan.java.collection;

 

import java.util.Vector;

 

public class VectorMemoryLeak {

    public staticvoid main(String args[]){

       Vector<String> vector = newVector<String>();

       forint i = 0; i < 1000;i++ ){

           String tempString = new String();

           vector.add(tempString);

           tempString = null;

       }

   }

}

  从上边这个例子能够看到,循环申请了String对象,而且将申请的对象放入了一个Vector中,若是仅仅是释放对象自己,由于Vector仍然引用了该对象,因此这个对象对CG来讲是不可回收的,所以若是对象加入到Vector后,还必须从Vector删除才可以回收,最简单的方式是将Vector引用设置成null。实际上这些对象已经没有用了,可是仍是被代码里面的引用引用到了,这种状况GC拿它就没有了任何办法,这样就能够致使了内存泄漏。

*Java语言由于提供了垃圾回收器,照理说是不会出现内存泄漏的,Java里面致使内存泄漏的主要缘由就是,先前申请了内存空间而忘记了释放。若是程序中存在对无用对象的引用,这些对象就会驻留在内存中消耗内存,由于没法让GC判断这些对象是否可达。若是存在对象的引用,这个对象就被定义为有效的活动状态,同时不会被释放,要肯定对象所占内存被回收,必需要确认该对象再也不被使用。典型的作法就是把对象数据成员设置成为null或者中集合中移除,当局部变量不须要的状况则不须要显示声明为null。】

ii.常见的Java内存泄漏

1)全局集合:

  在大型应用程序中存在各类各样的全局数据仓库是很广泛的,好比一个JNDI树或者一个Session table(会话表),在这些状况下,必须注意管理存储库的大小,必须有某种机制从存储库中移除再也不须要的数据。

[$]解决:

[1]经常使用的解决方法是周期运做清除做业,该做业会验证仓库中的数据而后清楚一切不须要的数据

[2]另一种方式是反向连接计数,集合负责统计集合中每一个入口的反向连接数据,这要求反向连接告诉集合合适会退出入口,当反向连接数目为零的时候,该元素就能够移除了。

2)缓存:

  缓存一种用来快速查找已经执行过的操做结果的数据结构。所以,若是一个操做执行须要比较多的资源并会屡次被使用,一般作法是把经常使用的输入数据的操做结果进行缓存,以便在下次调用该操做时使用缓存的数据。缓存一般都是以动态方式实现的,若是缓存设置不正确而大量使用缓存的话则会出现内存溢出的后果,所以须要将所使用的内存容量与检索数据的速度加以平衡。

[$]解决:

[1]经常使用的解决途径是使用java.lang.ref.SoftReference类坚持将对象放入缓存,这个方法能够保证当虚拟机用完内存或者须要更多堆的时候,能够释放这些对象的引用。

3)类加载器:

Java类装载器的使用为内存泄漏提供了许多可乘之机。通常来讲类装载器都具备复杂结构,由于类装载器不只仅是只与"常规"对象引用有关,同时也和对象内部的引用有关。好比数据变量方法各类类。这意味着只要存在对数据变量,方法,各类类和对象的类装载器,那么类装载器将驻留在JVM中。既然类装载器能够同不少的类关联,同时也能够和静态数据变量关联,那么至关多的内存就可能发生泄漏。

iii.Java引用【摘录自前边的《Java引用总结》】

Java中的对象引用主要有如下几种类型:

1)强可及对象(strongly reachable):

  能够经过强引用访问的对象,通常来讲,咱们平时写代码的方式都是使用的强引用对象,好比下边的代码段:

StringBuilder builder= new StringBuilder()

  上边代码部分引用obj这个引用将引用内存堆中的一个对象,这种状况下,只要obj的引用存在,垃圾回收器就永远不会释放该对象的存储空间。这种对象咱们又成为强引用(Strong references,这种强引用方式就是Java语言的原生的Java引用,咱们几乎天天编程的时候都用到。上边代码JVM存储了一个StringBuilder类型的对象的强引用在变量builder呢。强引用和GC的交互是这样的,若是一个对象经过强引用可达或者经过强引用链可达的话这种对象就成为强可及对象,这种状况下的对象垃圾回收器不予理睬。若是咱们开发过程不须要垃圾回器回收该对象,就直接将该对象赋为强引用,也是普通的编程方法。

2)软可及对象(softly reachable):

  不经过强引用访问的对象,即不是强可及对象,可是能够经过软引用访问的对象就成为软可及对象,软可及对象就须要使用类SoftReferencejava.lang.ref.SoftReference)。此种类型的引用主要用于内存比较敏感的高速缓存,并且此种引用仍是具备较强的引用功能,当内存不够的时候GC会回收这类内存,所以若是内存充足的时候,这种引用一般不会被回收的。不只仅如此,这种引用对象在JVM里面保证在抛出OutOfMemory异常以前,设置成为null。通俗地讲,这种类型的引用保证在JVM内存不足的时候所有被清除,可是有个关键在于:垃圾收集器在运行时是否释放软可及对象是不肯定的,并且使用垃圾回收算法并不能保证一次性寻找到全部的软可及对象。当垃圾回收器每次运行的时候均可以随意释放不是强可及对象占用的内存,若是垃圾回收器找到了软可及对象事后,可能会进行如下操做:

·            SoftReference对象的referent域设置成为null,从而使该对象再也不引用heap对象。

·            SoftReference引用过的内存堆上的对象一概被生命为finalizable

·            当内存堆上的对象finalize()方法被运行并且该对象占用的内存被释放,SoftReference对象就会被添加到它的ReferenceQueue,前提条件是ReferenceQueue自己是存在的。

  既然Java里面存在这样的对象,那么咱们在编写代码的时候如何建立这样的对象呢?建立步骤以下:

  先建立一个对象,并使用普通引用方式【强引用】,而后再建立一个SoftReference来引用该对象,最后将普通引用设置为null,经过这样的方式,这个对象就仅仅保留了一个SoftReference引用,同时这种状况咱们所建立的对象就是SoftReference对象。通常状况下,咱们能够使用该引用来完成Cache功能,就是前边说的用于高速缓存,保证最大限度使用内存而不会引发内存泄漏的状况。下边的代码段:

public static void main(String args[])

{

//建立一个强可及对象

A a = new A();

//建立这个对象的软引用SoftReference

SoftReference sr = new SoftReference(a);

//将强引用设置为空,以遍垃圾回收器回收强引用

a = null;

//下次使用该对象的操做

if( sr != null ){

a = (A)sr.get();

}else{

//这种状况就是因为内存太低,已经将软引用释放了,所以须要从新装载一次

a = new A();

sr = new SoftReference(a);

}

}

  软引用技术使得Java系统能够更好地管理内存,保持系统稳定,防止内存泄漏,避免系统崩溃,所以在处理一些内存占用大并且生命周期长使用不频繁的对象能够使用该技术。

3)弱可及对象(weakly reachable):

  不是强可及对象一样也不是软可及对象,仅仅经过弱引用WeakReferencejava.lang.ref.WeakReference)访问的对象,这种对象的用途在于规范化映射(canonicalizedmapping,对于生存周期相对比较长并且从新建立的时候开销少的对象,弱引用也比较有用,和软引用对象不一样的是,垃圾回收器若是碰到了弱可及对象,将释放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)虚可及对象(phantomly reachable):

  不是强可及对象,也不是软可及对象,一样不是弱可及对象,之因此把虚可及对象放到最后来说,主要也是由于它的特殊性,有时候咱们又称之为幽灵对象,已经结束的,能够经过虚引用来访问该对象。咱们使用类PhantomReferencejava.lang.ref.PhantomReference)来访问,这个类只能用于跟踪被引用对象进行的收集,一样的,能够用于执行per-mortern清除操做。PhantomReference必须与ReferenceQueue类一块儿使用。须要使用ReferenceQueue是由于它可以充当通知机制,当垃圾收集器肯定了某个对象是虚可及对象的时候,PhantomReference对象就被放在了它的ReferenceQueue上,这就是一个通知,代表PhantomReference引用的对象已经结束,能够收集了,通常状况下咱们恰好在对象内存在回收以前采起该行为。这种引用不一样于弱引用和软引用,这种方式经过get()获取到的对象老是返回null,仅仅当这些对象在ReferenceQueue队列里面的时候,咱们能够知道它所引用的哪些对对象是死引用(Dead Reference)。而这种引用和弱引用的区别在于:

  弱引用(WeakReference是在对象不可达的时候尽快进入ReferenceQueue队列的,在finalization方法执行和垃圾回收以前是确实会发生的,理论上这类对象是不正确的对象,可是WeakReference对象能够继续保持Dead状态,

  虚引用(PhantomReference是在对象确实已经从物理内存中移除事后才进入的ReferenceQueue队列,并且get()方法会一直返回null

  当垃圾回收器遇到了虚引用的时候将有可能执行如下操做:

·            PhantomReference引用过的heap对象声明为finalizable

·            虚引用在堆对象释放以前就添加到了它的ReferenceQueue里面,这种状况使得咱们能够在堆对象被回收以前采起操做*再次提醒,PhantomReference对象必须通过关联的ReferenceQueue来建立,就是说必须ReferenceQueue类配合操做

  看似没有用处的虚引用,有什么用途呢?

·            首先,咱们能够经过虚引用知道对象究竟何时真正从内存里面移除的,并且这也是惟一的途径。

·            虚引用避过了finalize()方法,由于对于此方法的执行而言,虚引用真正引用到的对象是异常对象,若在该方法内要使用对象只能重建。通常状况垃圾回收器会轮询两次,一次标记为finalization,第二次进行真实的回收,而每每标记工做不能实时进行,或者垃圾回收其会等待一个对象去标记finalization。这种状况颇有可能引发MemoryOut,而使用虚引用这种状况就会彻底避免。由于虚引用在引用对象的过程不会去使得这个对象由Dead复活,并且这种对象是能够在回收周期进行回收的。

  在JVM内部,虚引用比起使用finalize()方法更加安全一点并且更加有效。而finaliaze()方法回收在虚拟机里面实现起来相对简单,并且也能够处理大部分工做,因此咱们仍然使用这种方式来进行对象回收的扫尾操做,可是有了虚引用事后咱们能够选择是否手动操做该对象使得程序更加高效完美。

iv.防止内存泄漏[来自IBM开发中心]

1)使用软引用阻止泄漏:

[1]Java语言中有一种形式的内存泄漏称为对象游离(Object Loitering):

——[$]对象游离——

// 注意,这段代码属于概念说明代码,实际应用中不要模仿

public class LeakyChecksum{

    private byte[] byteArray;

    publicsynchronized int getFileCheckSum(String filename)

   {

       int len =getFileSize(filename);

       if( byteArray == null || byteArray.length< len )

           byteArray = new byte[len];

       readFileContents(filename,byteArray);

       // 计算该文件的值而后返回该对象

   }

}

  上边的代码是类LeakyChecksum用来讲明对象游离的概念,里面有一个getFileChecksum()方法用来计算文件内容校验和getFileCheckSum方法将文件内容读取到缓冲区中计算校验和,更加直观的实现就是简单地将缓冲区做为getFileChecksum中的本地变量分配,可是上边这个版本比这种版本更加聪明,不是将缓冲区缓冲在实例中字段中减小内存churn。该优化一般不带来预期的好处,对象分配比不少人指望的更加便宜。(还要注意,将缓冲区从本地变量提高到实例变量,使得类若不带有附加的同步,就再也不是线程安全的了。直观的实现不须要将 getFileChecksum() 声明为 synchronized,而且会在同时调用时提供更好的可伸缩性。)

  这个类存在不少的问题,可是咱们着重来看内存泄漏。缓存缓冲区的决定极可能是根据这样的假设得出的,即该类将在一个程序中被调用许屡次,所以它应该更加有效,以重用缓冲区而不是从新分配它。可是结果是,缓冲区永远不会被释放,由于它对程序来讲老是可及的(除非LeakyChecksum对象被垃圾收集了)。更坏的是,它能够增加,却不能够缩小,因此 LeakyChecksum 将永久保持一个与所处理的最大文件同样大小的缓冲区。退一万步说,这也会给垃圾收集器带来压力,而且要求更频繁的收集;为计算将来的校验和而保持一个大型缓冲区并非可用内存的最有效利用。LeakyChecksum 中问题的缘由是,缓冲区对于 getFileChecksum() 操做来讲逻辑上是本地的,可是它的生命周期已经被人为延长了,由于将它提高到了实例字段。所以,该类必须本身管理缓冲区的生命周期,而不是让 JVM 来管理。

这里能够提供一种策略就是使用Java里面的软引用:

  弱引用如何能够给应用程序提供当对象被程序使用时另外一种到达该对象的方法,可是不会延长对象的生命周期。Reference 的另外一个子类——软引用——可知足一个不一样却相关的目的。其中弱引用容许应用程序建立不妨碍垃圾收集的引用,软引用容许应用程序经过将一些对象指定为expendable 而利用垃圾收集器的帮助。尽管垃圾收集器在找出哪些内存在由应用程序使用哪些没在使用方面作得很好,可是肯定可用内存的最适当使用仍是取决于应用程序。若是应用程序作出了很差的决定,使得对象被保持,那么性能会受到影响,由于垃圾收集器必须更加辛勤地工做,以防止应用程序消耗掉全部内存。高速缓存是一种常见的性能优化,容许应用程序重用之前的计算结果,而不是从新进行计算。高速缓存是 CPU 利用和内存使用之间的一种折衷,这种折衷理想的平衡状态取决于有多少内存可用。若高速缓存太少,则所要求的性能优点没法达到;若太多,则性能会受到影响,由于太多的内存被用于高速缓存上,致使其余用途没有足够的可用内存。由于垃圾收集器比应用程序更适合决定内存需求,因此应该利用垃圾收集器在作这些决定方面的帮助,这就是件引用所要作的。若是一个对象唯一剩下的引用是弱引用或软引用,那么该对象是软可及的(softly reachable。垃圾收集器并不像其收集弱可及的对象同样尽可能地收集软可及的对象,相反,它只在真正 “须要” 内存时才收集软可及的对象。软引用对于垃圾收集器来讲是这样一种方式,即 只要内存不太紧张,我就会保留该对象。可是若是内存变得真正紧张了,我就会去收集并处理这个对象。 垃圾收集器在能够抛出OutOfMemoryError 以前须要清除全部的软引用。经过使用一个软引用来管理高速缓存的缓冲区,能够解决 LeakyChecksum中的问题,如上边代码所示。如今,只要不是特别须要内存,缓冲区就会被保留,可是在须要时,也可被垃圾收集器回收:

——[$]使用软引用修复上边代码段——

public class CachingChecksum

{

    private SoftReference<byte[]> bufferRef;

    public synchronizedint getFileChecksum(String filename)

   {

       int len =getFileSize(filename);

       byte[] byteArray =bufferRef.get();

       if( byteArray == null || byteArray.length< len )

       {

           byteArray = new byte[len];

           bufferRef.set(byteArray);

       }

       readFileContents(filename,byteArray);

   }

}

一种廉价缓存:

CachingChecksum使用一个软引用来缓存单个对象,并让 JVM 处理从缓存中取走对象时的细节。相似地,软引用也常常用于 GUI 应用程序中,用于缓存位图图形。是否可以使用软引用的关键在于,应用程序是否可从大量缓存的数据恢复。若是须要缓存不止一个对象,您能够使用一个 Map,可是能够选择如何使用软引用。您能够将缓存做为 Map<K,SoftReference<V>> SoftReference<Map<K,V>> 管理。后一种选项一般更好一些,由于它给垃圾收集器带来的工做更少,而且容许在特别须要内存时以较少的工做回收整个缓存。弱引用有时会错误地用于取代软引用,用于构建缓存,可是这会致使差的缓存性能。在实践中,弱引用将在对象变得弱可及以后被很快地清除掉——一般是在缓存的对象再次用到以前——由于小的垃圾收集运行得很频繁。对于在性能上很是依赖高速缓存的应用程序来讲,软引用是一个无论用的手段,它确实不能取代可以提供灵活终止期复制事务型高速缓存的复杂的高速缓存框架可是做为一种廉价(cheap and dirty的高速缓存机制,它对于下降价格是颇有吸引力的。正如弱引用同样,软引用也可建立为具备一个相关的引用队列,引用在被垃圾收集器清除时进入队列。引用队列对于软引用来讲,没有对弱引用那么有用,可是它们能够用于发出管理警报,说明应用程序开始缺乏内存

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 中存储这些信息:

public class SocketManager{

    private Map<Socket,User>m = new HashMap<Socket,User>();

    public void setUser(Sockets,User u)

   {

       m.put(s,u);

   }

    public User getUser(Sockets){

       return m.get(s);

   }

    public void removeUser(Sockets){

       m.remove(s);

   }

}

 

SocketManagersocketManager;

//...

socketManager.setUser(socket,user);

  这种方法的问题是元数据的生命周期须要与套接字的生命周期挂钩,可是除非准确地知道何时程序再也不须要这个套接字,并记住从 Map 中删除相应的映射,不然,Socket User 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。这会阻止 Socket User 对象被垃圾收集,即便应用程序不会再使用它们。这些对象留下来不受控制,很容易形成程序在长时间运行后内存爆满。除了最简单的状况,在几乎全部状况下找出何时 Socket 再也不被程序使用是一件很烦人和容易出错的任务,须要人工对内存进行管理。

[2]弱引用内存泄漏代码:

  程序有内存泄漏的第一个迹象一般是它抛出一个 OutOfMemoryError,或者由于频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集能够提供可以用来诊断内存泄漏的大量信息。若是以 -verbose:gc 或者 -Xloggc 选项调用 JVM,那么每次 GC 运行时在控制台上或者日志文件中会打印出一个诊断信息,包括它所花费的时间、当前堆使用状况以及恢复了多少内存。记录 GC 使用状况并不具备干扰性,所以若是须要分析内存问题或者调优垃圾收集器,在生产环境中默认启用 GC 日志是值得的。有工具能够利用 GC 日志输出并以图形方式将它显示出来,JTune 就是这样的一种工具。观察 GC 以后堆大小的图,能够看到程序内存使用的趋势。对于大多数程序来讲,能够将内存使用分为两部分:baseline 使用和 current load 使用。对于服务器应用程序,baseline 使用就是应用程序在没有任何负荷、可是已经准备好接受请求时的内存使用,current load 使用是在处理请求过程当中使用的、可是在请求处理完成后会释放的内存。只要负荷大致上是恒定的,应用程序一般会很快达到一个稳定的内存使用水平。若是在应用程序已经完成了其初始化而且负荷没有增长的状况下,内存使用持续增长,那么程序就可能在处理前面的请求时保留了生成的对象。

public class MapLeaker{

    public ExecuteService exec= Executors.newFixedThreadPool(5);

    public Map<Task,TaskStatus>taskStatus

       = Collections.synchronizedMap(new HashMap<Task,TaskStatus>());

    private Random random= new Random();

    private enum TaskStatus{ NOT_STARTEDSTARTEDFINISHED };

    privateclass Task implements Runnable{

       private int[] numbers = new int[random.nextInt(200)];

       public void run()

       {

           int[] temp = new int[random.nextInt(10000)];

           taskStatus.put(this,TaskStatus.STARTED);

           doSomework();

           taskStatus.put(this,TaskStatus.FINISHED);

       }

   }

    public Task newTask()

   {

       Task t = new Task();

       taskStatus.put(t,TaskStatus.NOT_STARTED);

       exec.execute(t);

       return t;

   }

}

[3]使用弱引用堵住内存泄漏:

SocketManager 的问题是 Socket-User 映射的生命周期应当与 Socket 的生命周期相匹配,可是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。幸运的是,从 JDK 1.2 开始,垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就能够帮助咱们防止这种内存泄漏——利用弱引用。弱引用是对一个对象(称为 referent的引用的持有者。使用弱引用后,能够维持对 referent 的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,若是对一个对象的引用只有弱引用,那么这个 referent 就会成为垃圾收集的候选对象,就像没有任何剩余的引用同样,并且全部剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weakly reachable))WeakReference referent 是在构造时设置的,在没有被清除以前,能够用 get() 获取它的值。若是弱引用被清除了(无论是 referent 已经被垃圾收集了,仍是有人调用了 WeakReference.clear()get() 会返回 null。相应地,在使用其结果以前,应当老是检查get() 是否返回一个非 null 值,由于 referent 最终老是会被垃圾收集的。用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期同样长。若是不当心,那么它可能就与程序的生命周期同样——若是将一个对象放入一个全局集合中的话。另外一方面,在建立对一个对象的弱引用时,彻底没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另外一种到达它的方法。弱引用对于构造弱集合最有用,如那些在应用程序的其他部分使用对象期间存储关于这些对象的元数据的集合——这就是 SocketManager 类所要作的工做。由于这是弱引用最多见的用法,WeakHashMap 也被添加到 JDK 1.2 的类库中,它对键(而不是对值)使用弱引用。若是在一个普通 HashMap 中用一个对象做为键,那么这个对象在映射从 Map 中删除以前不能被回收,WeakHashMap 使您能够用一个对象做为 Map 键,同时不会阻止这个对象被垃圾收集。下边的代码给出了 WeakHashMap  get() 方法的一种可能实现,它展现了弱引用的使用:

public class WeakHashMap<K,Vimplements Map<K,V>

{

    private staticclass Entry<K,Vextends WeakReference<Kimplements Map.Entry<K,V>

   {

       private V value;

       private final int hash;

       private Entry<K,V> next;

       // ...

   }

 

    public V get(Object key)

   {

       int hash =getHash(key);

       Entry<K,V> e =getChain(hash);

       while(e != null)

       {

           k eKey = e.get();

           if( e.hash == hash&& (key == eKey || key.equals(eKey)))

               return e.value;

           e = e.next;

       }

       return null;

   }

}

  调用 WeakReference.get() 时,它返回一个对 referent 强引用(若是它仍然存活的话),所以不须要担忧映射在 while 循环体中消失,由于强引用会防止它被垃圾收集。WeakHashMap 的实现展现了弱引用的一种常见用法——一些内部对象扩展 WeakReference。其缘由在下面一节讨论引用队列时会获得解释。在向 WeakHashMap 中添加映射时,请记住映射可能会在之后脱离,由于键被垃圾收集了。在这种状况下,get() 返回 null,这使得测试 get() 的返回值是否为 null 变得比平时更重要了。

[4]使用WeakHashMap堵住泄漏

  在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就好了,以下边代码所示。(若是 SocketManager 须要线程安全,那么能够用 Collections.synchronizedMap() 包装 WeakHashMap。当映射的生命周期必须与键的生命周期联系在一块儿时,能够使用这种方法。不过,应当当心不滥用这种技术,大多数时候仍是应当使用普通的 HashMap 做为 Map 的实现。

public class SocketManager{

    private Map<Socket,User>m = new WeakHashMap<Socket,User>();

    public void setUser(Socket s,User s)

   {

       m.put(s,u);

   }

    public User getUser(Sockets)

   {

       return m.get(s);

   }

}

  引用队列:

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

4)关于Java中引用思考:

  先观察一个列表:

级别

回收时间

用途

生存时间

强引用

历来不会被回收

对象的通常状态

JVM中止运行时终止

软引用

在内存不足时

在客户端移除对象引用事后,除非再次激活,不然就放在内存敏感的缓存中

内存不足时终止

弱引用

在垃圾回收时,也就是客户端已经移除了强引用,可是这种状况下内存仍是客户端引用可达的

阻止自动删除不须要用的对象

GC运行后终止

虚引用[幽灵引用]

对象死亡以前,就是进行finalize()方法调用附近

特殊的清除过程

不定,当finalize()函数运行事后再回收,有可能以前就已经被回收了。

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

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

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

如下是不肯定概念

  【*Java引用的深刻部分一直都是讨论得比较多的话题,上边大部分为摘录整理,这里再谈谈我我的的一些见解。从整个JVM框架结构来看,Java的引用垃圾回收器造成了针对Java内存堆的一个对象的闭包管理集,其中在基本代码里面经常使用的就是强引用,强引用主要使用目的是就是编程的正常逻辑,这是全部的开发人员最容易理解的,而弱引用和软引用的做用是比较回味无穷的。按照引用强弱,其排序能够为:强引用——软引用——弱引用——虚引用,为何这样写呢,实际上针对垃圾回收器而言,强引用是它绝对不会随便去动的区域,由于在内存堆里面的对象,只有当前对象不是强引用的时候,该对象才会进入垃圾回收器目标区域

软引用又能够理解为内存应急引用,也就是说它和GC是完整地配合操做的,为了防止内存泄漏,当GC在回收过程出现内存不足的时候,软引用会被优先回收,从垃圾回收算法上讲,软引用在设计的时候是很容易被垃圾回收器发现的。为何软引用是处理告诉缓存的优先选择的,主要有两个缘由:第一,它对内存很是敏感,从抽象意义上讲,咱们甚至能够任何它和内存的变化牢牢绑定到一块儿操做的,由于内存一旦不足的时候,它会优先向垃圾回收器报警以提示内存不足;第二,它会尽可能保证系统在OutOfMemoryError以前将对象直接设置成为不可达,以保证不会出现内存溢出的状况;因此使用软引用来处理Java引用里面的高速缓存是很不错的选择。其实软引用不只仅和内存敏感,实际上和垃圾回收器的交互也是敏感的,这点能够这样理解,由于当内存不足的时候,软引用会报警,而这种报警会提示垃圾回收器针对目前的一些内存进行清除操做,而在有软引用存在的内存堆里面,垃圾回收器会第一时间反应,不然就会MemoryOut了。按照咱们正常的思惟来考虑,垃圾回收器针对咱们调用System.gc()的时候,是不会轻易理睬的,由于仅仅是收到了来自强引用层代码的请求,至于它是否回收还得看JVM内部环境的条件是否知足,可是若是是软引用的方式去申请垃圾回收器会优先反应,只是咱们在开发过程不能控制软引用对垃圾回收器发送垃圾回收申请,而JVM规范里面也指出了软引用不会轻易发送申请到垃圾回收器。这里还须要解释的一点的是软引用发送申请不是说软引用像咱们调用System.gc()这样直接申请垃圾回收,而是说软引用会设置对象引用为null,而垃圾回收器针对该引用的这种作法也会优先响应,咱们能够理解为是软引用对象在向垃圾回收器发送申请。反应快并不表明垃圾回收器会实时反应,仍是会在寻找软引用引用到的对象的时候遵循必定的回收规则,反应快在这里的解释是相对强引用设置对象为null,当软引用设置对象为null的时候,该对象的被收集的优先级比较

弱引用是一种比软引用相对复杂的引用,其实弱引用和软引用都是Java程序能够控制的,也就是说能够经过代码直接使得引用针对弱可及对象以及软可及对象是可引用的,软引用和弱引用引用的对象实际上经过必定的代码操做是可从新激活的,只是通常不会作这样的操做,这样的用法违背了最初的设计。弱引用和软引用在垃圾回收器的目标范围有一点点不一样的就是,使用垃圾回收算法是很难找到弱引用的,也就是说弱引用用来监控垃圾回收的整个流程也是一种很好的选择,它不会影响垃圾回收的正常流程,这样就能够规范化整个对象从设置为null了事后的一个生命周期的代码监控。并且由于弱引用是否存在对垃圾回收整个流程都不会形成影响,能够这样认为,垃圾回收器找获得弱引用,该引用的对象就会被回收,若是找不到弱引用,一旦等到GC完成了垃圾回收事后,弱引用引用的对象占用的内存也会自动释放,这就是软引用在垃圾回收事后的自动终止。

  最后谈谈虚引用,虚引用应该是JVM里面最厉害的一种引用,它的厉害在于它能够在对象的内存物理内存中清除掉了事后再引用该对象,也就是说当虚引用引用到对象的时候,这个对象实际已经从物理内存堆清除掉了,若是咱们不用手动对对象死亡或者濒临死亡进行处理的话,JVM会默认调用finalize函数,可是虚引用存在于该函数附近的生命周期内,因此能够手动对对象的这个范围的周期进行监控。它之因此称为幽灵引用就是由于该对象的物理内存已经不存在的,我我的以为JVM保存了一个对象状态的镜像索引,而这个镜像索引里面包含了对象在这个生命周期须要的全部内容,这里的所须要就是这个生命周期内须要的对象数据内容,也就是对象死亡和濒临死亡以前finalize函数附近,至于强引用所须要的其余对象附加内容是不须要在这个镜像里面包含的,因此即便物理内存不存在,仍是能够经过虚引用监控到该对象的,只是这种状况是否可让对象从新激活为强引用我就不敢说了。由于虚引用在引用对象的过程不会去使得这个对象由Dead复活,并且这种对象是能够在回收周期进行回收的。

 

5.总结:

本章节主要涵盖了Java里面比较底层的一个章节,主要是以JVM内存模型为基础包括JVM针对内存的线程模型的探讨以及针对Java里面内存堆和栈的详细分析。特别感谢白远方同窗提供的汇编方面关于操做系统以及内存发展的资料提供。

  参考:IBM开发中心文档,《Inside JVM

  本文的讲解可能比较概念化,但愿全部读者可以仔细品味,Java与对象相关的底层内从这里面都说起到了,主要是方便初学者和深刻者可以更加理解Java虚拟机处理Java对象的一个流程以及底层的相关原理,也方便查询和参考,可能会有不完善的地方,若是有什么概念错误,请来Email告知,谢谢:oksend@163.com。并且由于这一个章节的内容概念不少,整理思考和撰写花了太长时间,抱歉!

相关文章
相关标签/搜索