概述

ES是一个搜索引擎,我们之所以要使用它就是为了借助它快速构建全文索引,帮助我们快速检索数据。

本文接着上篇文章ElasticSearch1初步使用继续来通过blogs索引实例说明如何简单的借助ES实现轻量级搜索功能。

问题

本文主要以应用ES为基本问题,主要探索ES通过GET方法进行搜索的使用方法。在实验的示例中本文也会简要的描述ES相关的理论知识。

如何使用索引进行搜索,对搜索结果进行分页,并使用简单的条件来过滤搜过结果是本文需要探讨的问题。

方法

本文采用对照RDBMS中SELECT功能的方法来描述ES中的轻量级搜索的概念,从总体上来讲,在ES中搜索数据其实和在RDBMS中SELECT数据是一样的,都可以指定搜索(查询)条件,都可以设置返回字段,也都可以进行一定的聚合运算。只不过ES搜索引擎使用全文搜索得到的结果会根据内容与搜索关键字的匹配程度给与每个结果一个权重,这个权重就作为搜索结果排序的依据。而RDBMS完全是依照ORDER BY子句中指定的排序规则进行排序的。

说明:

ES REST API所提供的完全在URL中描述参数的接口就是轻量级搜索接口,换句话说轻量级搜索接口都是使用GET方法的。所以文中示例使用的地址和API语法总结如没有对HTTP请求方法做出特殊声明,均使用GET方法。

