Java内存模型详解JMM

Java内存模型详解JMM
Java内存模型详解JMM

(原本准备把内存模型单独放到某一篇文章的某个章节里面讲解,后来查阅了

国外很多文档才发现其实JVM内存模型的内容还蛮多的,所以直接作为一个章

节的基础知识来讲解,可能该章节概念的东西比较多。一个开发Java的开发者,一旦了解了JVM内存模型就能够更加深入地了解该语言的语言特性,可能这个

章节更多的是概念,没有太多代码实例,所以希望读者谅解,有什么笔误来Email 告知:silentbalanceyh@https://www.360docs.net/doc/fb6096837.html,,本文尽量涵盖所有Java语言可以碰到的和内

存相关的内容,同样也会提到一些和内存相关的计算机语言的一些知识,为草案。因为平时开发的时候没有特殊情况不会进行内存管理,所以有可能有笔误的地

方比较多,我用的是Windows平台,所以本文涉及到的与操作系统相关的只是

仅仅局限于Windows平台。不仅仅如此,这一个章节牵涉到的多线程和另外一些内容并没有讲到,这里主要是结合JVM内部特性把本章节作为核心的概念性

章节来讲解,这样方便初学者深入以及彻底理解Java语言)

本文章节:

1.JMM简介

2.堆和栈

3.本机内存

4.防止内存泄漏

1.JMM简介

i.内存模型概述

Java平台自动集成了线程以及多处理器技术,这种集成程度比Java以前诞

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

1)JSR133:

在Java语言规范里面指出了JMM是一个比较开拓性的尝试,这种尝试视图定义一个一致的、跨平台的内存模型,但是它有一些比较细微而且很重要的缺点。其实Java语言里面比较容易混淆的关键字主要是synchronized和volatile,也因

为这样在开发过程中往往开发者会忽略掉这些规则,这也使得编写同步代码比较困难。

JSR133本身的目的是为了修复原本JMM的一些缺陷而提出的,其本身的制定目标有以下几个:

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

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

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

?提供大范围的流行硬件体系结构上的高性能JVM实现,现在的处理器在它们的内存模型上有着很大的不同,JMM应该能够适合于实际的尽可能多的体系结构而不以性能为代价,这也是Java跨平台型设计的基础?提供一个同步的习惯用法,以允许发布一个对象使他不用同步就可见,这种情况又称为初始化安全(initialization safety)的新的安全保证?对现有代码应该只有最小限度的影响

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

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

同步:同步就是在发出一个功能调用的时候,在没有得到响应之前,该调用就不返回,按照这样的定义,其实大部分程序的执行都是同步调用的,一般情况下,在描述同步和异步操作的时候,主要是指代需要其他部件协作处理或者需要协作响应的一些任务处理。比如有一个线程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)可见性、可排序性

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

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

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

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

ii.JMM结构:

1)简单分析:

Java语言规范中提到过,JVM中存在一个主存区(Main Memory或Java Heap Memory),Java中所有变量都是存在主存中的,对于所有线程进行共享,而每个线程又存在自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。而在多核处理器下,大部分数据存储在高速缓存中,如果高速缓存不经过内存的时候,也是不可见的一种表现。在Java程序中,内存本身是比较昂贵的资源,其实不仅仅针对Java应用程序,对操作系统本身而言内存也属于昂贵资源,Java程序在性能开销过程中有几个比较典型的可控制的来源。synchronized和volatile关键字提供的内存中模型的可见性保证程序使用一个特殊的、存储关卡(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线程必须依靠自身来维持对象的可见性以及对象自身应该提供相对应的操作而实现整个内存操作的三个特性,而不是仅仅依靠特定的修改对象状态的线程来完成如此复杂的一个流程。

【*:综上所属,JMM在JVM内部实现的结构就变得相对复杂,当然一般的Java初学者可以不用了解得这么深入。】

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

原子性(Atomicity):

访问存储单元内的任何类型的字段的值以及对其更新操作的时候,除开long 类型和double类型,其他类型的字段是必须要保证其原子性的,这些字段也包括为对象服务的引用。此外,该原子性规则扩展可以延伸到基于long和double 的另外两种类型:volatile long和volatile double(volatile为java关键字),没有被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线程需要做三件事情分别是A1、A2、A3,而B是另外一个线程具有操作B1、B2,如果把参考定位到B线程,那么对A线程而言,B的操作B1、B2有可能随时会访问到A的可见区域,比如A有一个可见区域a,

A1就是把a修改称为1,但是B线程在A线程调用了A1过后,却访问了a并且使用B1或者B2操作使得a发生了改变,变成了2,那么当A按照排序性进行A2操作读取到a的值的时候,读取到的是2而不是1,这样就使得程序最初设计的时候A线程的初衷发生了改变,就是排序被打乱了,那么B线程对A线程而言,其身份就是“间谍”,而且需要注意到一点,B线程的这些操作不会和A之间存在等待关系,那么B线程的这些操作就是异步操作,所以针对执行线程A而言,B的身份就是“非同步方法中的…间谍?。】

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

注意:第一点在JLS(Java 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中实现的,而不是只有字

符数组,因此字符数组可以在多个String和StringBuffer对象之间共享,而不需要在每次创建一个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-thread as-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的字段中)与易失性的写一起重新排序,因此另外一个线程可能会看到initialized为true,但是对于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 method stacks),这种情况下使得这些本地方法和其他内存运行时数据区的内容尽可能保证和其他内存运行时数据区独立,而且该方法的调用更靠近操作系统,这些方法执行的字节码有可能根据操作系统环境的不同使得其编译出来的本地字节码的结构也有一定的差异。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-Endian和Little-Endian】——

程序存储数据过程中,如果数据是跨越多个字节对象就必须有一种约定:?它的地址是多少:对于跨越多个字节的对象,一般它所占的字节都是连续的,它的地址等于它所占字节最低地址,这种情况链表可能存储的仅仅是表头

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

比如:int x,它的地址为0x100,那么它占据了内存中的0x100、0x101、0x102、0x103四个字节,所以一般情况我们觉得int是4个字节。上边只是内存组织的一种情况,多字节对象在内存中的组织有两种约定,还有一种情况:若一个整数为W位,它的表示如下:

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

它的最高有效字节MSB(Most Significant Byte)为:[Xw-1,Xw-2,...,Xw-8]最低有效字节LSB(Least Significant Byte)为:[X7,X6,...,X0]

其余字节则位于LSB和MSB之间

LSB和MSB谁位于内存的最低地址,即代表了该对象的地址,这样就引出了Big-Endian和Little-Endian的问题,如果LSB在MSB前,LSB是最低地址,则该机器是小端,反之则是大端。DES(Digital Equipment Corporation,现在是Compaq公司的一部分)和Intel机器(x86平台)一般采用小端,IBM、Motorola(Power PC)、Sun的机器一般采用大端。当然这种不能代表所有情况,有的CPU既能工作于小端、又可以工作于大端,比如ARM、Alpha、摩托罗拉的PowerPC,这些情况根据具体的处理器型号有所不同。但是大部分操作系统(Windows、FreeBSD、Linux)一般都是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-Endian和Little-Endian的简单讲解——

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

同样的,因为Java本身的自动内存管理,方法区也会被垃圾回收的,Java 程序可以通过类扩展动态加载器对象,类可以成为“未引用”向垃圾回收器进行申请,如果一个类是“未引用”的,则该类就可能被卸载,

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

【类型信息】:

?类型的完全限定名(https://www.360docs.net/doc/fb6096837.html,ng.String格式)

?类型的完全限定名的直接父类的完全限定名(除非这个父类的类型是一个接口或者https://www.360docs.net/doc/fb6096837.html,ng.Object)

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

?类型的修饰符(例如public、abstract、final)

?任何一个直接超类接口的完全限定名的列表

在JVM和类文件名的内部,类型名一般都是完全限定名(https://www.360docs.net/doc/fb6096837.html,ng.String)格式,在Java源文件里面,完全限定名必须加入包前缀,而不是我们在开发过程写的简单类名,而在方法上,只要是符合Java语言规范的类的完全限定名都可以,而JVM可能直接进行解析,比如:(https://www.360docs.net/doc/fb6096837.html,ng.String)在JVM内部名称为java/lang/String,这就是我们在异常捕捉的时候经常看到的ClassNotFoundException的异常里面类信息的名称格式。

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

?类型常量池

?字段信息

?方法信息

?所有定义在Class内部的(静态)变量信息,除开常量

?一个ClassLoader的引用

?Class的引用

【常量池】

针对类型加载的类型信息,JVM将这些存储在常量池里,常量池是一个根据类型定义的常量的有序常量集,包括字面量(String、Integer、Float常量)以及符号引用(类型、字段、方法),整个长量池会被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在加载了任何一个类型过后会创建一个https://www.360docs.net/doc/fb6096837.html,ng.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、引用以及返回值都占据了一个数组中的局部变量的条目,而byte、short、char则在存储到局部变量的时候是先转化成为int再进行操作的,则long和double则是在这样一个数组里面使用了两个元素的空间大小,在局部变量里面存储基本数据类型的时候使用的就是这样的结构。举个例子:

class Example3a{

public static int 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虚拟机是

java技术面试必问:JVM 内存模型讲解

java技术面试必问:JVM 内存模型讲解 今天我们就来聊一聊Java内存模型,面试中面试官会通过考察你对jvm的理解更深入得了解你的水平。在了解jvm内存模型前我们先回顾下,java程序的执行过程: java文件在通过java编译器生产.class 字节码文件,然后由jvm中的类加载器加载各个类中的字节码文件,加载完成后由jvm执行引擎执行,在整个加载过程中,jvm用一段空间来存储程序执行期间需要的数据和相关信息,这个空间就叫做jvm内存。 一、JVM 的重要性 首先你应该知道,运行一个 Java 应用程序,我们必须要先安装 JDK 或者 JRE 。这是因为 Java 应用在编译后会变成字节码,然后通过字节码运行在 JVM 中,而 JVM 是JRE 的核心组成部分。 二、优点 JVM 不仅承担了 Java 字节码的分析(JIT compiler)和执行(Runtime),同时也内置了自动内存分配管理机制。这个机制可以大大降低手动分配回收机制可能带来的内存泄露和内存溢出风险,使 Java 开发人员不需要关注每个对象的内存分配以及回收,从而更专注于业务本身。 三、缺点 这个机制在提升 Java 开发效率的同时,也容易使 Java 开发人员过度依赖于自动化,弱化对内存的管理能力,这样系统就很容易发生 JVM 的堆内存异常、垃圾回收(GC)的不合适以及 GC 次数过于频繁等问题,这些都将直接影响到应用服务的性能。 四、内存模型 JVM 内存模型共分为5个区:堆(Heap)、方法区(Method Area)、程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)。 其中,堆(Heap)、方法区(Method Area)为线程共享,程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)为线程隔离。 五、堆(Heap) 堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。 堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 区和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。

java数据在内存中存储详解

博客分类: JAVA 1. 有这样一种说法,如今争锋于IT战场的两大势力,MS一族偏重于底层实现,Java 一族偏重于系统架构。说法根据无从考证,但从两大势力各自的社区力量和图书市场已有佳作不难看出,此说法不虚,但掌握Java的底层实现对Java程序员来说是至关重要的,本文介绍了Java中的数据在内存中的存储。 2内存中的堆(stack)与栈(heap) Java程序运行时有6个地方可以存储数据,它们分别是寄存器、栈、堆、静态存储、常量存储和非RAM存储,主要是堆与栈的存储。 【随机存储器:Random Access Memory】 栈与堆都是Java用来在RAM中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。另外,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。 【寄存器位于CPU中】 3Java中数据在内存中的存储 3.1基本数据类型的存储 Java的基本数据类型共有8种,即int,short,long,byte,float,double, boolean,char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a=3;long b=255L;的形式来定义的,称为自动变量。值得注意的是:自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a=3;这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

java内存屏障与JVM并发详解

深入Java底层:内存屏障与JVM并发详解(1) 本文介绍了内存屏障对多线程程序的影响,同时将研究内存屏障与JVM并发机制的关系,如易变量(volatile)、同步(synchronized)和原子条件式(atomic conditional)。 AD:内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。本文假定读者已经充分掌握了相关概念和Java内存模型,不讨论并发互斥、并行机制和原子性。内存屏障用来实现并发编程中称为可见性(visibility)的同样重要的作用。 关于JVM更多内容,请参阅:JVM详解 Java虚拟机原理与优化 内存屏障为何重要? 对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。当数据是不可变的,同时/或者数据限制在线程范围内,这些优化是无害的。 如果把这些优化与对称多处理(symmetric multi-processing)和共享可变状态(shared mutable state)结合,那么就是一场噩梦。当基于共享可变状态的内存操作被重新排序时,程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据写入的顺序不一致。适当的放置内存屏障通过强制处理器顺序执行待定的内存操作来避免这个问题。 内存屏障的协调作用 内存屏障不直接由JVM暴露,相反它们被JVM插入到指令序列中以维持语言层并发原语的语义。我们研究几个简单Java程序的源代码和汇编指令。首先快速看一下Dekker算法中的内存屏障。该算法利用volatile变量协调两个线程之间的共享资源访问。 请不要关注该算法的出色细节。哪些部分是相关的?每个线程通过发信号试图进入代码第一行的关键区域。如果线程在第三行意识到冲突(两个线程都要访问),通过turn变量的操作来解决。在任何时刻只有一个线程可以访问关键区域。 1. // code run by first thread // code run by second thread 2. 3. 1 intentFirst = true; intentSecond = true;

Java内存区域划分、内存分配原理

本文由我司收集整编,推荐下载,如有疑问,请与我司联系 Java 内存区域划分、内存分配原理 2014/11/16 2448 运行时数据区域 Java 虚拟机在执行Java 的过程中会把管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程 的启动而存在,而有的区域则依赖线程的启动和结束而创建和销毁。 Java 虚拟机包括下面几个运行时数据区域: 程序计数器 程序计数器是一块较小的区域,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的模型里,字节码指示器就是通过改变程序计数器的值 来指定下一条需要执行的指令。分支,循环等基础功能就是依赖程序计数器来完成的。 由于java 虚拟机的多线程是通过轮流切换并分配处理器执行时间来完成,一个处理器同一时间只会执行一条线程中的指令。为了线程恢复后能够恢复正确的 执行位置,每条线程都需要一个独立的程序计数器,以确保线程之间互不影响。因 此程序计数器是“线程私有”的内存。 如果虚拟机正在执行的是一个Java 方法,则计数器指定的是字节码指令对应的地址,如果正在执行的是一个本地方法,则计数器指定问空undefined。程序计数器区域是Java 虚拟机中唯一没有定义OutOfMemory 异常的区域。 Java 虚拟机栈 和程序计数器一样也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应 一个栈帧在虚拟机栈中从入栈到出栈的过程。

精选大厂java多线程面试题50题

Java多线程50题 1)什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。 2)线程和进程有什么区别? 线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。更多详细信息请点击这里。 3)如何在Java中实现线程? https://www.360docs.net/doc/fb6096837.html,ng.Thread类的实例就是一个线程但是它需要调用https://www.360docs.net/doc/fb6096837.html,ng.Runnable接口来执行,由于线程类本身就是调用的 Runnable接口所以你可以继承https://www.360docs.net/doc/fb6096837.html,ng.Thread类或者直接调用Runnable接口来重写run()方法实现线程。 4)Thread类中的start()和run()方法有什么区别? 这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你

调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。 5)Java中Runnable和Callable有什么不同? Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的call()方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。 6)Java内存模型是什么? Java内存模型规定和指引Java程序在不同的内存架构、CPU 和操作系统间有确定性地行为。它在多线程的情况下尤其重要。 Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。 ●线程内的代码能够按先后顺序执行,这被称为程序次序 规则。 ●对于同一个锁,一个解锁操作一定要发生在时间上后发 生的另一个锁定操作之前,也叫做管程锁定规则。 ●前一个对Volatile的写操作在后一个volatile的读操作之 前,也叫volatile变量规则。 ●一个线程内的任何操作必需在这个线程的start()调用之 后,也叫作线程启动规则。 ●一个线程的所有操作都会在线程终止之前,线程终止规