对本文中语法说明特殊字符的声明:

  • {a} 代表a变量,如{host}代表主机名或ip地址,{index}代表索引名
  • [] 代表可出现也可不出现,一般情况下带有[]标识的变量出现和不出现会有不同的含义。对于URL开头的[http[s]://]表达式,如果省略整个表达式则系统会默认使用http协议。
  • … 对返回结果或请求内容进行部分省略。

全量数据搜索

语法

请求信息:

[http[s]://]{host}:{port}/[{index}/[{type}/]]_search

响应信息:

{
    "took": 7,//执行搜索请求的耗时,单位毫秒
    "timed_out": false,//搜索是否超时
    "_shards":{//参与本次搜索的分片信息
        "total": 5,//参与本次搜索的分票总个数
        "successful": 5,//有多少个分片成功的完成了搜索任务
        "failed": 0 //有多少分片执行搜索任务失败
    },
    "hits":{ //搜过结果信息
        "total": 1,//匹配到的文档总数
        "max_score": 1.0193838,//查询所匹配文档的 _score 的最大值。
        "hits":[...]//匹配到的文档详细信息结果集
    }
}

在没有指定分页条件的情况下,响应信息默认返回10条结果。

示例

搜索blogs索引下,articles类型下的全部文档。这是一个没有任何过滤条件的最简单的搜索。

请求信息:

http://ubuntu:9200/blogs/articles/_search

响应信息:

{
  ...,
    "hits" : [
      {
        "_index" : "blogs",
        "_type" : "articles",
        "_id" : "AV6GlxJvP5Roqj_P-AOw",
        "_score" : 1.0,
        "_source" : {
          "title" : "ES自动生成id索引",
          "author" : "为为",
          "since" : "2017-09-16 20:20:20",
          "categorie" : "搜索引擎",
          "tags" : [
            "java",
            "研发"
          ],
          "body" : "如果你无法手动对文档进行编号,可以使用POST方法向ES中索引一个新文档,其操作方法和链接规则如图8所示。"
        }
      },
      ...//更多内容暂时省略
    ]
  }
}

简单条件筛选

简单的条件筛选就像是给SQL中的SELECT语句加上WHERE子句,从而限定只查找满足某些条件的结果。

语法

在请求路径中使用q参数,并将查询条件赋值给q

[http[s]://]{host}:{port}/[{index}/[{type}/]]_search?q={param_name}:{param_value}
示例

我们搜索一下文章的tags包含 标签1 的文章。

请求参数:

http://ubuntu:9200/blogs/articles/_search?q=tags:标签1

响应参数:

{
  ...,
  "hits" : {
    ...
    "hits" : [
      {
        "_index" : "blogs",
        "_type" : "articles",
        "_id" : "1",
        "_score" : 1.0193838,
        "_source" : {
          "title" : "第一篇文章",
          "author" : "马华",
          "since" : "2017-09-10 20:20:20",
          "categorie" : "科学读物",
          "tags" : [
            "标签1",
            "标签2"
          ],
          "body" : "测试文章内容"
        }
      }
    ]
  }
}

分页

我们在SQL中SELECT语句可以使用LIMIT关键字进行分页,来保证我们每次查询只拿符合需求的数据条数。刚刚也提到过ES也支持分页,默认每页有10条数据。

语法

在搜索URL中可以使用size参数指定页大小,from应跳过的结果集条数。

请求信息:

[http[s]://]{host}:{port}/[{index}/[{type}/]]_search[?[size={size}][[&]from={from}]]
示例

不使用任何过滤条件搜索blogs索引type类型下的所有文档,指定页大小为2,从第跳过1条结果。

请求信息:

http://ubuntu:9200/blogs/articles/_search?size=2&from=1

响应信息请自行演示。

多索引和类型

如果你需要在一个或多个特殊的索引并且在一个或者多个特殊的类型中进行搜索。我们可以通过在URL中指定特殊的索引和类型达到这种效果,下面举例说明如何使用多索引或多类型。
在所有的索引中搜索所有的类型

http://ubuntu:9200/_search

在 blogs 索引中搜索所有的类型

http://ubuntu:9200/blogs/_search

在 blogs 和 pictures 索引中搜索所有的文档

http://ubuntu:9200/blogs,pictures/_search

在任何以 b 或者 p 开头的索引中搜索所有的类型

http://ubuntu:9200/b*,g*/_search

在 blogs 索引中搜索 aiticles 类型

http://ubuntu:9200/blogs/articles/_search

在 blogs 和 pictures 索引中搜索 articles 和 a_images 类型的文档

http://ubuntu:9200/blogs,pictures/articles,a_images/_search

在所有的索引中搜索 articles 和 a_images 类型的文档

http://ubuntu:9200/_all/articles,a_images/_search

当在单一的索引下进行搜索的时候,Elasticsearch 转发请求到索引的每个分片中,可以是主分片也可以是副本分片,然后从每个分片中收集结果。多索引搜索恰好也是用相同的方式工作的,只是会涉及到更多的分片。

多个搜索条件

刚刚介绍的条件搜索只能使用一个搜索条件,而我们一般的业务都需要更为复杂的搜索条件。

_all字段

在 blogs/aiticles 中搜索“为为”的相关信息,注意该搜索中并未指定“为为”属于哪个字段。

http://ubuntu:9200/blogs/_search?q=为为
同时搜索多个字段

在 blogs/aiticles中搜索author包含”为为”, title包含”ES”的信息。

http://ubuntu:9200/blogs/articles/_search?q=+title:ES自动生成id索引 +author:为为
  • 前缀表示必须与查询条件匹配。类似地, -前缀表示一定不与查询条件匹配。没有 + 或者 -的所有其他条件都是可选的——匹配的越多,文档就越相关。在存在多个条件时,如果没有明确使用default_operator=AND指定多个条件的关系为AND,则多个条件的关系为OR。
同一个字段下多种可能性

在 blogs/aiticles中搜索author包含”为为”,tags为”java”或”编码”的信息。

http://ubuntu:9200/blogs/articles/_search?q=+tags:(java 研发)  +author:为为

在 blogs/aiticles中搜索author包含”为为”,tags为”java”或”编码”的信息。

http://ubuntu:9200/blogs/articles/_search?q=+tags:(java 研发)  +author:为为&default_operator=AND

总结

经过本文示例我们可以看出ES不仅可以作为一个NoSQL数据库,存储格式化的JSON数据,其更强大的功能在于搜索。ES不仅会存储文档,文档中的每个字段都将被索引并且可以被查询 。不仅如此,在简单查询时,Elasticsearch 可以使用 所有(all)索引字段,快度返回结果,我们甚至不必指定具体要搜索哪个字段就。

总之ES的搜索可以完成以下任务:

  • 在结构化的数据(JSON)中使用结构化查询。
  • 全文检索。

    轻量级搜索虽然简单方便,但其也有缺点:

  • 当查询字符串中很小的语法错误,像 - , : , / 或者 “ 不匹配等,将会返回错误而不是搜索结果。
  • 允许任何用户在索引的任意字段上执行可能较慢且重量级的查询,这可能会暴露隐私信息,甚至将集群拖垮。

    基于以上两点原因,不推荐向用户直接开放轻量级搜索功能,一般情况下只在开发调试中使用。

引用

本文是我在学习使用ES时的笔记,在本文的写过过程中参考了大量其它资料,有些材料来源于网络,我由衷的表示感谢,但由于原作者不明,恕不能一一记述。

  1. Elasticsearch 权威指南.——https://www.elastic.co/
  2. Elasticsearch技术解析与实战/朱林编著.——北京:机械工业出版社,2016.12(数据分析与决策技术丛书)

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

链接

评论和共享

elastic search初步使用

ElasticSearch是一个基于Lucene的搜索引擎,是当前世界上最受欢迎的全文搜索引擎,其主要特点如下:

  • 横向可拓展性: 往集群中增加机器时只需要更改一点配置就可以将新机器加入集群
  • 分片机制: 同一个索引切分成不同的分片
  • 高可用: 提供复制集机制,一个分片可以设置多个复制集,某台机器如果宕机不至于使集群无法工作
  • 使用简单,基于 REST api就可以完成搜索引擎的全部工作,所需学习成本低。

如无特殊声明,本文和后续文档将 Elastic Search 简称为ES

全文搜索

全文搜索是指搜索程序扫描整个文档,通过一定的分词方法,对每一个词建立索引,并指明该词在文档中出现的位置和次数,最后搜索引擎再通过索引关键字搜索出文档并返回给用户的过程。

Lucene

Lucene是Apache下的一个开源全文搜索引擎工具,提供了完整的查询引擎和索引引擎和部分文本分析引擎。不过Lucene仅仅是一个工具包,其目的是为了让研发人员能够通过这些工具包快速的为自己的应用搭建一个搜索引擎或者基于这些工具包开发出一个完整的搜索引擎。

倒排序索引

Lucene中的索引采用的是倒排序索引的模式。所谓的倒排序索引(Inverted Index)是通过属性的值来确定整条纪录的位置,而不是由纪录来确定属性的值。倒排索引把普通索引中的文档编号和值的关系倒过来,变成:“关键词”对“拥有该关键词的所有文章号”。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(inverted file)。倒排索引主要就由索引关键字和倒排文件所组成。

建立搜索引擎的关键步骤就在于建立倒排索引,倒排索引一般以以下数据结构出现:

关键字 文章号[出现频率] 出现位置
简单 1[3] 2,9,89
美女 5[1] 1
tip 9[2] 2,7,22

表1 倒排索引数据结构示例

倒排索引在存储上使用LSM树,维护其索引方法是当需要新增文档进入系统时,首先解析文档,之后更新内存中维护的临时索引,文档中出现的每个单词,在其倒排表列表末尾追加倒排表列表项;一旦临时索引将指定内存消耗光,即进行一次索引合并,这里需要倒排文件里的倒排列表存放顺序已经按照索引单词字典顺序由低到高排序,这样直接顺序扫描合并即可。

实现

lucene在实现倒排索引时将索引划分为词典文件(Term Dictionary)、频率文件(Frequencies)、位置文件(Positions)保存。其中词典文件不仅包含关键词,还有该关键词指向频率文件和位置文件的指针。

为了节省存储空间,提升索引效率。Lucene还对索引进行了压缩。

安装

ES使用JAVA语言开发,所以在安装ES之前需要在系统中安装有JDK。

表2是本文中的示例所使用的软件环境信息。

类型 名称 版本
操作系统 ubuntu server 16.04.2 LTS
内核 Kernel 4.4.0-62-generic
容器 docker 1.28
java openjdk 1.8.0_141
搜索引擎 elasticsearch 5.5.2

表2 本机软件环境信息

docker镜像安装

为了方便部署,我直接采用docker镜像的方式搭建ES。镜像启用命令为:

$ docker run -d -v "$PWD/esdata":/usr/share/elasticsearch/data --name elasticsearch -H elasticsearch elasticsearch  -Etransport.host=0.0.0.0 -Ediscovery.zen.minimum_master_nodes=1 elasticsearch

系统内核参数

由于elastic search需要用到nio和mmap(虚拟内存映射)技术,在启动该镜像之前首先需要检查一下系统内核对虚拟内存映射数目的限制(vm.max_map_count参数)是否大于262144,如果不满足这个条件,elastic search镜像将不会启动。

我们可以通过以下两种方式去设置系统内核的vm.max_map_count参数:

sysctl -w vm.max_map_count=262144

如果想永久修改此参数,可以通过修改 /etc/sysctl.conf 文件的方式使其永久性的成为系统内核参数。

测试

在linux系统下,我们可以使用 curl 程序来完成REST接口的调用与测试,《Elasticsearch权威指南》对 curl 调用REST接口的方式描述如图1所示。

image
图一 curl命令示意图

使用以下命令可以检测elastic search是否启动成功。

curl -i -XGET 'localhost:9200/'

-i 参数是说明要打印http请求头信息,GET是请求方法,localhost:9200/就是我们服务器的地址和端口,elastic search默认与外部交互的端口就是9200.如果服务启动成功,你会收到类似虾苗的响应信息。

HTTP/1.1 200 OK
content-type: application/json; charset=UTF-8
content-length: 331

{
  "name" : "Franz Kafka",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "JP677C9kRNqjEWYnFae_gQ",
  "version" : {
    "number" : "5.5.2",
    "build_hash" : "b2f0c09",
    "build_date" : "2017-08-14T12:33:14.154Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.0"
  },
  "tagline" : "You Know, for Search"
}

使用

为了方便操作,本文与后续内容都将使用chrome浏览器的 Restlet Client插件来模拟REST请求,在这里推荐一下,感谢该插件的作者将本插件开源。

创建索引

Elastic Search就像是一个nosql数据库一样,存储我们要进行查询的信息,在ES中,数据库名就是索引名。一个 Elastic Search 集群可以包含多个索引,相应的每个索引可以包含多个类型 。这些不同的类型存储着多个文档 ,每个文档又有多个属性。

我们可以使用PUT方法创建blogs库,详细请求信息如图2所示。

image
图二 创建blogs数据库

如果收到以下回应内容,则说明blogs数据库创建成功。

image
图3 创建blogs数据库成功的回应信息

使用GET地址 http://ubuntu:9200/_cat/indices?v可以查看库的状态,返回结果如下:

health status index uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   blogs Y9hkRePSQmiAzjNQ_K_FSw   5   1          0            0       810b           810b

其中yellow代表健康度,状态是活动,索引为blogs,主分片数量5,复制集1,已存储的文档数为0.

新建文档

ES是一个面向文档的数据库,每一个文档都代表一条完整的实体记录,本文实例中一个文档就代表一篇文章。存储一个文档到ES的行为就叫做索引,在索引一个文档之前,首先应该明确应该将文档存储在哪里。

本文对于数据存储使用以下设计:

  • 每一篇文章被定义为一个文档,包含作者,发布时间,文章分类,正文等信息。
  • 每个文档都属于articles类型
  • articles类型保存在blogs索引里
  • blogs索引在我们的ES集群中。

    按照这个设计思路,我们向ES中索引一篇文章,其请求信息图4所示。

image
图4 向blogs索引articles类型插入一篇新文档

如果新建文档成功,则会收到HTTP状态码为201的回应,回应信息如图5所示。

image
图5 向blogs索引articles类型插入一篇新文档回应信息

接着我们再索引几篇新文档,如图6,图7.
image
图6 向blogs索引articles类型插入更多新文档
image
图7 向blogs索引articles类型插入更多新文档。

如果你无法手动对文档进行编号,可以使用POST方法向ES中索引一个新文档,其操作方法和链接规则如图8所示。
image
图8 向blogs索引articles类型POST插入一篇新文档

从其相应结果中可以看出ES通过自己的规则为文档增加了_id字段,相应结果如图9所示.
image
图9 向blogs索引articles类型POST插入一篇新文档响应信息

检索文档

在ES中检索已存在的文档只需使用GET方法访问刚刚新增文档时的连接就可以了。

GET http://ubuntu:9200/blogs/articles/2

返回结果如图10所示。
image
图10 从blogs索引articles类型检索文档

_index属性代表文档所属索引, _type是文档所属类型, _id是文档编号, _version是文档的版本(每对文档做一次修改, _version就会加1), _found代表是否查询到指定文档, _source就是文档中所存储的内容。

更新与删除文档

使用REST API对ES中的数据进行修改或删除也是极其方便的。如果我们要删除某个文档,只需使用DELETE方法访问这个文档的链接就可以了。

DELETE http://ubuntu:9200/blogs/articles/2

我们先介绍一种全量更新文档的方法,使用PUT方法访问我们的文档地址,参数信息加上文档的新内容就可以全量更新文档了。文档更新后,ES会犯规给我们新的文档版本和操作结果。

示例,对文档2进行全量更新,其请求与回应信息如图11,图12所示。

image
图11 PUT文档全量更新
image
图12 PUT文档全量更新响应

ES的文档不能被修改,只能被替换。但ES为我们提供了 update API。通过update API操作从整体来看,我们可以对文档的某个位置进行部分更新。不过在底层实现上 update API 还是需要进行完整的检索-修改-重建索引 的处理过程。 区别在于这个过程发生在分片内部,这样就避免了多次请求的网络开销。因为减少了检索和重建索引步骤之间的时间,更新此文档时与其他进程的更新操作冲突的可能性也会减少。
update 请求最简单的一种形式将待更新的字段作为doc的参数, 它只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段或者新增字段。

示例,修改文档2的author和tags属性,其请求、新文档内容如图13、图14所示。
image
图13 POST update文档部分更新
image
图14 POST update文档部分更新后的结果

如果更新文档是一个并发操作,当前工作在更新文档的检索步骤会获取当前版本号,在重建索引之前会检查文档此时的版本号是否与检索时的版本号一致,如果版本号不一致,当前线程就会发生操作失败,放弃文档的修改。如果业务上允许重复更新的话,可以通过retry_on_conflict属性,来设置失败重试。该属性用法如下面代码所示。对于不能重试的业务场景,update API提供了version属性进行乐观并发控制。

PUT http://ubuntu:9200/blogs/articles/2/_update?retry_on_conflict=5

搜索文档

搜索是ES的核心,不过鉴于篇幅原因,关于搜索功能的使用将放到下篇文章中。

引用

本文是我在学习使用ES时的笔记,在本文的写过过程中参考了大量其它资料,有些材料来源于网络,我由衷的表示感谢,但由于原作者不明,恕不能一一记述。

  1. Elasticsearch 权威指南.——https://www.elastic.co/
  2. Elasticsearch技术解析与实战/朱林编著.——北京:机械工业出版社,2016.12(数据分析与决策技术丛书)

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

链接

评论和共享

摘要

工欲善其事,必先利其器。作为JAVA的开发者,如果说会使用jdk是工作的基础,那么了解jdk的实现原理,甚至自己写一些框架或包就是工作的进阶。尤记得听一名老工程师跟我说过,等你把jdk都研究透了,你就是一名合格的高级工程师。Map集合在JAVA开发中很重要,就常用程度来讲,其在现代业务开发的代码里出现的频率位居前列。今天这篇文章主旨就是探索Map及其常见的实现类。
本文着重从jdk8代码出发来分析Map,与之响应的会讨论部分数据结构和算法方面的知识。我希望读者你已经有了很好的java基础,能够了解常用的设计模式并对面向对象编程思想拥有深刻的理解。本文会忽略部分过于基础的内容和不在本文讨论范围内的部分API。
首先,文章会简要的介绍Map接口,接着去分析jdk中几个最常见的Map接口的实现类。文章会花些笔墨在HashMap这个最常见的Map类上,其次会去分析TreeMap等类。最后文章还会对比HashMap来研究适合在并发场景下使用的ConcurrentHashMap,来分析它究竟是如何优化才能保证HashMap的线程安全性的。

方法

基本接口

Map是一种键-值对应的数据结构,java.util.Map 接口定义了Map常用的api。接下来我以代码的形式简要的对Map接口进行介绍。

package java.util;

import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.io.Serializable;

/**
 * 返回map中键值对集合的数目
 */
int size();

/**
 * 判断map中是否有键值对信息,如果该Map为空则返回 true 。
 */
boolean isEmpty();

/**
 判断map中是否有指定的key元素,如果有该元素,则返回 true 。
 */
boolean containsKey(Object key);

/**
 如果map中有一个或多个key所映射的值为value,则返回 true 。
 */
boolean containsValue(Object value);

/**
 根据指定的key在map中获取与之对应的value并返回。
 */
V get(Object key);

/**
 将key/value对放入到map中,如果map中已经存在了键为key的元素,则将原来的值替换为新value。
 */
V put(K key, V value);

/**
 从map中移除键为key的元素并返回
 */
V remove(Object key);


/**
将目标集合中的所有元素全部放到该map中
 */
void putAll(Map<? extends K, ? extends V> m);

/**
 清空集合中的所有元素
 */
void clear();


/**
 返回map中所有key的集合
 该方法所返回的集合并非创建一个全新集合,如果对该集合进行删除操作会直接影响到原来map中的元素
 */
Set<K> keySet();

/**
 返回map中所有value的集合
 该方法所返回的集合也与map有对应关系,对map本身所做的删除操作会进而影响到此集合
 */
Collection<V> values();

/**
 返回map中的元素集合
 该元素集合与map还存在映射关系
 */
Set<Map.Entry<K, V>> entrySet();

/**
 * map元素接口
 将map元素封装成键-值对的格式
 */
interface Entry<K,V> {
  ...//Entry中的内容暂不介绍
}

boolean equals(Object o);

int hashCode();

// Defaultable methods

/**
 返回指定的key所对应的value或默认值
 * @since 1.8
 */
default V getOrDefault(Object key, V defaultValue) {
    V v;
    return (((v = get(key)) != null) || containsKey(key))
        ? v
        : defaultValue;
}

/**
 对map集合进行遍历操作
 * @since 1.8
 */
default void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    for (Map.Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }
        action.accept(k, v);
    }
}

/**
 将符合条件的map中的元素进行替换
 * @since 1.8
 */
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
    Objects.requireNonNull(function);
    for (Map.Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }

        // ise thrown from function is not a cme.
        v = function.apply(k, v);

        try {
            entry.setValue(v);
        } catch(IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }
    }
}