字节跳动面试经验分享(非常详细)

字节跳动面试官一招差点KO我,一共面试了3轮(5年经验),艰难拿下2-2职级offer! 前言 我从大学毕业开始做Android开发,现在已经五年时间了,现在在山东老家济南做Android开发。这三年里面,也只是一心在做Android开发,其他语言接触的并不多,了解点JS之类的。现在感觉Android开始不像以前那样好做了,也可能是现在年纪慢慢大了,要考虑的事情变多了的缘故吧。 不知道以后应该何去何从,总是感觉做Android或者说做程序员一直处在一种不稳定之中,在一些中小公司里面,可能工作一年两年就因为各种各样的原因而离职。马上就要结婚了,该买房了。济南的房价一直在涨,而自己的收入还是这么不温不火的,加上这不稳定的工作,让人对于前途实在是乐观不起来。 再加上今年的大环境非常差,互联网企业裁员的现象比往年更严重了,可今年刚好是我的第一个“五年计划”截止的时间点,说什么也不能够耽搁了,所以早早准备的跳槽也在疫情好转之后开始进行了。但是,不得不说,这次字节的面试真的太难为我了,可以说是和面试官大战了3个回合,不过好在最后给了offer。 我个人情况是5年Android开发经验,字节跳动定级2-2(年薪是50-100w左右含加班费和股票折现,不含车餐房补)的样子,我是拿到了年薪60w+,13薪。下面是我的面试经历,与学习经验分享,希望能带来一些不一样的启发和帮助。 我与字节跳动面试官“大战”3回合,胜! 我的学习经验 1—4年大学 ?Java

无论什么级别的Android从业者,Java作为Android开发基础语言。不管是工作还是面试中,Java都是必考题。如果不懂Java的话,薪酬会非常吃亏(美团尤为重视Java基础) 详细介绍了Java泛型、注解、并发编程、数据传输与序列化、高效IO、容器集合、反射与类加载以及JVM重点知识线程、内存模型、JVM运行时内存、垃圾回收与算法、Java中四种引用类型、GC 分代收集算法 VS 分区收集算法、GC 垃圾收集器、JAVA IO/NIO 、JVM 类加载机制的各大知识点。 ?筑基必备 Android架构师筑基包括哪些内容呢: 1.深入 Java 泛型. 2.解深入浅出 3.并发编程 4.数据传输与序列化 5.Java 虚拟机原理 6.反射与类加载 7.高效 IO 8.Kotlin项目实战 大学1-4年架构师筑基必备 ?学习笔记整理 架构师筑基必备目录 架构师筑基必备第一章

java初级面试题

JAVA相关基础知识 1.Finally,final,finalize Finally:释放资源(内存之外的,打开的文件、连接、屏幕上的图形,,) ①总会执行 ②非后台线程结束,后台线程被强关,不会执行finally ③当try和catch中有return时,finally在return之后执行,但是返回值不会改变(finally中不会改变已保存的返回结果) ④finally中最好不要包含return,否则程序会从finally中退出,返回值不是try或catch中保存的返回值。 final: 基本数据类型:不可更改 类:不可继承 对象:引用不可变,对象内容可变 finalze:回收前调用,不适合用来清理或释放资源。对象免死最后机会!保证会被调用,但不保证会执行完(在低优先级线程中执行) 2. 数据在各个网络层之间是怎么传输的? 数据在各层之间的单位都是不一样的, 在物理层数据的单位称为比特(bit);在数据链路层,数据的单位称为(frame);在网络层,数据的单位称为数据包(packet);传输层,数据的单位称为数据段(segment)。 3. Hashtable、HashMap Hashtable 与HashMap类似,但是主要有6点不同。 1.HashTable的方法是同步的,HashMap未经同步,如Vector和ArrayList一样。 2.HashTable不允许null,key和value都不可以,HashMap允许null值,key和value 都可以。HashMap允许key值只能由一个null

3.HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。 4.遍历的时候,HashTable使用Enumeration,HashMap使用Iterator。 5.HashTable中hash数组默认大小是11,增加的方式是old*2+1。HashMap中hash 数组的默认大小是16,而且一定是2的指数。 6.哈希值的使用不同,HashTable直接使用对象的hashCode。 Hashtable继承自Dictionary类,实现了Map接口。而HashMap是继承自AbstractMap,实现了Map接口。 4. GET,POST区别? 答:基础知识:Http的请求格式如下。 主要包含三个信息:1、请求的类型(GET或POST),2、要访问的资源(如\res\img\a.jif),3、Http版本(http/1.1)

用来说明服务器要使用的附加信息 这是Http的规定,必须空一行 [] 请求的内容数据 区别: 1、Get是从服务器端获取数据,Post则是向服务器端发送数据。 2、在客户端,Get方式通过URL提交数据,在URL地址栏可以看到请求消息,该消息被编码过;Post数据则是放在Html header内提交。

JAVA经典面试题:Java内存模型