/**
 如果map中没有指定的key,或指定的key对应的value值为null,则将key-value中的value放入到map中,并返回新value。如果map中已经存在该key,且value不是null,则返回原值。
 * @since 1.8
 */
default V putIfAbsent(K key, V value) {
    V v = get(key);
    if (v == null) {
        v = put(key, value);
    }

    return v;
}

/**
以同时指定键值对的方式来从map中移除元素,返回操作成功状态
 * @since 1.8
 */
default boolean remove(Object key, Object value) {
    Object curValue = get(key);
    if (!Objects.equals(curValue, value) ||
        (curValue == null && !containsKey(key))) {
        return false;
    }
    remove(key);
    return true;
}

/**
 如果原元素存在,则将原元素的值替换为新值
 * @since 1.8
 */
default boolean replace(K key, V oldValue, V newValue) {
    Object curValue = get(key);
    if (!Objects.equals(curValue, oldValue) ||
        (curValue == null && !containsKey(key))) {
        return false;
    }
    put(key, newValue);
    return true;
}

/**
 当前的key在map中有对应的映射时,使用新元素替换原有元素
 * @since 1.8
 */
default V replace(K key, V value) {
    V curValue;
    if (((curValue = get(key)) != null) || containsKey(key)) {
        curValue = put(key, value);
    }
    return curValue;
}

/**
 如果指定的key在map中的映射为空(或者map中不包含此key),则尝试使用给定的算法去计算一个值作为key的映射,并将这个元素存放进map中。
 * @since 1.8
 */
default V computeIfAbsent(K key,
        Function<? super K, ? extends V> mappingFunction) {
    Objects.requireNonNull(mappingFunction);
    V v;
    if ((v = get(key)) == null) {
        V newValue;
        if ((newValue = mappingFunction.apply(key)) != null) {
            put(key, newValue);
            return newValue;
        }
    }

    return v;
}

/**
 如果map中已有指定的key和其映射,则尝试使用给定的函数重新计算一个value替换原值
 * @since 1.8
 */
default V computeIfPresent(K key,
        BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    V oldValue;
    if ((oldValue = get(key)) != null) {
        V newValue = remappingFunction.apply(key, oldValue);
        if (newValue != null) {
            put(key, newValue);
            return newValue;
        } else {
            remove(key);
            return null;
        }
    } else {
        return null;
    }
}

/**
 使用给定的函数计算一个在map中与key对应的新值用来替换掉原来与key映射的value
 * @since 1.8
 */
default V compute(K key,
        BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    V oldValue = get(key);

    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue == null) {
        // delete mapping
        if (oldValue != null || containsKey(key)) {
            // something to remove
            remove(key);
            return null;
        } else {
            // nothing to do. Leave things as they were.
            return null;
        }
    } else {
        // add or replace old mapping
        put(key, newValue);
        return newValue;
    }
}

/**
 如果在map中与key所对应的value为null,则使用新value去替换原来的值,否则就使用remappingFunction在原值和新值之间计算出一个值用来做key的映射。
 * @since 1.8
 */
default V merge(K key, V value,
        BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if(newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}
}

/*

*/

我们看到,jdk8对map类进行了大量的改动,增加了很多带有默认方法的api,其中包括如foreach等实用的api。

Map骨架

java.util.AbstractMap 类是一个抽象类,其对map接口提供了最简单的模版方法实现,如求map的entrySet(),hashCode()等方法。我们常用的类HashMap就是继承自此类,不过由于此类实现的时间较早(jdk1.2),当前大部分继承自此类的的类激斗都对此类的方法进行了更符合性能的优化重写。

哈希表 (HashTable)

java.util.HashTable是数据结构中的哈希表的java实现,其内部存储key-value形式的键值对元素,任何不是null的对象都可以被当做key或value。为了能够正常的检索、存储对象,所有的key都应该有可用的hashCode()方法。哈希表的性能主要由初始化大小和负载因数两个重要的要素所影响,HashTable基于大量数据进行测验与分析,综合考虑存储时的空间兼用与检索时的时间消耗情况比,将负载因数的初始值设置为 0.75f ,如无必要情况,无需修改此值。对于初始化大小这个参数,其默认值为11,我们可以根据实际业务任意进行定制。HashTable是线程安全的,但是在实际开发使用中在没有线程安全的场景下java官方推荐我们直接使用HashTable,在高并发的场景下,我们更应该优先考虑使用java.util.concurrent.ConcurrentHashMap类。

public class Hashtable<K,V>
  extends Dictionary<K,V>
  implements Map<K,V>, Cloneable, java.io.Serializable {

  /** 哈希表数据实际存放在Entry数组中 */
  private transient Entry<?,?>[] table;

  /** 哈希表中entry的数量 */
  private transient int count;

  /* 重新进行hash计算的阈值 */
  private int threshold;

  /** 哈希表的负载因数 */
  private float loadFactor;

  /**
   哈希表已修改数据结构的次数
   增加或减少Entry节点的数目或对哈希表进行扩容都会触发该值的变化,modCount主要用来在高并发场景下进行快速失败使用。
   */
  private transient int modCount = 0;

  /** 序列化版本id */
  private static final long serialVersionUID = 1421746759512286392L;


}
/**
while(i.hasBext()){
  Entry e = i.next();
  if(value == e.getValue || value.equals(e.getValue))
    return true;
  }
  return false;
  ****/

哈希表的基本属性中有一个 threshold 属性,这个属性代表着重新分配哈希空间的阈值,当哈希表中存放的数据超出此值所标识的大小,哈希表就会进行自动扩容并重新计算 threshold 值,其计算公式为 threshold = (int)(capacity * loadFactor)

首先我们分析一下用来检查是否包含某元素的contains()方法:

public synchronized boolean contains(Object value) {
      if (value == null) {
          throw new NullPointerException();
      }

      Entry<?,?> tab[] = table;
      for (int i = tab.length ; i-- > 0 ;) {
          for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
              if (e.value.equals(value)) {
                  return true;
              }
          }
      }
      return false;
  }
  public synchronized V get(Object key) {
      Entry<?,?> tab[] = table;
      int hash = key.hashCode();
      int index = (hash & 0x7FFFFFFF) % tab.length;
      for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
          if ((e.hash == hash) && e.key.equals(key)) {
              return (V)e.value;
          }
      }
      return null;
  }

  /** 为哈希表添加节点 ****/

  private void addEntry(int hash, K key, V value, int index) {
      modCount++;

      Entry<?,?> tab[] = table;
      if (count >= threshold) {
          // Rehash the table if the threshold is exceeded
          rehash();

          tab = table;
          hash = key.hashCode();
          index = (hash & 0x7FFFFFFF) % tab.length;
      }

      // Creates the new entry.
      @SuppressWarnings("unchecked")
      Entry<K,V> e = (Entry<K,V>) tab[index];
      tab[index] = new Entry<>(hash, key, value, e);
      count++;
  }

  /** 向哈希表中存放某值 */
  public synchronized V put(K key, V value) {
      // Make sure the value is not null
      if (value == null) {
          throw new NullPointerException();
      }

      // 确认哈希表中不存在该key
      Entry<?,?> tab[] = table;
      int hash = key.hashCode();
      int index = (hash & 0x7FFFFFFF) % tab.length;
      @SuppressWarnings("unchecked")
      Entry<K,V> entry = (Entry<K,V>)tab[index];
      for(; entry != null ; entry = entry.next) {
          if ((entry.hash == hash) && entry.key.equals(key)) {
              V old = entry.value;
              entry.value = value;
              return old;
          }
      }

      addEntry(hash, key, value, index);
      return null;
  }
  /** 向哈希表中添加节点 ****/
  public synchronized void clear() {
      Entry<?,?> tab[] = table;
      modCount++;
      for (int index = tab.length; --index >= 0; )
          tab[index] = null;
      count = 0;
  }

  public Set<K> keySet() {
      if (keySet == null)
          keySet = Collections.synchronizedSet(new KeySet(), this);
      return keySet;
  }

HashTable的contains方法只能遍历查找,其时间复杂度是O(n),在最坏的情况下我们可能需要遍历整个哈希表中的所有Entry。HashTable中可能发生线程安全问题的方法都使用 synchronized 关键字添加互斥锁,如此以来,虽然保证了其线程安全性,但对方法实现互斥会让整个对象都受影响,这也间接的给出了java不推荐在高并发的场景下使用HashTable的解释。其get()方法首先对key进行哈希运算然后通过取模等操作计算出key所在的索引,然后进行遍历操作,其时间复杂度为 O(logn)。这种算法被称之为哈希查找,拥有极高的效率,比较推崇。向哈希表中存放数据时,首先根据key的hash计算索引,然后在索引的table上找到其Entry链表,遍历该链表中是否已存在该key,如果已存在则将value替换,否则就在table上index索引的Entry链表后追加该元素。这种算法原理符合人类的思维形式,比较利于理解,可计算机CPU并不擅长求余计算,进行哈希查找时有一定的计算资源浪费。至于clear()方法就是遍历清除table上的每一个节点,将哈希表的大小置0。等待垃圾回收时直接就将哈希表中的内容全部回收,释放内存。哈希表的keySet()等方法也都是线程安全的,其实现原理就是通过Collections.synchronizedSet()方法将原有的keySet结合哈希表本身作为互斥锁转化成线程安全的集合。keySet的迭代操作使用哈希表内部进行专有优化的迭代器进行迭代,在保证线程安全的前提下,尽量加快迭代速度。

HashMap 经典容器

HashMap是我们在开发中最为钟爱的Map类,使用时简单方便,且性能极高。其内部实现由自己定制化的高效hash算法,jdk8更是对HashMap做出了很多的优化。在使用时HashMap允许null作为其键或值,增加了API的灵活性,但是HashMap并不是线程安全的,请注意不要在并发使用此类。HashMap的内部基本存储方式是大致可以看成是哈希表,当HashMap中的某个哈希桶内部的链表存储的元素达到一定数目时,随着链表的不断拓展,查找元素的算法的时间复杂度将上升到O(n)。为了能够保证HashMap的效率,当链表的程度大于阈值(默认为8)时,这个哈希桶就会被转化成效率更高红黑树。

HashMap的属性代码:

评论和共享

JVM虚拟机-垃圾收集器

发布在 jvm

摘要

垃圾收集器(GC,Garbage Collector):自动化的内存管理工具,垃圾收集器主要做以下几种工作:

  • 在新生代中给新对象分配内存,并将老对象放到老年代中去管理。(Allocating objects to a young generation and promoting aged objects into an old generation.)
  • Java HotSpot VM在Java堆内存占用率超过默认阈值时触发标记阶段,标记阶段将通过并行标记的方式在老年代中寻找存活的对象。(Finding live objects in the old generation through a concurrent (parallel) marking phase. The Java HotSpot VM triggers the marking phase when the total Java heap occupancy exceeds the default threshold.)
  • 通过并行复制的算法压缩存活的对象从而恢复可用内存。(Recovering free memory by compacting live objects through parallel copying.)

    如何选择垃圾收集器才能使应用的性能达到最优?HotSpot团队都难以对这个问题给出完美的答案。一般而言,暂停应用程序进行垃圾回收的频率越低且每次垃圾回收的时间越短,应用程序的性能就会越高。但是拥有许多大数据对象的多线程高事务率的大型应用的垃圾回收很难同时满足以上两点。

    阿姆达定理(系统中对某一部件采用更快执行方式所能获得的系统性能改进程度,取决于这种执行方式被使用的频率,或所占总执行时间的比例。)说明,给定问题的并行加速度由问题的顺序执行部分限制其性能。其意为者部分工作并不能完美的并行化,只能串行的执行。在JAVA平台也受此定理的影响,JAVA SE 1.4之前并不支持并行垃圾回收,故垃圾收集器在多处理器下对性能的影响随程序的并行化而增长。