JAVA经典面试题:Java内存模型 因为Java内存模型不仅是java重点要学习的技术知识,还是面试的时候经典面试题,希望引起同学们的重视,今天千锋小编就来分享一下java内存模型的相关技术知识。 不同的渠道,内存模型是不一样的,但是jvm的内存模型标准是一致的。其实java的多线程并发问题都会反映在java的内存模型上,所谓线程安全无非是要操控多个线程对某个资源的有序拜访或修改。总结java的内存模型,要解决两个首要的问题:可见性和有序性。 可见性:多个线程之间是不能相互传递数据通信的,它们之间的交流只能经过同享变量来进行。Java内存模型(JMM)规定了jvm有主内存,主内存是多个线程同享的。当new一个目标的时分,也是被分配在主内存中,每个线程都有自己的作业内存,作业内存存储了主存的某些目标的副本,当然线程的作业内存大小是有限制的。当线程操作某个目标时,履行次序如下: (1) 从主存仿制变量到当前作业内存(read and load) (2) 履行代码,改动同享变量值(use and assign) (3) 用作业内存数据改写主存相关内容(store and write)

当一个同享变量在多个线程的作业内存中都有副本时,如果一个线程修改了这个同享变量,那么其他线程应该可以看到这个被修改后的值,这就是多线程的可见性问题。 有序性:线程在引证变量时不能直接从主内存中引证,如果线程作业内存中没有该变量,则会从主内存中复制一个副本到作业内存中,完成后线程会引证该副本。当同一线程再度引证该字段时,有可能从头从主存中获取变量副本(read-load-use),也有可能直接引证本来的副本(use),也就是说read,load,use次序可以由JVM完成体系决议。 线程不能直接为主存中字段赋值,它会将值指定给作业内存中的变量副本(assign),完成后这个变量副本会同步到主存储区(store- write),至于何时同步往昔,依据JVM完成体系决议。有该字段,则会从主内存中将该字段赋值到作业内存中,这个进程为read-load,完成后线程会引证该变量副本。 知识就财富,这句话再IT行业显示的尤其现实残酷,懂就是懂,不懂就是不懂,所以各位同学,你的努力与否与你财富直接挂钩。一起加油吧!更多java 技术经典面试题欢迎关注千锋小编。

杭研Java秋招面经——【网易 笔试面试精品资源】

一面 Java内存模型讲一下 GC算法,知道的都讲一下 HashMap,get,put实现 JsonWebToken具体实现流程(简历) Spring AOP如何实现,写一个 AOP功能的主要流程 数据库引擎用过哪些,它们的区别 设计大流量访问系统,要做节流控制(类似秒杀) Linux命令用过哪些 频繁 gc排查处理 内存过大排查的处理,用 jmap,jstack怎么做,不用又怎么做 MySQL主从复制场景问题(bin_log) 项目中技术上最大的成长,项目中的问题解决方案讲一下 二面 手撕算法:比如 123+234=357,对应两个链表 3->2->1,4->3->2(输

入),输出结果链表(7->5->3),写一个函数实现,输入为两个链表,输出为一个结果链表。(考虑极端情况和进位情况,花了很久写出来还是不完善) 输入网址到展现发生了什么,越详细越好(我考虑了 DNS轮询,负载均衡和 CDN以及状态码,然后就全讲) 负载均衡是怎么做的,CDN具体是怎么实现的 TCP三次握手四次挥手 cookie和 session的区别,多台服务器的情况呢 四次挥手时最后两者的状态,Client的 TIME_WAIT避免什么问题,没有它会怎么样 SSM和 Spring Boot的区别 MyBatis和 Hibernate区别

TCP流量控制和拥塞控制,具体在场景中是怎么起作用的 Java线程和 OS中的线程的关系,与内存对应关系,一个 JVM线程数的上限受哪些因素限制 HR 挨个讲讲项目 项目哪个对技术成长大,哪个对个人成长大 面了哪些公司,走到了什么流程 为什么选择网易 之后的学习规划 全程自己讲了很多 总监面 protocolbuf主要优势(性能、安全性、跨语言) Java本身序列化存在的问题(不知道) Java内存模型 网络 IO编程中的内存使用了 JMM哪部分

java内存模型