引言

小至简单的 public static void main 程序,大至拥有数百台服务器集群的大型WEB服务,全世界目前有几十亿部设备正在使用JAVA。为了支持这种多样化的部署,Java HotSpot虚拟机(Java HotSpot VM)提供了多个垃圾收集器,每个垃圾回收器被设计为满足不同的需求。JAVA应用程序可以根据自身运行的类型(调试版或生产版)与载体物理机的配置情况,自动的去选择合适的垃圾收集器。但当需求上需要我们写出更加高性能的应用程序时,JAVA的默认配置就很难再满足我们的需求。此时便需要作为开发或运维人员的你通过设置一些JVM参数,明确的指定垃圾收集器等方式,来达到性能需求上所指明的条件。

在阅读本文之前,我希望你已经读过我的上篇文章JVM虚拟机-垃圾回收算法,对常见的垃圾回收算法及其在HotSpot虚拟机下的实现有了解,如果说垃圾回收算法是理论基础,那么本文所介绍的垃圾收集器就是JVM垃圾收集的具体实现。另外希望你在工作或研究的过程中感受过JAVA默认的垃圾回收机制给你带来过性能上的压力,因为只有亲身的经历过,才能感受到了解垃圾收集器的机制的重要性。

本文会以JAVA8版本的HotSpot虚拟机为基础环境逐个的介绍每个垃圾收集器的实现原理并分析其优缺点,简略的探讨一下什么样的应用在什么样的环境下应该选用哪种垃圾收集器。本文的目标是能够帮助读者对应用程序进行JVM调优,做出高性能的应用。

本文着重对CMS和G1两款垃圾收集器进行介绍分析。

基础

HotSpot虚拟机提供了三种大类的垃圾收集器,每种垃圾收集器都有不同的特征和性能表现。简单描述如下:

  • 串行类垃圾收集器( serial collector):
    串行类垃圾收集器是最受争议的垃圾收集器,串行垃圾收集器使用单个线程完成所有的垃圾收集工作。其工作时不需要与其它的线程进行交互,所以垃圾回收的效率是很高的。串行类垃圾收集器尤其适合单CPU的硬件平台上使用,且在使用数据集小于100MB的多CPU硬件平台上也能取得很好的效果。HotSpot在Client模式下默认使用串行垃圾收集器,在其它环境下也可以通过使用 -XX:+UseSerialGC 参数来明确指定使用串行垃圾收集器。

  • 并行垃圾收集器(Parallel Collector):
    并行垃圾收集器又被称为吞吐量优先垃圾收集器(Throughput Collector),通常我们所指的吞吐量优先垃圾收集器指的都是适用于新生代的 Parallel Scavenge 垃圾收集器。并行垃圾收集器能够显著的减少多CPU环境下中型或大型应用垃圾收集所消耗的时间,HotSpot Server版本默认使用的使用的垃圾收集器就是并行垃圾收集器。通过启动虚拟机的时候指定参数 -XX:+UseParallelGC 可以手动指定年轻代使用吞吐量优先收集器。与年轻代相呼应的还有老年代并行垃圾收集器(Oracle 称其为 Parallel compaction 意为并行压缩),当使用参数 -XX:+UseParallelGC 时,JVM老年代的默认开启 Parallel compaction ,同时也可以通过参数 -XX:-UseParallelOldGC 来手动开启。

    • 并发垃圾收集器(Concurrent Collector):
      从字面上理解,并发垃圾收集器的意思是让用户线程和垃圾回收线程并发运行。其在大中型应用系统中并发垃圾收集器工作的过程中仅仅有很短的 ‘Stop the world’ ,且相对于并行垃圾收集器能够大幅度的减小系统响应时间(可能以牺牲系统吞吐量为代价)。JAVA8版本的HotSpot虚拟机下目前有两款并发收集器,CMS垃圾收集器(通过参数 XX:+UseConcMarkSweepGC 开启)和G1(通过参数 -XX:+UseG1GC 开启)。

探索

引用

本文是对JVM垃圾收集器的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java® Virtual Machine Specification · Java SE 8 Edition》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》
  3. 《Java Platform,Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到
weiwei02@vip.qq.com 我的github:
https://github.com/weiwei02/ 我相信技术能够改变世界 。

评论和共享

JVM虚拟机-垃圾回收算法

发布在 java, jvm

引言

C和C++ 程序员能够直接操作内存,凭自己的需要来决定何时去申请多大内存,何时去释放这块内存。他们甚至可以使用指针,来确定的去操作某块内存地址。作为后来者的JAVA远远不如他们强大,我认识的JAVA程序员可能有半数都从未去关注过内存。在使用JAVA进行业务开发时,我们不需要去关注对象到底存储在内存的哪个位置,也不需要关注这个对象到底占用了多大的内存空间,更不需要特意的为某个对象去释放内存空间。需要我们关注的,只有如何完成产品需求,在规定时间内交付合格的产品给客户。如果你对编程的看法跟上面所述的类似,或者你很认同“完成比完美更重要”这条工程师信条,那么这篇文章就不太适合你来阅读了。

内存控制给编程界造成了一个具有魔性的圈,圈内的人想出去,圈外的人想进来。就像C程序员早就受够了内存的申请与释放的折磨,受够了各种内存错误。而很多像你一样的JAVA程序员,立志要写出高性能应用的人,一直在用心地思索怎么才能写出性能更好的多线程应用,当这个追求达到一定层次的时候,你甚至已经开始在乎JAVA垃圾回收所消耗的时间了。由于JAVA自身的限制,我们没办法进入到内存控制的圈内,但我们可以通过设置JVM参数等方式,选择适当的垃圾收集器或设置垃圾回收线程对JAVA的垃圾回收机制进行优化。

概述

本文的重点是介绍JVM虚拟机下常用的垃圾回收算法的理论知识,并不介绍具体算法实现代码。在阅读本文之前,希望读者已经掌握了JVM的内存划分设计、对象引用的可达性分析算法与JAVA的四种对象引用级别(Strong Reference/Soft Reference/Weak Reference/Phantom Reference)等相关知识。在本文之后,我还会再去整理一些关于垃圾收集器等方面的知识。文中所涉及的代码或理论都已在JDK8中进行验证。

垃圾回收算法

标记-清除算法(Mark-Sweep)

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除算法的工作过程如图1所示。

图1 标记-清除算法示意图

  • 优点: 算法简单清晰,其它垃圾收集算法都是据此算法的思想并对其不足改进而得到的。
  • 缺点: 1.效率低,标记和清除需要遍历内存,效率极低。2.回收内存之后产生大量的内存碎片,当内存碎片过多时,应用需要给大对象分配内存时无法分配连续内存。

复制算法(Copying)

将内存按容量划分为完全等大小的两块,每次只使用其中一块内存。当这一块内存用完后,将还活着的对象全部复制到另一块上面,然后把这一块上已使用的内存空间全部清理掉。其示意图如图二所示。


图2 复制算法示意图

  • 优点: 实现简单,运行高效。每次都是对整个半区进行内存回收,内存分配时无需考虑内存碎片等复杂情况。
  • 缺点: 浪费内存空间。在最坏的情况下,这种垃圾回收算法可能浪费了一半的内存空间。

    在JAVA应用中,90%之上的对象的生存时间都极短,所以JVM把内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中一块Survior空间。当需要进行垃圾回收时,将Eden和Survivor中还处于可达状态的对象一次性的复制到另一块Survivor空间中,最后清空Eden和刚刚使用的Survivor空间。当Survivor空间不够用时,还会将长时间存活的对象转存的老年代中。在HotSpot虚拟机中,默认的Eden和Survivor空间的比例是8:1,这样可以做到只浪费10%的内存。

标记-整理算法(Mark-Compact)

标记-整理算法的标记过程与标记-清除算法的标记过程类似,但在标记完成后并不是直接对可回收的对象进行清理,而是让所有正在存活的对象都向前端移动,然后直接清理掉边界以外的内存。其示意图见图3.


图3 标记-整理算法示意图

  • 优点: 节省内存空间,提升内存运用率,且不会产生内存碎片。
  • 缺点: 性能低。

分代收集算法(Generational Collection)

根据对象的存活时间把内存分为新生代和老年代,根据个代对象的存活特点,每个代采用不同的垃圾回收算法。

分代收集就是根据不同代的特性,使用最合适的垃圾回收算法进行垃圾回收。如新生代中,每次垃圾收集都会有大量的对象死去,只有极小部分对象存活,所以更适合复制算法。老年代中对象存活率高,且没有额外的内存空间为它进行分配担保,所以更适合用标记-清除或标记-整理算法。

HotSpot算法实现

枚举根节点(GC Roots)

在垃圾回收时,我们要想办法找出哪些对象是存活的,一般会选取一些被称为GC Root的对象,从这些对象开始枚举。在进行GC Root枚举时要求所有对象停下来,也就是JVM所称的“Stop the world”。所有的算法实现都会将虚拟机停下来的,否则分析结果的准确性将无法保证。

由于HotSpot采用准确式GC,该技术主要功能就是让虚拟机可以准确的知道内存中某个位置的数据类型是什么,比如某个内存位置到底是一个整型的变量,还是对某个对象的 reference。这样在进行 GC Roots 枚举时,只需要枚举 reference 类型的即可。在能够准确地确定 Java 堆和方法区等 reference 准确位置之后,HotSpot 就能极大地缩短 GC Roots 枚举时间,所以当执行系统停顿下来之后,虚拟机不需要遍历所有的根节点和上下文去确定GC Roots,而是存在着一个OopMap的数据结构来达到这个目的。

在类加载完成的时候,虚拟机就会把什么类的什么偏移上是什么类型的数据计算出来。在JIT编译的时候也会在特定位置记下在寄存器和栈中哪些位置是引用,GC在扫描时就可直接得到信息。

安全点(Safepoint)

Safepoint:会导致 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那么将需要大量的额外空间,这样对导致 GC 成本很高,所以 HotSpot 只在 “特定位置” 记录这些信息,这些位置被称为 安全点(Safepoint)。并非程序在任意时刻都可以停顿下来进行 GC,而只有程序到达 安全点(Safepoint) 以后才可以停顿下来进行 GC;所以安全点既不能太少,以至于 GC 过程等待程序到达安全点的时间过长,也不能太多,以至于 GC 过程带来的成本过高。

由于在 GC 过程中必须保证程序已执行,那么也就是说 必须等待所有线程都到达安全点上方可进行 GC。一般来说有两种解决方案可以选择:

  • 抢先式中断:不需要线程的执行代码去主动配合,当发生 GC 时,先强制中断所有线程,然后如果发现某些线程未处于安全点,那么将其唤醒,直至其到达安全点再次将其中断;这样一直等待所有线程都在安全点后开始 GC。现在几乎没有虚拟机使用这种方式。

  • 主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时轮询这个标记,一旦发现标记被改变(出现中断标记)时,那么将运行到安全点后自己中断挂起;目前所有商用虚拟机全部采用主动式中断。

安全区(Safe Region)

安全点机制仅仅是保证了程序执行时不需要太长时间就可以进入一个安全点进行 GC 动作,但是当特殊情况时,比如线程休眠、线程阻塞等状态的情况下,显然 JVM 不可能一直等待被阻塞或休眠的线程正常唤醒执行,此时就引入了安全区的概念。

安全区(Safe Region):安全区域是指在一段代码区域内,对象引用关系等不会发生变化,在此区域内任意位置开始 GC 都是安全的。线程运行到Safe Region中的代码时,首先标记自己进入了安全区,然后在这段区域内,如果线程发生了阻塞、休眠等操作,JVM 发起 GC 时将忽略这些处于安全区的线程。当线程再次被唤醒时,首先他会检查是否完成了 GC Roots枚举(或这个GC过程),然后选择是否继续执行,否则将继续等待 GC 的完成。

引用

本文是对JVM垃圾收集算法的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java® Virtual Machine Specification · Java SE 8 Edition》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》
  3. 《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》

    非原创声明

    本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到 weiwei02@vip.qq.com
我的github: https://github.com/weiwei02/
我相信技术能够改变世界。

评论和共享

class文件介绍

发布在 java, jvm

非原创声明

本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

*.class文件介绍

一般来讲.class文件是.java文件在编译器编译后生成的jvm能够运行的文件,*.class文件又常被称为字节码文件。java在创始之初,就提倡“一次编写,处处运行的概念”,在当今编程圈中这个概念早已不是什么特例。java通过将开发人员所编写的java代码编译成class文件,然后由jvm虚拟机在执行时将不分平台的class文件中的字节码,再翻译成机器码,交给硬件执行。java就是靠jvm虚拟机的这个设计来实现与平台无关的特性的。class文件不但与硬件平台和操作系统无关,也和具体的编程语言无关,就目前来说,如函数式编程语言scala与Groovy都可以通过自己的编译器将源代码编译成class文件,在jvm上运行。
综合来讲,class文件有以下两点特性:

  • 与硬件和操作系统平台无关
  • 与源码所使用的编程语言无关

class类文件的结构

每一个class文件都唯一对应着java类或接口枚举等定义信息,但类不一定都定义在class文件中,类可能是由类加载器动态生成的。

class文件所以被称之为字节码据我猜测可能是因为class文件以8位(1字节)为单位进行存储的二进制信息。字节码中各个数据项目严格按照顺序紧凑的排列,中间没有任何分割符。在需要存储整型或浮点型这些大于8位的数据项目时,则会使用Big-Endian的字节序进行存储,将最高位字节放在地址最低位,最低位字节放在地址最高位。

class文件格式采用表来存储数据,表中有无符号数和表两种数据类型。对于这两种数据类型说明如下:

  • 无符号数: 无符号数是基本的数据类型,可以用来表示数字、索引引用、数量值或者按照UTF-8编码构成的字符串。如u1,u2,u4,u8分别代表1,2,4,8个字节字节的无符号数。
  • : 表是由多个无符号数或者其它表作为数据项所组成的复合数据结构,表用于描述层次关系和复合的数据结构,整个class文件就是一张表。通常表以 _info 结尾。如表1就是一个class的表示例。
类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 asscess_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods method_count
u2 attributes_count 1
attribute_info attributes attributes_count

表 1 class文件格式

class文件的顺序和格式必须严格实现按照上述规则,否则jvm将不能识别执行。

Magic Number 魔数

魔数是一个和文件后缀名相似的用于文件格式识别的约定,一般规定文件内容开头的前几个字节为文件的魔数。不同于文件后缀名很容易被用户以重命名的方式进行更改,文件的魔数作为识别手段可以更安全的确定文件的可用性。

class文件使用前4个字节作为魔数,来确定.class文件是否是一个能够被虚拟机识别的文件,其值是 0xCAFFEBABE 。

版本

class文件的第5-6个字节代表的可执行该class文件的目标虚拟机的最低次版本号(Minor Version),第7-8个字节是主版本号(Major Version)。java虚拟机可以运行比当前虚拟机版本号低的class文件,拒绝运行版本号不合法,或比自己版本高的class文件。JDK1.1的版本号是45,之后的每个大版本发布都把主版本号加1,如JDK1.2主版本号是46,JDK8的版本号是52。
JDK在编译java文件是可以通过 javac -target 1.6 ...命令来指定编译后的class文件可以在1.6的虚拟机版本上运行。

常量池

常量池是class文件中第一个表类型的数据项目,常量池是class文件中的资源仓库,是class文件结构中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一。

class文件里紧随版本之后的数据项是常量池,由于常量池的数量是不固定的,所以在常量池的数据项之前放置的有一个u2类型的数据,代表常量池的大小。常量池大小的初始值是1,如常量池的大小的数据如果显示的是10,就代表该class文件中有9个常量。
常量池中主要存放两大类常量:Literal(字面常量)、Symbolic Reference(符号引用常量)。字面常量类似于java中常量的概念,如文本字符串、final关键字所声明的常量等。而符号引用常量则是编译中的概念,主要包括以下三种类型的常量。

  • 符号常量

    • 类和接口的全限定名(Fully Qualified Name)
    • 字段名称和描述符(Descriptor)
    • 方法的名称和描述符

    class文件中不会保存各个方法或字段在内存中的布局信息,而是在虚拟机加载class文件时进行动态的连接。虚拟机在运行class文件时从常量池中获取对应的符号引用,再在创建类或者运行时解析连接到具体的内存地址当中。
    常量池中的每一个常量都是一个表,在JDK8中有14种表结构的常量表。如表 2 所示。

类型 标识 描述 
CONSTANT_utf8_info 1  UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

表 2 常量池数据项目类型表