12.Java内存模型 (原本准备把内存模型单独放到某一篇文章的某个章节里面讲解,后来查阅了国外很多文档才发现其实JVM内存模型的内容还蛮多的,所以直接作为一个章节的基础知识来讲解,可能该章节概念的东西比较多。一个开发Java的开发者,一旦了解了JVM内存模型就能够更加深入地了解该语言的语言特性,可能这个章节更多的是概念,没有太多代码实例,所以希望读者谅解,有什么笔误来Email告知:silentbalanceyh@https://www.360docs.net/doc/fb6096837.html,,本文尽量涵盖所有Java语言可以碰到的和内存相关的内容,同样也会提到一些和内存相关的计算机语言的一些知识,为草案。因为平时开发的时候没有特殊情况不会进行内存管理,所以有可能有笔误的地方比较多,我用的是Windows平台,所以本文涉及到的与操作系统相关的只是仅仅局限于Windows平台。不仅仅如此,这一个章节牵涉到的多线程和另外一些内容并没有讲到,这里主要是结合JVM内部特性把本章节作为核心的概念性章节来讲解,这样方便初学者深入以及彻底理解Java 语言) 本文章节: 1.JMM简介 2.堆和栈 3.本机内存 4.防止内存泄漏 1.JMM简介 i.内存模型概述 Java平台自动集成了线程以及多处理器技术,这种集成程度比Java以前诞生的计算机语言要厉害很多,该语言针对多种异构平台的平台独立性而使用的多线程技术支持也是具有开拓性的一面,有时候在开发Java同步和线程安全要求很严格的程序时,往往容易混淆的一个概念就是内存模型。究竟什么是内存模型?内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,这点没有错,但是编译器、运行库、处理器或者系统缓存可以有特权在变量指定内存位置存储或者取出变量的值。【JMM】(Java Memory Model的缩写)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了final或synchronized明确请求了某些可见性的保证。 1)JSR133:

jvm内存结构

结构概览 JVM内存区域也称为Java运行时数据区域。其中包括:程序计数器、栈、堆、方法区等。 内存结构主要分为三大部分:堆内存,方法区和栈。 堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间。 方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。 JVM和系统调用之间的关系:

Java堆(Heap) Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。 简要归纳:新的对象分配是首先放在年轻代 (Young Generation) 的Eden区,Survivor区作为 Eden区和Old区的缓冲,在Survivor区的对象经历若干次收集仍然存活的,就会被转移到老年代Old中。 方法区(Method Area) 是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(存放的是Class)。它有一个别名,叫非堆。

Java新手必学的21个技术点

Java新手必学的21个技术点 以下为大家盘点作为Java新手必学的21个技术点,希望能够对想要学习编程,学习JAVA的人有些帮助! JNI Java Native Interface,可以允许Java中调用本地接口方法,一般用于C/C++代码的调用。需要注意的是在java中加载so/dll文件的路径问题,本身调用接口并不复杂,但是经常在是否加载了所需的本地接口库中花费较多时间 RMI RemoteMethodInvocation ,java语言特有的远程调用接口,使用还是比较简单方便。不过需要跨语言的情况下,就需要使用 webservice 等其他方式来支持。一般来说,程序都不需要使用RMI,不过可以在特定的情况下使用,我就在一个项目中,使用RMI来进行程序远程启动停止的控制。 标注 也是jdk5 之后引入的。Spring是个优秀的框架,最开始就以xml作为标准的配置文件。不过到了Spring3 之后,尤其是 spring-boot 兴起之后,越来越推崇使用标注来简化xml配置文件了,对于开发者来说,可以节省不少xml配置的时间。但是劣势是在于标注散落在各个类中,不像xml,可以对所有配置有个全局性的理解和管理,所以还没有办法说完全就取代所有的xml。对于一般开发者,会使用标注即可,一些公共组建的开发者可能会需要了解标注的定义和实现,可以在具体需要的时候再细看。 泛型

这是JDK5开始引入的新概念,其实是个语法糖,在编写java代码时会有些许便利,一般的应用或者是业务的开发,只需要简单使用,不一定会用到定义泛型这样的操作,但是开发一些基础公共组件会使用到,可以在需要的时候再细看这个部分,一般情况下只要会简单使用即可。 Maven的使用 Maven 也不是Java里面的内容,但是maven是革命性的,给java开发带来了巨大的便利。从依赖的引入和管理,开发流程的更新和发布产出,乃至版本的更新,使用maven可以大大简化开发过程中的复杂度,从而节省大量时间。可以说,maven已经成为java开发者的标配了。所以我把maven也作为一个java开发者对于基础必备的知识点。以后会再放上一些我的一些对于maven使用的经验和技巧等,这里就不再细说了 XML解析/ JSON解析 其实这两块内容都不是J2SE里面的内容,但是在日常开发中,和其他程序交互,和配置文件交互,越来越离不开这两种格式的解析。 不过对于一个开发者来说,能够了解一些XML/JSON具体解析的原理和方法,有助于你在各个具体的场景中更好的选择合适你的方式来使得你的程序更有效率和更加健壮。 XML:需要了解 DOM解析和 SAX解析的基本原理和各自的适用场景JSON:需要了解一些常用JSON框架的用法, 如 Jackson, FastJson, Gson 等。 时间日期处理

java面试2019

J2SE基础: 1. 九种基本数据类型的大小,以及他们的封装类。 2. Switch能否用string做参数? 3. equals与==的区别。 4. Object有哪些公用方法? 5. Java的四种引用,强弱软虚,用到的场景。 6. Hashcode的作用。 7. ArrayList、LinkedList、Vector的区别。 8. String、StringBuffer与StringBuilder的区别。 9. Map、Set、List、Queue、Stack的特点与用法。 10. HashMap和HashTable的区别。 11. HashMap和ConcurrentHashMap的区别,HashMap的底层源码。 12. TreeMap、HashMap、LindedHashMap的区别。 13. Collection包结构,与Collections的区别。 14. try catch finally,try里有return,finally还执行么? 15. Excption与Error包结构。OOM你遇到过哪些情况,SOF你遇到过哪些情况。 16. Java面向对象的三个特征与含义。 17. Override和Overload的含义去区别。 18. Interface与abstract类的区别。 19. Static class 与non static class的区别。 20. java多态的实现原理。 21. 实现多线程的两种方法:Thread与Runable。 22. 线程同步的方法:sychronized、lock、reentrantLock等。 23. 锁的等级:方法锁、对象锁、类锁。

适用于云计算的数据库开发和使用案例

适用于云计算的数据库 开发及案例
Copyright ? Versant Corp. All rights reserved.
0001
By Tiger Lau,CTO of Versant China

数据库发展简史
模型
层次化,结构化 网络化 关系型 对象型
优点
性能 性能 灵活性, 支持查询 性能,灵活性
缺点
灵活性, 对查询的支持 灵活性, 对查询的支持 性能 随机性较强, 支持既成的查询
数据
简单 简单 简单 复杂
这两点是现有很多系统的核心问题所在

什么是Versant云数据库
Versant数据库是大规模的分布式数据库 Versant数据库是专门为复杂数据提供服务的数据库:
设计目标是优化对象的存储与操作。 有能力管理任何类型的复杂模型。
简单类型: 整型, 字符串 数据类型与程序语言定义完全一致, 非自建数据结构 多值类型(Multi-valued): 动态类型数组 可以有效支持复杂程序数据结构,无需拆分
有能力管理对象间的任何关系。
对象间引用 (链接) 集合 (唯一性), 列表 (排序), 图 (关联性查找) 一次性存储,透明装载,能极大提高系统效率
Versant数据库能够实现数据与程序语言的无缝集成。

什么是复杂数据?
应用自身的数据结构以及数据非常复杂。
面向图形/GIS的复杂应用系统 基于复杂对象导航访问模式的应用系统
受到面向对象模型的影响而形成的复杂数据。
继承 集合 关联
由于无法简单的映射到关系模型而 形成的复杂数据。
大量的映射代码 大量的联合(JOIN)操作 性能不佳

JAVA 内存管理总结

JAVA 内存管理总结 1. java是如何管理内存的 Java的内存管理就是对象的分配和释放问题。(两部分) 分配:内存的分配是由程序完成的,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。 释放:对象的释放是由垃圾回收机制决定和执行的,这样做确实简化了程序员的工作。但同时,它也加重了JVM的工作。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。 2. 什么叫java的内存泄露 在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连(也就是说仍存在该内存对象的引用);其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。 3. JVM的内存区域组成 java把内存分两种:一种是栈内存,另一种是堆内存1。在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配;2。堆内存用来存放由new创建的对象和数组以及对象的实例变量在函数(代码块)中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间;在堆中分配的内存由java虚拟机的自动垃圾回收器来管理 堆和栈的优缺点 堆的优势是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。 缺点就是要在运行时动态分配内存,存取速度较慢;栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。 另外,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。 4. Java中数据在内存中是如何存储的 a) 基本数据类型