这14种常量结构表开始的第一个字节都是一个u1类型的标识位,其值就是表2中每项常量表类型所对应的标识列的值,代表当前常量属于哪种常量类型。这14种常量类型各自有自己不同的表结构,详情如表 3 所示。

常量 项目 类型 描述
CONSTANT_Utf8_info tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes u1 utf-8编码的字符串
CONSTANT_Integer_info tag u1 值为3
bytes u4 按照Big-Endian存储的int值
CONSTANT_Float_info tag u1 4
bytes u4 按照Big-Endian存储的float值
CONSTANT_Long_info tag u1 5
bytes u8 按照Big-Endian存储的long值
CONSTANT_Double_info tag u1 6
bytes u8 按照Big-Endian存储的long值double值
CONSTANT_Class_info tag u1 7
index u2 指向全限定名常量项的索引
CONSTANT_String_info tag u1 8
index u2 指向字符串常量的索引
CONSTANT_Fieldref_info tag u1 9
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_Methodref_info tag u1 10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_InterfaceMethodref_info tag u1 11
index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引值
index u2 指向CONSTANT_NameAndType_info的索引值
CONSTANT_NameAndType_info tag u1 12
index u2 指向该字段或方法名称常量的索引值
index u2 指向该字段或方法描述符常量的索引值
CONSTANT_MethodHandle_info tag u1 15
reference_kind u1 值必须1~9,它决定了方法句柄的的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 对常量池的有效索引
CONSTANT_MethodType_info tag u1 16
description_index u2 对常量池中方法描述符的有效索引常量池在该处的索引必须是CONSTANT_Utf8_info的结构,表示方法的描述符。
CONSTANT_InvokeDynamic_info tag u1 18
bootstap_method_attr_index u2 对当前class文件中引导方法表的bootstap_methods[]数组的有效索引
name_and_type_index u2 对当前常量池的有效索引,常量池在此处必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述。

表 3 常量池中14中常量的结构总表

在表 3 中可以看到class文件所支持的所有的常量的结构信息,另外由于class中的类名、方法、字段都要用CONSTANT_Utf8_info型的常量来描述名称,所以CONSTANT_Utf8_info的最大长度也是java中类名、方法名或字段的最大长度65535,如果超出这个最大长度,便会无法编译。

访问标识

在class文件中位于常量池之后的是访问权限标识。class文件的访问标识是用于修饰这个class文件所代表的的类或接口本身的一些属性。访问标识是用一个u2类型的数据来代表的,如0x0001代表public,0x0020代表这是一个接口。具体的访问标识和标志值的对应关系见表4.访问标识是按位进行标识的,一个u2类型的数据有16位,目前其中的8位都被赋予了实际意义。

标识名称 标识值 意义
ACC_PUBLIC 0X0001 public的访问权限
ACC_FINAL 0x0010 如果该class文件代表一个类,那么该类是final修饰的(只能类才能拥有此标识)
ACC_SUPER 0x0020 使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 接口
ACC_ABSTRACT 0x0400 abstract(只有接口和抽象类有效)
ACC_SYNTHETIC 0x10000 本类不是用户代码所产生的类
ACC_ANNOTATION 0x2000 注解
ACC_ENUM 0x4000 枚举

表4 访问标识和标志值的对应关系表

类索引

类索引是一个u2类型的数据,用于确定这个类的全限定名。其索引值指向一个类型为CONSTANT_Class_info的常量。

父类索引

父类索引指向着当前类所继承的类的父类的全限定名,其类型信息与类索引相同。所有java类都有父类索引。

接口索引

java不支持多重继承,所以只有一个父类索引。但一个java类或接口可以实现多个其它接口,所以class的接口索引是一个数量并不固定的集合。
在声明接口索引之前,class文件首先会声明一个u2类型的数据代表接口索引的总数量,其后面紧跟者就是接口索引集合,如果该类没有实现任何接口,那么接口索引的总数量的值就是0。接口索引同样也指向常量池中的CONSTANT_Class_info类型的常量,使用CONSTANT_Utf8_info的字符串常量来表示接口的全限定名。

字段表集合

20170701 00:10:00 编辑到字段表集合

字段集合用于描述接口或类中所声明的全局变量。如public,static、final等词都是用来描述某个字段的修饰词。因为java中可用来修饰字段的修饰符有限,所以class文件对于字段的修饰符的表示是通过使用标识位来实现的。表5详细分析了字段表的数据结构。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attrobute_info attributes attributes_count

表5 字段表的数据结构

access_flags标识位中存放的是字段的修饰符,使用标志位进行标识区分。具体区分方式如表6字段访问标识表所示。

标识名称 标识值 含义
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_FINAL 0x0010 final
ACC_VOLATILE 0x0040 volatile
ACC_TRANSIENT 0x0080 transient
ACC_SYNTHETIC 0x1000 该字段是否由编译器自动生成的
ACC_ENUM 0x4000 字段是否是enum

表6 字段访问标识对应表

name_index 和 descriptor_index 都是对常量池索引的引用,name_index引用的是字段的名称,descriptor_index引用的是字段和方法的描述符。

20170801 00:10:00 编辑到字段表集合

引用

本文是对class文件的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java Virtual Machine Specification》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》

关于

本项目和文档中所用的内容仅供学习和研究之用,转载或引用时请指明出处。如果你对文档有疑问或问题,请在项目中给我留言或发email到 weiwei02@vip.qq.com

from weiwei.wang 20170625

评论和共享

java内存模型

发布在 java, jvm

非原创声明

本文并非我的原创文章,而是我学习jvm时的笔记。文中的材料与数据大部分来自于其它资料,详细请查看本文的引用章节。

JAVA内存模型

犹记得大学时操作系统课上,我们迷茫的眼神注视着带着厚眼镜教授向我们一遍遍的强调,一个程序最少有一个进程组成,进程是操作系统提供独立资源供应用程序运行的基本单位。另外老师向我们讲到,为了更好的提高计算机的并行计算能力,计算机科学家们又设计了线程。线程是比进程更小的单位,一个进程可以由多个线程组成。同时线程也是在得到cpu时间片时可运行的最小的单位。尤记得在这些理论基础下,我慢慢的学会了使用C在LINUX环境下使用多进程和多线程进行编程。这些并发API都是LINUX提供的标准API,程序在编译链接之后可以直接调用通过系统内核创建进程或线程,在类UNIX操作系统下这样去做可以让程序有更高的性能。但是这种编程方式所编写出来的代码是与操作系统绑定的,我在linux下明明可以完美运行的代码,在windows下连编译甚至都做不到。除非是用 windows API重新把与操作系统进行交互的那些代码给替换掉,否则就不要想着让程序去跨平台运行了。
在进行并发编程时相对于C,我更喜欢java的编程体验。java消除操作系统之间的差异,原生的对多线程应用提供了很好的支持,特别是在jdk1.5之后,jdk还提供了currency包,更好的让程序员们无需去关注并发的难点与细节,更专心的关注应用的业务需求实现。
在多线程编程中最长遇到的问题就是线程安全问题,其中又以内存中的数据安全问题最为常见(这个数据安全指的是发生脏读、幻读等并发编程中会遇到的错误)。为了消除不同硬件和操作系统对内存操作的差异,在硬件设备和操作系统的内存模型之上,java虚拟机规范定义了一种java内存模型(JAVA Memory Model,简称JMM),将内存分为了工作内存和主内存。JMM主要定义了JVM中在内存中操作变量的规则和细节,用来解决在并发竞争对变量操作时可能会发生的各种问题。JMM完全兼容CPU的多级cache机制,并且支持编译器的代码重排序。
注意:JMM内存模型的概念不同于jvm中6大内存区域的概念,两者不可强行混为一谈。

主内存和工作内存

JMM规定了所有的全局变量都存于主内存(Main Memory)中,一般情况下这些全局变量都会被保存到堆里。每个线程都有自己的工作内存(Working Memory),工作内存中会保存当前线程所需用到的主内存中全局变量的拷贝和自己的局部变量。一般情况下工作内存指的是栈内存,从物理上来讲,工作内存一般情况下都会工作于cpu的cache里。一个线程对全局变量的任何操作必须在自己的工作内存中进行,不允许直接操作工作内存。不同线程不能访问对方的工作内容,只能通过主内存进行数据交换。

工作内存在对主内存中变量做拷贝时,如果变量是基本类型的,则会拷贝其值;如果是引用类型的,那么仅仅会拷贝其引用

JMM内存操作

JMM定义了8种主内存与工作内存之间的具体交互操作,虚拟机在实现JMM时必须保证每种内存操作都是原子的。表1详细列出了JMM的8种内存操作。

指令 操作名 作用区域 描述
lock 锁定 主内存 把一个变量标识为一个线程独占的状态
unlock 解锁 主内存 把一个处于锁定状态的变量释放出来
read 读取 主内存 把一个变量的值从主内存读取到工作内存中,以便于随后的load指令使用
load 载入 工作内存 把read指令从主内存中读取的变量值放入到工作内存的变量副本中
use 使用 工作内存 将工作内存中一个变量的值传递给执行引擎
assign 赋值 工作内存 把一个从执行引擎接收到的值赋给工作内存的变量
store 存储 工作内存 把一个工作内存中变量的值传送到主内存中,以便于随后的write指令使用
write 写入 主内存 把store指令从工作内存传出的变量的值写入到主内存的变量中

表1 JMM内存操作指令表

20170702 01:48 编辑到JMM内存操作指令表

JMM规定了在使用以上8种内存操作时必须遵守以下规则:

  • read,load或store,write必须成对出现。如:执行read操作从主内存读取一个变量后,必须在工作内存使用load载入这个变量。两者之间的顺序不可错,但两者之间可以穿插其它指令。同理,在工作内存中对一个变量使用了store指令,必须在主内存中使用write指令进行写入。
  • 不允许一个线程丢弃它最近做的assign操作。变量在工作内存中改变了之后必须将这个变化同步到主内存中。
  • 不允许一个线程没有发生过assign操作就将数据从工作内存同步到主内存。
  • 只能在主内存中新建全局变量,不能在工作内存中直接使用一个未被初始化的变量。可以使用assign和load指令在工作内存对一个变量进行初始化操作。
  • 一个变量在同一时刻只能被一个线程执行lock操作。
  • 如果对一个变量执行lock,那将清空工作内存中此变量的值。在执行引擎使用这个变量之前,需要重新load或assign重新初始化变量的值。
  • unlock只能解锁被本线程锁定的变量。
  • 对一个变量执行unlock之前,必须将此变量同步回主内存。

volatile 关键字

volatile是java语言所提供的关键字,使java最轻量级的内存同步的措施。与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

java中锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。
volatile 变量具有 synchronized 的可见性特性,但是不具备锁的原子特性。所以即便volatile没有不一致的问题,但volatile变量在并发的运算下并不是原子操作,所以依然可能会有安全问题。

package com.github.weiwei02.jvm.jmm.volatile_test;

/**
* volatile 线程安全性测试
* @author Wang Weiwei <email>weiwei02@vip.qq.com / weiwei.wang@100credit.com</email>
* @version 1.0
* @sine 2017/7/2
*/
public class VolatileTest {
public static volatile int race = 0;
public static final int THREAD_COUNT=20;

public static void main(String[] args) {
    Thread threads[] = new Thread[THREAD_COUNT];
    System.out.println(race);
    for (int i =0; i < THREAD_COUNT; i++){
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 1000 ; j++) {
                increase();
            }
        });
        threads[i].start();
    }


    //等待所有累加线程都结束
    while (Thread.activeCount() > 1){
        Thread.yield();
    }

    //等待所有线程执行完毕之后,打印race的最终值
    System.out.println(race);
}

private static void increase() {
    race++;
}
}

代码1 volatile 线程安全测试

程序的执行结果如图2所示。
volatile 线程安全测试
图2 volatile 线程安全测试

这个程序执行的结果应该是20000,可实际执行所得到的结果只有18439.并且多次执行本程序都会得到不一样的结果。所以会出现这种情况的原因是 race++ 这个操作并不是原子操作,volatile关键字只能保证在使用race变量时,工作线程能够从主内存中拿取最新的race的值,并不能保证进行过++操作之后写回到主内存期间其它线程没有对race变量进行修改。
volatile关键字只能保证可见性,只有在以下两种条件下,我们可以认为volatile是线程安全的;

  1. 运算结果并不依赖变量的当前值,或者能够保证只有一个线程修改变量的值。
  2. 变量不需要与其它的变脸共同参与不变约束。

    当不符合这两种条件时,想要保证volatile变量的原子性,只能通过 机制来实现。

    volatile变量的第二个重要作用是禁止编译器进行指令重排序。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。从硬件层面上来讲,是指CPU允许多条指令不按程序规定的顺序分开发送到个相应的电路单元来处理。CPU必须要正确的处理指令的依赖情况,不能将指令任意的排序。比如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3将地址B中的值减去3。在这一系列指令中,指令1,2之间是有依赖关系的,不能进行重排序。但指令3的执行顺序可以改到指令1之前也可以放到指令1之后,只要能够保证在执行完毕这三条指令程序中A,B的值是正确的即可。volatile在某些情况下性能高于锁,由于虚拟机对锁实行有多种消除和优化,我们很难衡量volatile会比锁快多少。

原子性、可见性和有序性

20170803 23:11:51 原子性、可见性和有序性
JAVA内存模型的核心问题时为了解决多线程应用中的原子性、可见性与有序性的问题,保证了这三点才能使多线程应用有数据安全性可言。

  • 原子性 (Atomicity)
    由java内存模型来直接保证原子性的操作包括read、load、assign、use、store和write。对于一个变量的基本读写操作都是具有原子性的。如果需要对多种这几种基本读写操作的操作集合保证原子性,可以使用lock和unlock。
  • 可见性(Visibility)
    可见性是指当变量的值被一个线程修改了之后,其它线程能够立即知道这个修改。JAVA内存模型时通过变量内容修改后立刻同步到主内存,读取变量的值之前先从内存刷新这种方式来保证可见性。JAVA中除了volatile之外还有synchronized、final两个关键字能够实现内存可见性。synchronized的可见性是由”对一个变量执行unlock操作之前,必须先把此变量同步会主内存中(执行store,write操作)”这条规则获得的。final的可见性是由于被final修饰的变量在构造器中一旦被初始化完成,并且构造器没有吧this的引用传递出去(this引用是一件非常危险的事,其它线程可能通过这个引用访问到初始化一半的对象),,那在其它线程中就能看见final变量的值。
  • 有序性
    如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句指的是“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句指的是“指令重排序”现象和“工作内存与主内存同步延迟”现象。

先行发生原则

20170804 02:02:23 先行发生原则

先行发生原则(Happens-before)是JAVA内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,操作A造成的影响就能被操作B观察到。JAVA只能怪的先行发生原则无需任何同步协助,可以直接编码使用。以下列出JAVA中的先行发生原则,如果两个操作之间的关系不在下面的规则中,且无法通过下面的规则推导出来,虚拟机就能随意的对他们进行指令重排序。

  • 程序次序规则(Program Order Rule)
    :在一个线程内,按照程序的代码顺序,书写在前的操作先于书写在后面的操作发生。准确的说,应该是控制流顺序而不是程序代码顺序,因为需要考虑分支、循环等结构。

  • 管程锁定规则(Monitor Lock Rule)
    : 一个unlocak操作先行发生于后面对同一个锁的lock操作。

  • volatile变量规则(Volatile Variable Rule)
    :对一个volatile的写操作,先行发生于后面对这个变量的读操作。

  • 线程启动规则(Thread Start Rule)
    :线程的start方法先行发生于此线程的每一个动作。

  • 线程终止规则(Thread Termination Rule)
    : 线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.jion()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 线程中断原则(Thread Interruption Rule)
    :对线程的interrupt()方法的调用先行发生于被中断的线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则(Finalizer Rule)
    :一个对象的初始化完成(够构造函数执行结束)先行发生于它的fnalize()方法开始。

  • 传递性(Transitivity)
    :如果A操作先行发生于B操作,B操作先行发生于C操作,就可以得出A先行发生于C的结论。

    时间先后顺序与先行发生原则之间基本没有关系,所以当我们需要衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

    20170804 23:59:46 先行发生原则

引用

本文是对class文件的学习笔记,笔记的内容并非是原创,而是大量参考其它资料。在写作本文的过程中引用了以下资料,为为在此深深谢过以下资料的作者。

  1. 《The Java Virtual Machine Specification》
  2. 《深入理解Java虚拟机:JVM高级特性与最佳实践/周志明著.——2版.——北京:机械工业出版社,2013.6》

关于

原文链接 https://weiwei02.github.io/2017/07/02/jvm/001-java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/
我的github: https://github.com/weiwei02/
我相信技术能够改变世界。

评论和共享

  • 第 1 页 共 1 页
Copyrights © 2017 weiwei02. All Rights Reserved. github空间地址: https://weiwei02.github.io/ 国内空间地址: https://weiwei02.cording.me/
作者的图片

weiwei02

技术,改变世界


软件工程师


北京,海淀
国外主站 国内主站