Java并发编程:JMM(Java内存模型)和volatile

Java并发编程:JMM(Java内存模型)和volatile 1. 并发编程的3个概念 并发编程时,要想并发程序正确地执行,必须要保证原子性、可见性和有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 1.1. 原子性 原子性:即一个或多个操作要么全部执行并且执行过程中不会被打断,要么都不执行。 一个经典的例子就是银行转账:从账户A向账户B转账1000元,此时包含两个操作:账户A减去1000元,账户B加上1000元。这两个操作必须具备原子性才能保证转账安全。假如账户A减去1000元之后,操作被打断了,账户B却没有收到转过来的1000元,此时就出问题了。 1.2. 可见性 可见性:即多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的最新值。 例如下段代码,线程1修改i的值,线程2却没有立即看到线程1修改的i的最新值: 假如执行线程1的是CPU1,执行线程2的是CPU2。当线程1执行i=10 时,会将CPU1的高速缓存中i的值赋值为10,却没有立即写入主内存中。此时线程2执行j=i,会

先从主内存中读取i的值并加载到CPU2的高速缓存中,此时主内存中的i=0,那么就会使得j最终赋值为0,而不是10。 1.3. 有序性 有序性:即程序执行的顺序按代码的先后顺序执行。 例如下面这段代码: 在代码顺序上i=1 在flag=true 前面,而JVM 在真正执行代码的时候不一定能保证i=1 在flag=true 前面执行,这里就发生了指令重排序。 指令重排序 一般是为了提升程序运行效率,编译器或处理器通常会做指令重排序: ?编译器重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序 ?处理器重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。CPU 在指令重排序时会考虑指令之间的数据依赖性,如果指令2必须依赖用到指令1的结果,那么CPU会保证指令1在指令2之前执行。 指令重排序不保证程序中各个语句的执行顺序和代码中的一致,但会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上例中的代码,i=1 和flag=true 两个语句先后执行对最终的程序结果没有影响,就有可能CPU 先执行flag=true,后执行i=1。2. java 内存模型 由于volatile 关键字是与java 内存模型相关的,因此了解volatile 前,需要先了解下java 内存模型相关概念

JAVA虚拟机规范

运行时数据区域 Java虚拟机所管理的内存将会包括以下几个运行时数据区域: 程序计数器 Program Counter Register是一块较小的内存空间,作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机规范里,字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等基础功能都依赖程序计数器完成。 由于Java虚拟机的多线程时通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因为,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间的计数器不影响,独立存储,因此这类区域为“线程私有”的内存。 如果线程正在执行一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址:如果执行的是Native方法,计数器值为空Undefined。此区域是虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 Java虚拟机栈 Java虚拟机栈也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执

行的内存模型:每个方法被执行时会创建一个栈帧Stack Frame用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用到执行完成,对应一个栈帧在栈中从入栈到出栈的过程。 局部变量表存放了编译期可知的8种基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据占用2个局部变量空间(Slot),其余数据类型占用1个。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。 在虚拟机规范中,这个区域规定了两种异常:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果栈可以动态扩展,当扩展无法申请到足够的内存时抛出OutOfMemoryError异常。 本地方法栈 Native Method Stack与虚拟机栈作用类似,不过是虚拟机栈为虚拟机执行的Java方法(即字节码)服务,而本地方法栈为虚拟机使用的Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。 Java堆 Java虚拟机规范描述是:所有对象实例以及数组都在堆上分配。堆是所有线程共享的,但从内存分配角度看,线程共享的堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例。 方法区 Method Area是线程共享的,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 Java虚拟机规范对方法区限制比较宽松,除了和堆一样不需要连续的内存和可用选择固定大小或者可扩展外,还可以选择不实现垃圾收集。一般来说,垃圾收集行为在这个区域比较少见,但并非数据进入了方法区就永久存在了。这个区域的内存回收目标主要针对常量池的回收和类型的卸载。该区域无法分配内存时,抛出OutOfMemoryError异常。 运行时常量池Runtime Constant Pool是方法区一部分,Class文件除了有类的版本、字段、方法和接口等描述信息外,还有常量池Constant Pool Table,用于存放编译期生成的字面量和符号引用。 Java语言不要求常量一定只能在编译期生成,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,比如String类的intern()方法。 对象访问 Object obj=new Object()

深入理解 Java String#intern() 内存模型

深入理解 Java String#intern() 内存模型

深入理解 Java String#intern() 内存模型 字符串常量池是一个固定大小的HashMap,桶的数量默认是1009, 从Java7u40开始,该默认值增大到60013。在Java6当中,字符串常量池是放在Perm空间的,从Java7开始,字符串常量池被移到Heap空间。下面,我们通过测试程序来窥探字符串常量池在Java6,Java7两个不同版本底下的内存分配情况。 测试程序 public class StringPoolTest { public void testStringPoolWithLongString(){ long i=0; while(true){ String longString = "This is a very long stri ng, very very long string to test the gc behavior of the string constant pool"+i; longString.intern(); i++; } } public static void main(String[] args){ StringPoolTest stringPoolTest = new StringPoolTes t(); stringPoolTest.testStringPoolWithLongString(); } } 测试程序很简单,一个死循环,循环里面通过递增变量i制造唯一的字符串,然后用main函数启动程序。 Java 6 我们使用版本Jdk1.6.0_29来跑该程序,打开Java VisualVM监控,可以看到,Perm区不断发生GC,由此的出结论,虽然字符串常量池放在Perm空间,但当Perm空间接近满的时候,JVM会将字符串常量池中的无用字符串回收掉。

15-JVM面试题(87题)

JVM面试题 1、java中会存在内存泄漏吗,请简单描述。 会。自己实现堆载的数据结构时有可能会出现内存泄露,可参看e?ective java. 2、64 位JVM 中,int 的长度是多数? Java 中,int 类型变量的长度是一个固定值,与平台无关,都是32 位。意思就是说,在32 位和64 位的Java 虚拟机中,int 类型的长度是相同的。 3、Serial 与Parallel GC 之间的不同之处? Serial 与Parallel 在GC 执行的时候都会引起stop-the-world。它们之间主要不同serial 收集器是默认的复制收集器,执行GC 的时候只有一个线程,而parallel 收集器使用多个GC 线程来执行。4、32 位和64 位的JVM,int 类型变量的长度是多数? 32 位和64 位的JVM 中,int 类型变量的长度是相同的,都是32 位或者4个字节。 5、Java 中WeakReference 与SoftReference 的区别? 虽然WeakReference 与SoftReference 都有利于提高GC 和内存的效率,但是WeakReference ,一旦失去最后一个强引用,就会被GC 回收,而软引用虽然不能阻止被回收,但是可以延迟到JVM 内存不足的时候。

6、JVM 选项-XX:+UseCompressedOops 有什么作用?为什么要使用 当你将你的应用从32 位的JVM 迁移到64 位的JVM 时,由于对象的指针从32 位增加到了64 位,因此堆内存会突然增加,差不多要翻倍。这也会对CPU缓存(容量比内存小很多)的数据产生不利的影响。因为,迁移到64 位的JVM主要动机在于可以指定最大堆大小,通过压缩OOP 可以节省一定的内存。通过-XX:+UseCompressedOops 选项,JVM 会使用32 位的OOP,而不是64 位的OOP。 7、怎样通过Java 程序来判断JVM 是32 位还是64位? 你可以检查某些系统属性如sun.arch.data.model 或os.arch 来获取该信息。 8、32 位JVM 和64 位JVM 的最大堆内存分别是多数? 理论上说上32 位的JVM 堆内存可以到达2^32,即4GB,但实际上会比这个小很多。不同操作系统之间不同,如Windows 系统大约1.5 GB,Solaris 大约3GB。64 位JVM 允许指定最大的堆内存,理论上可以达到2^64,这是一个非常大的数字,实际上你可以指定堆内存大小到100GB。甚至有的JVM,如Azul,堆内存到1000G 都是可能的。 9、JRE、JDK、JVM 及JIT 之间有什么不同? JRE 代表Java 运行时(Java run-time),是运行Java 引用所必须的。JDK 代表Java 开发工具(Java development kit),是Java 程序的开发工具,如Java编译器,它也包含JRE。JVM 代表Java 虚拟机(Java virtual machine),它的责任是运行Java 应用。JIT 代表即时编译(Just In Time compilation),当代码执行的次数超过一定的阈值时,会将Java 字节码转换为本地代码,如,主要的热点代码会被准换为本地代码,这样有利大幅度提高Java 应用的性能。 10、解释Java 堆空间及GC? 当通过Java 命令启动Java 进程的时候,会为它分配内存。内存的一部分用于创建堆空间,当程序中创建对象的时候,就从对空间中分配内存。GC 是JVM 内部的一个进程,回收无效对象的内存用于将来的分配。 11、JVM 内存区域

相关主题
相关文档
最新文档