精华内容
下载资源
问答
  • Git内部原理之深入解析Git的引用和包文件
    千次阅读
    2021-09-27 13:34:26

    一、Git 分支本质

    • 如果对仓库中从一个提交(比如 1a410e)开始往前的历史感兴趣,那么可以运行 git log 1a410e 这样的命令来显示历史,不过需要记得 1a410e 是查看历史的起点提交。如果我们有一个文件来保存 SHA-1 值,而该文件有一个简单的名字, 然后用这个名字指针来替代原始的 SHA-1 值的话会更加简单。
    • 在 Git 中,这种简单的名字被称为“引用(references,或简写为 refs)”,可以在 .git/refs 目录下找到这类含有 SHA-1 值的文件。在目前的项目中,这个目录没有包含任何文件,但它包含了一个简单的目录结构:
    $ find .git/refs
    .git/refs
    .git/refs/heads
    .git/refs/tags
    $ find .git/refs -type f
    
    • 若要创建一个新引用来帮助记忆最新提交所在的位置,从技术上讲只需简单地做如下操作:
    $ echo 1a410efbd13591db07496601ebc7a059dd55cfe9 > .git/refs/heads/master
    
    • 现在,就可以在 Git 命令中使用这个刚创建的新引用来代替 SHA-1 值:
    $ git log --pretty=oneline master
    1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
    cac0cab538b970a37ea1e769cbbde608743bc96d second commit
    fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
    
    • 但是,不提倡直接编辑引用文件,如果想更新某个引用,Git 提供了一个更加安全的命令 update-ref 来完成此事:
    $ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9
    
    • 这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用,若想在第二个提交上创建一个分支,可以这么做:
    $ git update-ref refs/heads/test cac0ca
    
    • 这个分支将只包含从第二个提交开始往前追溯的记录:
    $ git log --pretty=oneline test
    cac0cab538b970a37ea1e769cbbde608743bc96d second commit
    fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
    
    • 至此,我们的 Git 数据库从概念上看起来像这样:

    包含分支引用的 Git 目录对象

    • 当运行类似于 git branch 这样的命令时,Git 实际上会运行 update-ref 命令,取得当前所在分支最新提交对应的 SHA-1 值,并将其加入想要创建的任何新引用中。

    二、HEAD 引用

    • 现在的问题是,当执行 git branch 时,Git 如何知道最新提交的 SHA-1 值呢? 答案是 HEAD 文件。
    • HEAD 文件通常是一个符号引用(symbolic reference),指向目前所在的分支。所谓符号引用,表示它是一个指向其他引用的指针。
    • 然而在某些罕见的情况下,HEAD 文件可能会包含一个 git 对象的 SHA-1 值。当在检出一个标签、提交或远程分支,让仓库变成 “分离 HEAD”状态时,就会出现这种情况。
    • 如果查看 HEAD 文件的内容,通常会看到类似这样的内容:
    $ cat .git/HEAD
    ref: refs/heads/master
    
    • 如果执行 git checkout test,Git 会像这样更新 HEAD 文件:
    $ cat .git/HEAD
    ref: refs/heads/test
    
    • 当执行 git commit 时,该命令会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置其父提交字段。
    • 也可以手动编辑该文件,然而同样存在一个更安全的命令来完成此事:git symbolic-ref,借助此命令来查看 HEAD 引用对应的值:
    $ git symbolic-ref HEAD
    refs/heads/master
    
    • 同样可以设置 HEAD 引用的值:
    $ git symbolic-ref HEAD refs/heads/test
    $ cat .git/HEAD
    ref: refs/heads/test
    
    • 不能把符号引用设置为一个不符合引用规范的值:
    $ git symbolic-ref HEAD test
    fatal: Refusing to point HEAD outside of refs/
    

    三、标签引用

    • 我们知道, Git 有三种主要的对象类型(数据对象、树对象和提交对象,具体请参考:Git内部原理之深入解析Git对象),然而实际上还有第四种:标签对象(tag object), 它非常类似于一个提交对象,包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象,像是一个永不移动的分支引用,永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。
    • 正如 Git之深入解析本地仓库的基本操作·仓库的获取更新和提交历史的查看撤销以及标签别名的使用 中所讨论的那样,存在两种类型的标签:附注标签和轻量标签,可以像这样创建一个轻量标签:
    $ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d
    
    • 这就是轻量标签的全部内容,一个固定的引用。 然而,一个附注标签则更复杂一些,若要创建一个附注标签,Git 会创建一个标签对象,并记录一个引用来指向该标签对象,而不是直接指向提交对象。可以通过创建一个附注标签来验证这个过程(使用 -a 选项):
    $ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag'
    
    • 下面是上述过程所建标签对象的 SHA-1 值:
    $ cat .git/refs/tags/v1.1
    9585191f37f7b0fb9444f35a9bf50de191beadc2
    
    • 现在对该 SHA-1 值运行 git cat-file -p 命令:
    $ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
    object 1a410efbd13591db07496601ebc7a059dd55cfe9
    type commit
    tag v1.1
    tagger Scott Chacon <schacon@gmail.com> Sat May 23 16:48:58 2009 -0700
    
    test tag
    
    • 不难注意到,object 条目指向打了标签的那个提交对象的 SHA-1 值。另外,标签对象并非必须指向某个提交对象,可以对任意类型的 Git 对象打标签。例如,在 Git 源码中,项目维护者将它们的 GPG 公钥添加为一个数据对象,然后对这个对象打了一个标签,可以克隆一个 Git 版本库,然后通过执行下面的命令来在这个版本库中查看上述公钥:
    $ git cat-file blob junio-gpg-pub
    
    • Linux 内核版本库同样有一个不指向提交对象的标签对象,首个被创建的标签对象所指向的是最初被引入版本库的那份内核源码所对应的树对象。

    四、远程引用

    • 现在将看到的第三种引用类型是远程引用(remote reference),如果添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在 refs/remotes 目录下。例如,可以添加一个叫做 origin 的远程版本库,然后把 master 分支推送上去:
    $ git remote add origin git@github.com:schacon/simplegit-progit.git
    $ git push origin master
    Counting objects: 11, done.
    Compressing objects: 100% (5/5), done.
    Writing objects: 100% (7/7), 716 bytes, done.
    Total 7 (delta 2), reused 4 (delta 1)
    To git@github.com:schacon/simplegit-progit.git
      a11bef0..ca82a6d  master -> master
    
    • 此时,如果查看 refs/remotes/origin/master 文件,可以发现 origin 远程版本库的 master 分支所对应的 SHA-1 值,就是最近一次与服务器通信时本地 master 分支所对应的 SHA-1 值:
    $ cat .git/refs/remotes/origin/master
    ca82a6dff817ec66f44342007202690a93763949
    
    • 远程引用和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于,远程引用是只读的。虽然可以 git checkout 到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用。因此,永远不能通过 commit 命令来更新远程引用,Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。

    五、包文件

    • 如果跟着做完了上文的所有步骤,那么现在应该有了一个测试用的 Git 仓库, 其中包含 11 个对象:四个数据对象,三个树对象,三个提交对象和一个标签对象:
    $ find .git/objects -type f
    .git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
    .git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
    .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
    .git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
    .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
    .git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
    .git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
    .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
    .git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
    .git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
    .git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1
    
    • Git 使用 zlib 压缩这些文件的内容,而且并没有存储太多东西,所以上文中的文件一共只占用了 925 字节。接下来,我们添加一些大文件到仓库中,以此展示 Git 的一个很有趣的功能。为了便于演示,要把之前在 Grit 库中用到过的 repo.rb 文件添加进来,如下所示,这是一个大小约为 22K 的源代码文件:
    $ curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb
    $ git checkout master
    $ git add repo.rb
    $ git commit -m 'added repo.rb'
    [master 484a592] added repo.rb
     3 files changed, 709 insertions(+), 2 deletions(-)
     delete mode 100644 bak/test.txt
     create mode 100644 repo.rb
     rewrite test.txt (100%)
    
    • 如果查看生成的树对象,可以看到 repo.rb 文件对应的数据对象的 SHA-1 值:
    $ git cat-file -p master^{tree}
    100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
    100644 blob 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5      repo.rb
    100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt
    
    • 接下来可以使用 git cat-file 命令查看这个对象有多大:
    $ git cat-file -s 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
    22044
    
    • 现在,稍微修改这个文件,然后看看会发生什么:
    $ echo '# testing' >> repo.rb
    $ git commit -am 'modified repo.rb a bit'
    [master 2431da6] modified repo.rb a bit
     1 file changed, 1 insertion(+)
    
    • 查看这个最新的提交生成的树对象,可以看到一些有趣的东西:
    $ git cat-file -p master^{tree}
    100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
    100644 blob b042a60ef7dff760008df33cee372b945b6e884e      repo.rb
    100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt
    
    • repo.rb 对应一个与之前完全不同的数据对象,这意味着,虽然只是在一个 400 行的文件后面加入一行新内容,Git 也会用一个全新的对象来存储新的文件内容:
    $ git cat-file -s b042a60ef7dff760008df33cee372b945b6e884e
    22054
    
    • 磁盘上现在有两个几乎完全相同、大小均为 22K 的对象(每个都被压缩到大约 7K),如果 Git 只完整保存其中一个,再保存另一个对象与之前版本的差异内容,岂不更好?
    • 事实上 Git 可以那样做,最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率,当版本库中有太多的松散对象,或者手动执行 git gc 命令,或者向远程服务器执行推送时,Git 都会这样做。要看到打包过程,可以手动执行 git gc 命令让 Git 对对象进行打包:
    $ git gc
    Counting objects: 18, done.
    Delta compression using up to 8 threads.
    Compressing objects: 100% (14/14), done.
    Writing objects: 100% (18/18), done.
    Total 18 (delta 3), reused 0 (delta 0)
    
    • 这个时候再查看 objects 目录,会发现大部分的对象都不见了,与此同时出现了一对新文件:
    $ find .git/objects -type f
    .git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
    .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
    .git/objects/info/packs
    .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
    .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack
    
    • 仍保留着的几个对象是未被任何提交记录引用的数据对象,在此例中是之前创建的 “what is up, doc?” 和 “test content” 这两个示例数据对象,因为从没将它们添加至任何提交记录中,所以 Git 认为它们是悬空(dangling)的,不会将它们打包进新生成的包文件中。
    • 剩下的文件是新创建的包文件和一个索引,包文件包含了刚才从文件系统中移除的所有对象的内容,索引文件包含了包文件的偏移信息,我们通过索引文件就可以快速定位任意一个指定对象。有意思的是运行 gc 命令前磁盘上的对象大小约为 15K,而这个新生成的包文件大小仅有 7K,通过打包对象减少了一半的磁盘占用空间。
    • Git 是如何做到这点的呢? Git 打包对象时,会查找命名及大小相近的文件,并只保存文件不同版本之间的差异内容。可以查看包文件,观察它是如何节省空间的,git verify-pack 这个底层命令可以就可以查看已打包的内容:
    
    $ git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
    2431da676938450a4d72e260db3bf7b0f587bbc1 commit 223 155 12
    69bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit 214 152 167
    80d02664cb23ed55b226516648c7ad5d0a3deb90 commit 214 145 319
    43168a18b7613d1281e5560855a83eb8fde3d687 commit 213 146 464
    092917823486a802e94d727c820a9024e14a1fc2 commit 214 146 610
    702470739ce72005e2edff522fde85d52a65df9b commit 165 118 756
    d368d0ac0678cbe6cce505be58126d3526706e54 tag    130 122 874
    fe879577cb8cffcdf25441725141e310dd7d239b tree   136 136 996
    d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree   36 46 1132
    deef2e1b793907545e50a2ea2ddb5ba6c58c4506 tree   136 136 1178
    d982c7cb2c2a972ee391a85da481fc1f9127a01d tree   6 17 1314 1 \
      deef2e1b793907545e50a2ea2ddb5ba6c58c4506
    3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree   8 19 1331 1 \
      deef2e1b793907545e50a2ea2ddb5ba6c58c4506
    0155eb4229851634a0f03eb265b69f5a2d56f341 tree   71 76 1350
    83baae61804e65cc73a7201a7252750c76066a30 blob   10 19 1426
    fa49b077972391ad58037050f2a75f74e3671e92 blob   9 18 1445
    b042a60ef7dff760008df33cee372b945b6e884e blob   22054 5799 1463
    033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   9 20 7262 1 \
      b042a60ef7dff760008df33cee372b945b6e884e
    1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob   10 19 7282
    non delta: 15 objects
    chain length = 1: 3 objects
    .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack: ok
    
    • 此处,033b4 这个数据对象(即 repo.rb 文件的第一个版本)引用了数据对象 b042a,即该文件的第二个版本,命令输出内容的第三列显示的是各个对象在包文件中的大小,可以看到 b042a 占用了 22K 空间,而 033b4 仅占用 9 字节。同样有趣的地方在于,第二个版本完整保存了文件内容,而原始的版本反而是以差异方式保存的,这是因为大部分情况下需要快速访问文件的最新版本。
    • 最妙之处是可以随时重新打包,Git 时常会自动对仓库进行重新打包以节省空间。当然也可以随时手动执行 git gc 命令来这么做。
    更多相关内容
  • 注意,引用量会随着时间发生快速的变化,本文参考的是本文发表时候的引用量。 在这份清单中,超过75%的文章都提到了深度学习和神经网络,尤其是卷积神经网络(CNN),其中,50%的文章是计算机领域模式识别应用。随着...

    本文来源:全球人工智能

    作者:Pedro Lopez,数据科学家,从事金融与商业智能

    深度学习是机器学习和统计学交叉领域的一个子集,在过去的几年里得到快速的发展。强大的开源工具以及大数据爆发使其取得令人惊讶的突破进展。本文根据微软学术(academic.microsoft.com)的引用量作为评价指标,从中选取了20篇顶尖论文。注意,引用量会随着时间发生快速的变化,本文参考的是本文发表时候的引用量。


    在这份清单中,超过75%的文章都提到了深度学习和神经网络,尤其是卷积神经网络(CNN),其中,50%的文章是计算机领域模式识别应用。随着硬件性能、数据量及开源工具的发展,使用基于GPU的工具箱,如TensorFlow、Theano等,有利于数据科学家和机器学习工程师扩展相应的应用领域。


    640?wx_fmt=png&wxfrom=5&wx_lazy=1


    1.Deep Learning,Yann L., Yoshua B. & Geoffrey H. (2015) (引用量: 5,716)


    本文是深度学习大牛Hinton的开山之作,引用量高达5700多次。在这篇文章中,提出了深度学习的方法,它允许由多个处理层组成的计算模型来学习具有多个抽象层次的数据表示。这些方法极大地促进了语言识别、视觉物体识别、目标检测以及药物发现、基因组合灯许多领域的进展。


    论文地址:


    https://www.cs.toronto.edu/~hinton/absps/NatureDeepReview.pdf?spm=a2c4e.11153940.blogcont576283.17.3ac27677LdbpjU&file=NatureDeepReview.pdf


    2.TensorFlow: Large-Scale Machine Learning on Heterogeneous Distributed Systems, by Martín A., Ashish A. B., Eugene B. C., et al. (2015) (引用量: 2,423)


    这篇文章发布了TensorFlow工具箱,该工具箱使用非常灵活,十分受研究者的追捧,很多研究者认为该工具箱在之后的研究中会占据主导地位。TensorFlow可以用来表示各种各样的算法,包括深层神经网络模型以及推理算法等。TensorFlow已经被用于研究,并将计算机学习系统部署到计算机科学和其它的多个领域,包括语言识别、计算机视觉、机器人、信息检索、自然语言处理、地理信息提取等。


    论文地址:


    http://download.tensorflow.org/paper/whitepaper2015.pdf?spm=a2c4e.11153940.blogcont576283.18.3ac27677XarSxP&file=whitepaper2015.pdf


    3.TensorFlow: a system for large-scale machine learning, by Martín A., Paul B., Jianmin C., Zhifeng C., Andy D. et al. (2016) (引用量: 2,227)


    TensorFlow支持各种应用,对深层神经网络的训练和推理能力成为了研究者的关注点。谷歌在其一些产品中使用了TensorFlow,并将其公开为一个开源项目,目前已被广泛用于机器学习的研究中。


    论文地址:


    https://www.usenix.org/legacy/system/files/conference/osdi16/osdi16-abadi.pdf%20rel=?spm=a2c4e.11153940.blogcont576283.19.3ac27677wXFSLT&file=osdi16-abadi.pdf%20rel=


    4.Deep learning in neural networks, by Juergen Schmidhuber (2015) (引用量: 2,196)


    这篇文中是一个综述类文章,总结了深度学习和神经网络的发展历史。浅层和深层学习器是通过网络层数的数量区分,并且详细讲解了有监督学习(简要介绍反向传播算法的历史)、无监督学习、强化学习、进化计算以及深层编码网络。


    论文地址:


    https://arxiv.org/pdf/1404.7828.pdf?spm=a2c4e.11153940.blogcont576283.20.3ac27677wM2vCk&file=1404.7828.pdf


    5.Human-level control through deep reinforcement learning, by Volodymyr M., Koray K., David S., Andrei A. R., Joel V et al (2015) (引用量: 2,086)


    这篇文章主要是使用深层神经网络的最新进展——强化学习,并训练了一种新颖且智能的代理,被称为Q网络。Q网络使用端到端强化学习直接从高维感官输入学习到成功的策略,并使用经典游戏Atari 2600对其进行测试,结果表明其效果非常好。


    论文地址:


    https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf?spm=a2c4e.11153940.blogcont576283.21.3ac27677khlbo4&file=MnihEtAlHassibis15NatureControlDeepRL.pdf


    6.Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks, by Shaoqing R., Kaiming H., Ross B. G. & Jian S. (2015) (引用量: 1,421)


    这篇文章的主要工作是引入了区域推荐网络(RPN),该网络能够与检测网络共享全图像卷积特征,从而使无成本的区域推荐成为可能。一个RPN网络是一个全卷积网络,同时预测每个位置对象的范围和其分数。


    论文地址:


    https://arxiv.org/pdf/1506.01497.pdf?spm=a2c4e.11153940.blogcont576283.22.3ac276779Ttljn&file=1506.01497.pdf


    7.Long-term recurrent convolutional networks for visual recognition and description, by Jeff D., Lisa Anne H., Sergio G., Marcus R., Subhashini V. et al. (2015) (引用量: 1,285)


    与当前假设固定时空感受野或简单时间平均序列的模型处理相比而言,递归卷积模型是“双份深度”,这是由于该模型是“时间层”和“空间层”的组合。


    论文地址:


    https://arxiv.org/pdf/1411.4389.pdf?spm=a2c4e.11153940.blogcont576283.23.3ac27677bXpou7&file=1411.4389.pdf


    8.MatConvNet: Convolutional Neural Networks for MATLAB, by Andrea Vedaldi & Karel Lenc (2015) (引用量: 1,148)


    本文是针对MATLAB开发的深度学习工具箱,它揭露了CNN模型也可以通过简单使用MATLAB函数完成搭建。该工具箱提供了卷积层、池化层等功能。该文档简单介绍了CNN,并说明如何在matconvnet工具箱中实现模型的搭建,并给出了每个计算块的技术细节。


    论文地址:


    https://arxiv.org/pdf/1412.4564.pdf?spm=a2c4e.11153940.blogcont576283.24.3ac27677k3MDgU&file=1412.4564.pdf


    9.Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks, by Alec R., Luke M. & Soumith C. (2015) (引用量: 1,054)


    在这篇文章中,希望通过CNN缩小有监督学习和无监督学习二者之间的差距。引入了一类新的CNN模型,被称作深层卷积生成对抗网络(DCGANs)。该网络具有一定体系结构约束,也被证明是无监督学习问题中一种有效方法。


    论文地址:


    https://arxiv.org/pdf/1511.06434.pdf?spm=a2c4e.11153940.blogcont576283.25.3ac276771TzGAy&file=1511.06434.pdf


    10.U-Net: Convolutional Networks for Biomedical Image Segmentation, by Olaf R., Philipp F. &Thomas B. (2015) (引用量: 975)


    对于深度学习而言,人们大多有个共识——深层网络训练成功需要依赖于大量的训练样本。而在本篇文章中,提出了一种新的网络和训练策略,其训练策略依赖于数据增强,使其可以更高效地使用现有样本。


    论文地址:


    https://arxiv.org/pdf/1505.04597.pdf?spm=a2c4e.11153940.blogcont576283.26.3ac276770IHiyG&file=1505.04597.pdf


    11.Conditional Random Fields as Recurrent Neural Networks, by Shuai Z., Sadeep J., Bernardino R., Vibhav V. et al (2015) (引用量: 760)


    在这篇文章中,引入了一种新的模型,它将卷积神经网络(CNN)与条件随机场(CRF)二者的优点相结合,构造出RNN网络。


    论文地址:


    http://www.robots.ox.ac.uk/~szheng/papers/CRFasRNN.pdf?spm=a2c4e.11153940.blogcont576283.27.3ac27677wwmxH5&file=CRFasRNN.pdf


    12.Image Super-Resolution Using Deep Convolutional Networks, by Chao D., Chen C., Kaiming H. & Xiaoou T. (2014) (引用量: 591)


    这篇文章采用方法是直接学习低分辨率到高分辨率图像的端到端映射,该映射被表示为一个深层卷积神经网络(CNN),它以低分辨率图像作为输入,输出的是高分辨率图像。


    论文地址:


    https://arxiv.org/pdf/1501.00092.pdf?spm=a2c4e.11153940.blogcont576283.28.3ac27677uoRVt5&file=1501.00092.pdf


    13.Beyond short snippets: Deep networks for video classification, by Joe Y. Ng, Matthew J. H., Sudheendra V., Oriol V., Rajat M. & George T. (2015) (引用量: 533)


    在这篇文章中,提出了将CNN与LSTM结合在一起对视频数据进行特征提取,单帧的图像信息通过CNN获取特征,然后将CNN的输出按时间顺序通过LSTM,最终将视频数据在空间和时间维度上进行了特征表达。


    论文地址:


    https://arxiv.org/pdf/1503.08909.pdf?spm=a2c4e.11153940.blogcont576283.29.3ac27677qJnF3L&file=1503.08909.pdf


    14.Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning, by Christian S., Sergey I., Vincent V. & Alexander A A. (2017) (引用量: 520)


    卷积网络变得越来越深,网络模型也变得越来越复杂。谷歌公司提出的Inception-v4网络结构是近年来图像识别领域取得的最大进展。该网络结构具有三个残差网络和一个Inception-v4结构单元。在ImageNet挑战赛(CLS)top-5中实现3.08%的错误率。


    论文地址:


    https://arxiv.org/pdf/1602.07261.pdf?spm=a2c4e.11153940.blogcont576283.30.3ac27677uhbfsE&file=1602.07261.pdf


    15.Salient Object Detection: A Discriminative Regional Feature Integration Approach, by Huaizu J., Jingdong W., Zejian Y., Yang W., Nanning Z. & Shipeng Li. (2013) (引用量: 518)


    在这篇文章中,将显著图计算转化为一个回归问题。采用的方法是基于多层次的图像分割,并利用监督学习方法将区域特征向量映射为一个显著性分数。


    论文地址:


    https://arxiv.org/pdf/1410.5926.pdf?spm=a2c4e.11153940.blogcont576283.31.3ac27677YZbKox&file=1410.5926.pdf


    16.Visual Madlibs: Fill in the Blank Description Generation and Question Answering, by Licheng Y., Eunbyung P., Alexander C. B. & Tamara L. B. (2015) (引用量: 510)


    在这篇文章中,介绍了一个新的数据集——Visual Madlibs。该数据集包含360001个针对10738幅图像的自然语言描述,它是在空白模板中利用自动生成进行收集,收集的是有针对性的描述,比如人和物体、外貌、活动和互动、以及对一般场景或更广阔背景的推论。


    论文地址:


    https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Yu_Visual_Madlibs_Fill_ICCV_2015_paper.pdf?spm=a2c4e.11153940.blogcont576283.32.3ac27677NTzhEK&file=Yu_Visual_Madlibs_Fill_ICCV_2015_paper.pdf


    17.Asynchronous methods for deep reinforcement learning, by Volodymyr M., Adrià P. B., Mehdi M., Alex G., Tim H. et al. (2016) (引用量: 472)


    A3C算法是AC算法的异步变体,在Atari领域是最先进的算法。训练时候采用的多核CPU,而不是单个GPU,节约了一半的时间。此外,还发现A3C算法在各种各样的连续电机控制问题上性能表现优异。


    论文地址:


    http://proceedings.mlr.press/v48/mniha16.pdf?spm=a2c4e.11153940.blogcont576283.33.3ac27677kBRXJT&file=mniha16.pdf


    18.Theano: A Python framework for fast computation of mathematical expressions., by by Rami A., Guillaume A., Amjad A., Christof A. et al (2016) (引用量: 451)


    Theano是一个Python库,它允许使用者定义、优化以及有效地评估涉及多维数组的数学表达式。自推出以来,它一直是最常用的CPU和GPU数学编译器,尤其是在机器学习社区中显示出其性能的稳定提升。


    论文地址:


    https://arxiv.org/pdf/1605.02688.pdf?spm=a2c4e.11153940.blogcont576283.34.3ac27677blRucu&file=1605.02688.pdf


    19.Deep Learning Face Attributes in the Wild, by Ziwei L., Ping L., Xiaogang W. & Xiaoou T. (2015) (引用量: 401)


    该框架不仅大大提升了系统的性能,而且表明了学习人脸表征是有价值的事实。(1)展示了人脸定位(LNET)和属性预测(ANET)可以通过不同的预训练方法改进;(2)尽管只微调了LNet过滤器,但它们在整个图像上的响应图对人脸位置有很强的指示性。


    论文地址:


    https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Liu_Deep_Learning_Face_ICCV_2015_paper.pdf?spm=a2c4e.11153940.blogcont576283.35.3ac27677CwhXcF&file=Liu_Deep_Learning_Face_ICCV_2015_paper.pdf


    20.Character-level convolutional networks for text classification, by Xiang Z., Junbo Jake Z. & Yann L. (2015) (引用量: 401)


    这篇文章使用字符级卷积神经网络(Char-CNN)实现文本的分类,并构建了几个大规模数据集,实验结果表明,字符级卷积神经网络可以实现很好的性能。


    论文地址:


    http://papers.nips.cc/paper/5782-character-level-convolutional-networks-for-text-classification.pdf?spm=a2c4e.11153940.blogcont576283.36.3ac276778WChsu&file=5782-character-level-convolutional-networks-for-text-classification.pdf


    近期热文


    广告、商业合作

    请添加微信:guodongwe1991

    (备注:商务合作)

    展开全文
  • Java值传递和引用传递详细说明

    千次阅读 多人点赞 2020-07-14 15:53:17
    学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的话题又是充满争议:有的论坛帖子说Java只有值传递,...

    本文旨在用最通俗的语言讲述最枯燥的基本知识

    学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的话题又是充满争议:有的论坛帖子说Java只有值传递,有的博客说两者皆有;这让人有点摸不着头脑,下面我们就这个话题做一些探讨,对书籍、对论坛博客的说法,做一次考证,以得出信得过的答案。

    其实,对于值传递和引用传递的语法和运用,百度一下,就能出来可观的解释和例子数目,或许你看一下例子好像就懂,但是当你参加面试,做一道这个知识点的笔试题时感觉自己会,胸有成熟的写了答案,却发现是错的,或者是你根本不会做。

    是什么原因?

    那是因为你对知识点没有了解透彻,只知道其皮毛。要熟读一个语法很简单,要理解一行代码也不难,但是能把学过的知识融会贯通,串联起来理解,那就是非常难了,在此,关于值传递和引用传递,小编会从以前学过的基础知识开始,从内存模型开始,一步步的引出值传递和引用传递的本质原理,故篇幅较长,知识点较多,望读者多有包涵。

    1. 形参与实参

    我们先来重温一组语法:

    1. 形参:方法被调用时需要传递进来的参数,如:func(int a)中的a,它只有在func被调用期间a才有意义,也就是会被分配内存空间,在方法func执行完成后,a就会被销毁释放空间,也就是不存在了

    2. 实参:方法被调用时是传入的实际值,它在方法被调用前就已经被初始化并且在方法被调用时传入。

    举个栗子:

    1public static void func(int a){
    2 a=20;
    3 System.out.println(a);
    4}
    5public static void main(String[] args) {
    6 int a=10;//变量
    7 func(a);
    8}
    

    例子中
    int a=10;中的a在被调用之前就已经创建并初始化,在调用func方法时,他被当做参数传入,所以这个a是实参。
    而func(int a)中的a只有在func被调用时它的生命周期才开始,而在func调用结束之后,它也随之被JVM释放掉,,所以这个a是形参。

    2. Java的数据类型

    所谓数据类型,是编程语言中对内存的一种抽象表达方式,我们知道程序是由代码文件和静态资源组成,在程序被运行前,这些代码存在在硬盘里,程序开始运行,这些代码会被转成计算机能识别的内容放到内存中被执行。
    因此

    数据类型实质上是用来定义编程语言中相同类型的数据的存储形式,也就是决定了如何将代表这些值的位存储到计算机的内存中。

    所以,数据在内存中的存储,是根据数据类型来划定存储形式和存储位置的。
    那么
    Java的数据类型有哪些?

    1. 基本类型:编程语言中内置的最小粒度的数据类型。它包括四大类八种类型:

    4种整数类型:byte、short、int、long
    2种浮点数类型:float、double
    1种字符类型:char
    1种布尔类型:boolean

    1. 引用类型:引用也叫句柄,引用类型,是编程语言中定义的在句柄中存放着实际内容所在地址的地址值的一种数据形式。它主要包括:


    接口
    数组

    有了数据类型,JVM对程序数据的管理就规范化了,不同的数据类型,它的存储形式和位置是不一样的,要想知道JVM是怎么存储各种类型的数据,就得先了解JVM的内存划分以及每部分的职能。

    3.JVM内存的划分及职能

    Java语言本身是不能操作内存的,它的一切都是交给JVM来管理和控制的,因此Java内存区域的划分也就是JVM的区域划分,在说JVM的内存划分之前,我们先来看一下Java程序的执行过程,如下图:

    640?wx_fmt=png


    有图可以看出:Java代码被编译器编译成字节码之后,JVM开辟一片内存空间(也叫运行时数据区),通过类加载器加到到运行时数据区来存储程序执行期间需要用到的数据和相关信息,在这个数据区中,它由以下几部分组成:

     

    1. 虚拟机栈

    2. 堆

    3. 程序计数器

    4. 方法区

    5. 本地方法栈

    我们接着来了解一下每部分的原理以及具体用来存储程序执行过程中的哪些数据。


    1. 虚拟机栈

    虚拟机栈是Java方法执行的内存模型,栈中存放着栈帧,每个栈帧分别对应一个被调用的方法,方法的调用过程对应栈帧在虚拟机中入栈到出栈的过程。

    栈是线程私有的,也就是线程之间的栈是隔离的;当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈。

    下图表示了一个Java栈的模型以及栈帧的组成:

    640?wx_fmt=png


    栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。

     

    每个栈帧中包括:

    1. 局部变量表:用来存储方法中的局部变量(非静态变量、函数形参)。当变量为基本数据类型时,直接存储值,当变量为引用类型时,存储的是指向具体对象的引用。

    2. 操作数栈:Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指操作数栈。

    3. 指向运行时常量池的引用:存储程序执行时可能用到常量的引用。

    4. 方法返回地址:存储方法执行完成后的返回地址。


    2. 堆:

    堆是用来存储对象本身和数组的,在JVM中只有一个堆,因此,堆是被所有线程共享的。


    3. 方法区:

    方法区是一块所有线程共享的内存逻辑区域,在JVM中只有一个方法区,用来存储一些线程可共享的内容,它是线程安全的,多个线程同时访问方法区中同一个内容时,只能有一个线程装载该数据,其它线程只能等待。

    方法区可存储的内容有:类的全路径名、类的直接超类的权全限定名、类的访问修饰符、类的类型(类或接口)、类的直接接口全限定名的有序列表、常量池(字段,方法信息,静态变量,类型引用(class))等。


    4. 本地方法栈:

    本地方法栈的功能和虚拟机栈是基本一致的,并且也是线程私有的,它们的区别在于虚拟机栈是为执行Java方法服务的,而本地方法栈是为执行本地方法服务的。

    有人会疑惑:什么是本地方法?为什么Java还要调用本地方法?


    5. 程序计数器:

    线程私有的。
    记录着当前线程所执行的字节码的行号指示器,在程序运行过程中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。


    4. 数据如何在内存中存储?

    从上面程序运行图我们可以看到,JVM在程序运行时的内存分配有三个地方:

    • 静态方法区

    • 常量区

    相应地,每个存储区域都有自己的内存分配策略:

    • 堆式:

    • 栈式

    • 静态

    我们已经知道:Java中的数据类型有基本数据类型和引用数据类型,那么这些数据的存储都使用哪一种策略呢?
    这里要分以下的情况进行探究:

    1. 基本数据类型的存储:

    • A. 基本数据类型的局部变量

    • B. 基本数据类型的成员变量

    • C. 基本数据类型的静态变量

    2. 引用数据类型的存储


    1. 基本数据类型的存储


    我们分别来研究一下:

    A.基本数据类型的局部变量

    1. 定义基本数据类型的局部变量以及数据都是直接存储在内存中的栈上,也就是前面说到的“虚拟机栈”,数据本身的值就是存储在栈空间里面。

      640?wx_fmt=png


      如上图,在方法内定义的变量直接存储在栈中,如

    1int age=50;
    2int weight=50;
    3int grade=6;
    

    当我们写“int age=50;”,其实是分为两步的:

    1int age;//定义变量
    2age=50;//赋值
    

    首先JVM创建一个名为age的变量,存于局部变量表中,然后去栈中查找是否存在有字面量值为50的内容,如果有就直接把age指向这个地址,如果没有,JVM会在栈中开辟一块空间来存储“50”这个内容,并且把age指向这个地址。因此我们可以知道:
    我们声明并初始化基本数据类型的局部变量时,变量名以及字面量值都是存储在栈中,而且是真实的内容。

    我们再来看“int weight=50;”,按照刚才的思路:字面量为50的内容在栈中已经存在,因此weight是直接指向这个地址的。由此可见:栈中的数据在当前线程下是共享的

    那么如果再执行下面的代码呢?

    1weight=40;
    

    当代码中重新给weight变量进行赋值时,JVM会去栈中寻找字面量为40的内容,发现没有,就会开辟一块内存空间存储40这个内容,并且把weight指向这个地址。由此可知:

    基本数据类型的数据本身是不会改变的,当局部变量重新赋值时,并不是在内存中改变字面量内容,而是重新在栈中寻找已存在的相同的数据,若栈中不存在,则重新开辟内存存新数据,并且把要重新赋值的局部变量的引用指向新数据所在地址。


    B. 基本数据类型的成员变量

    成员变量:顾名思义,就是在类体中定义的变量。
    看下图:

    640?wx_fmt=png

     

    我们看per的地址指向的是堆内存中的一块区域,我们来还原一下代码:

     1public class Person{
     2  private int age;
     3  private String name;
     4  private int grade;
     5//篇幅较长,省略setter getter方法
     6  static void run(){
     7     System.out.println("run...."); 
     8   };
     9}
    10
    11//调用
    12Person per=new Person();
    

    同样是局部变量的age、name、grade却被存储到了堆中为per对象开辟的一块空间中。因此可知:基本数据类型的成员变量名和值都存储于堆中,其生命周期和对象的是一致的。


    C. 基本数据类型的静态变量

    前面提到方法区用来存储一些共享数据,因此基本数据类型的静态变量名以及值存储于方法区的运行时常量池中,静态变量随类加载而加载,随类消失而消失


    2. 引用数据类型的存储:

    上面提到:堆是用来存储对象本身和数组,而引用(句柄)存放的是实际内容的地址值,因此通过上面的程序运行图,也可以看出,当我们定义一个对象时

    1Person per=new Person();
    

    实际上,它也是有两个过程:

    1Person per;//定义变量
    2per=new Person();//赋值
    

    在执行Person per;时,JVM先在虚拟机栈中的变量表中开辟一块内存存放per变量,在执行per=new Person()时,JVM会创建一个Person类的实例对象并在堆中开辟一块内存存储这个实例,同时把实例的地址值赋值给per变量。因此可见:
    对于引用数据类型的对象/数组,变量名存在栈中,变量值存储的是对象的地址,并不是对象的实际内容。

    6. 值传递和引用传递

    前面已经介绍过形参和实参,也介绍了数据类型以及数据在内存中的存储形式,接下来,就是文章的主题:值传递和引用的传递。

    值传递:
    在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。

    来看个例子:

     1public static void valueCrossTest(int age,float weight){
     2    System.out.println("传入的age:"+age);
     3    System.out.println("传入的weight:"+weight);
     4    age=33;
     5    weight=89.5f;
     6    System.out.println("方法内重新赋值后的age:"+age);
     7    System.out.println("方法内重新赋值后的weight:"+weight);
     8    }
     9
    10//测试
    11public static void main(String[] args) {
    12        int a=25;
    13        float w=77.5f;
    14        valueCrossTest(a,w);
    15        System.out.println("方法执行后的age:"+a);
    16        System.out.println("方法执行后的weight:"+w);
    17}
    

    输出结果:

    1传入的age:25
    2传入的weight:77.5
    3
    4方法内重新赋值后的age:33
    5方法内重新赋值后的weight:89.5
    6
    7方法执行后的age:25
    8方法执行后的weight:77.5
    

    从上面的打印结果可以看到:
    a和w作为实参传入valueCrossTest之后,无论在方法内做了什么操作,最终a和w都没变化。

    这是什么造型呢?!!

    下面我们根据上面学到的知识点,进行详细的分析:

    首先程序运行时,调用mian()方法,此时JVM为main()方法往虚拟机栈中压入一个栈帧,即为当前栈帧,用来存放main()中的局部变量表(包括参数)、操作栈、方法出口等信息,如a和w都是mian()方法中的局部变量,因此可以断定,a和w是躺着mian方法所在的栈帧中
    如图:

    640?wx_fmt=jpeg


    而当执行到valueCrossTest()方法时,JVM也为其往虚拟机栈中压入一个栈,即为当前栈帧,用来存放valueCrossTest()中的局部变量等信息,因此age和weight是躺着valueCrossTest方法所在的栈帧中,而他们的值是从a和w的值copy了一份副本而得,如图:

    640?wx_fmt=png

    因而可以a和age、w和weight对应的内容是不一致的,所以当在方法内重新赋值时,实际流程如图:

    640?wx_fmt=jpeg

    也就是说,age和weight的改动,只是改变了当前栈帧(valueCrossTest方法所在栈帧)里的内容,当方法执行结束之后,这些局部变量都会被销毁,mian方法所在栈帧重新回到栈顶,成为当前栈帧,再次输出a和w时,依然是初始化时的内容。
    因此:
    值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。

     

    引用传递:
    ”引用”也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向通愉快内存地址,对形参的操作会影响的真实内容。

    举个栗子:
    先定义一个对象:

     1public class Person {
     2        private String name;
     3        private int age;
     4
     5        public String getName() {
     6            return name;
     7        }
     8        public void setName(String name) {
     9            this.name = name;
    10        }
    11        public int getAge() {
    12            return age;
    13        }
    14        public void setAge(int age) {
    15            this.age = age;
    16        }
    17}
    

    我们写个函数测试一下:

     1public static void PersonCrossTest(Person person){
     2        System.out.println("传入的person的name:"+person.getName());
     3        person.setName("我是张小龙");
     4        System.out.println("方法内重新赋值后的name:"+person.getName());
     5    }
     6//测试
     7public static void main(String[] args) {
     8        Person p=new Person();
     9        p.setName("我是马化腾");
    10        p.setAge(45);
    11        PersonCrossTest(p);
    12        System.out.println("方法执行后的name:"+p.getName());
    13}
    

    输出结果:

    1传入的person的name:我是马化腾
    2方法内重新赋值后的name:我是张小龙
    3方法执行后的name:我是张小龙
    

    可以看出,person经过personCrossTest()方法的执行之后,内容发生了改变,这印证了上面所说的“引用传递”,对形参的操作,改变了实际对象的内容。

    那么,到这里就结题了吗?
    不是的,没那么简单,
    能看得到想要的效果
    是因为刚好选对了例子而已!!!

    下面我们对上面的例子稍作修改,加上一行代码,

    1public static void PersonCrossTest(Person person){
    2        System.out.println("传入的person的name:"+person.getName());
    3        person=new Person();//加多此行代码
    4        person.setName("我是张小龙");
    5        System.out.println("方法内重新赋值后的name:"+person.getName());
    6    }
    

    输出结果:

    1传入的person的name:我是马化腾
    2方法内重新赋值后的name:我是张小龙
    3方法执行后的name:我是马化腾
    

    `
    为什么这次的输出和上次的不一样了呢?
    看出什么问题了吗?

    按照上面讲到JVM内存模型可以知道,对象和数组是存储在Java堆区的,而且堆区是共享的,因此程序执行到main()方法中的下列代码时

    1Person p=new Person();
    2        p.setName("我是马化腾");
    3        p.setAge(45);
    4        PersonCrossTest(p);
    

    JVM会在堆内开辟一块内存,用来存储p对象的所有内容,同时在main()方法所在线程的栈区中创建一个引用p存储堆区中p对象的真实地址,如图:

    640?wx_fmt=png


    当执行到PersonCrossTest()方法时,因为方法内有这么一行代码:

    1person=new Person();
    

    JVM需要在堆内另外开辟一块内存来存储new Person(),假如地址为“xo3333”,那此时形参person指向了这个地址,假如真的是引用传递,那么由上面讲到:引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变

    可以推出:实参也应该指向了新创建的person对象的地址,所以在执行PersonCrossTest()结束之后,最终输出的应该是后面创建的对象内容。

    然而实际上,最终的输出结果却跟我们推测的不一样,最终输出的仍然是一开始创建的对象的内容。

    由此可见:引用传递,在Java中并不存在。

    但是有人会疑问:为什么第一个例子中,在方法内修改了形参的内容,会导致原始对象的内容发生改变呢?

    这是因为:无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。

    640?wx_fmt=png

     

    有图可以看出,方法内的形参person和实参p并无实质关联,它只是由p处copy了一份指向对象的地址,此时:

    p和person都是指向同一个对象

    因此在第一个例子中,对形参p的操作,会影响到实参对应的对象内容。而在第二个例子中,当执行到new Person()之后,JVM在堆内开辟一块空间存储新对象,并且把person改成指向新对象的地址,此时:

    p依旧是指向旧的对象,person指向新对象的地址。

    所以此时对person的操作,实际上是对新对象的操作,于实参p中对应的对象毫无关系

    结语

    因此可见:在Java中所有的参数传递,不管基本类型还是引用类型,都是值传递,或者说是副本传递。
    只是在传递过程中:

    如果是对基本数据类型的数据进行操作,由于原始内容和副本都是存储实际值,并且是在不同的栈区,因此形参的操作,不影响原始内容。

    如果是对引用类型的数据进行操作,分两种情况,一种是形参和实参保持指向同一个对象地址,则形参的操作,会影响实参指向的对象的内容。一种是形参被改动指向新的对象地址(如重新赋值引用),则形参的操作,不会影响实参指向的对象的内容。

    原文转至:https://blog.csdn.net/bntx2jsqfehy7/article/details/83508006

    展开全文
  • Java引用对象

    万次阅读 2018-12-11 10:09:03
    同样要记住的是软引用、弱引用、虚引用只有在没有其他强引用指向被引用者时才有意义,它们让你可以在对象通常会成为垃圾回收器的食物时候获得该对象。这可能看起来很奇怪,如果不再持有强引用了,为什么我还关心这个...

    简介

    在写了15年C/C++之后,我于1999年开始写Java。借助指针切换(pointer handoffs)等编码实践或者Purify等工具,我认为自己对C风格的内存管理已经得心应手了,甚至已经不记得上次发生内存泄露是什么时候了。所以起初我接触到Java的自动内存管理时有些不屑,但很快就爱上它了。在我不需要再管理内存后我才意识到之前耗费了多少精力。

    接着我就遇到了第一个OutOfMemoryError。当时我就坐在那面对着控制台,没有堆栈,因为堆栈也需要内存。调试这个错误很困难,因为常用的工具都不能用了,甚至malloc logger都没有,而且1999年的时候Java的调试器还很原始。

    我不记得当时是什么原因导致的错误了,但我肯定当时没有用引用对象解决它。引用对象是在一年后我写服务端数据库缓存,尝试用软引用来限制缓存大小时才进入我的“工具箱”的。结果证明它们在这种场景下用处不大,我下面会解释原因。但当引用类型才进入我的“工具箱”后,我发现了很多其他用途,并且对JVM也有了更好的理解。

    Java堆和对象生命周期

    对于刚接触Java的C++程序员来说,栈和队之间的关系很难理解。在C++中,对象可以通过new操作在堆上创建,也可以通过“自动”分配在栈上创建。下面这种在C++中是合法的,会在栈上创建一个新的Integer对象,但对于Java编译器来说这有语法错误。

    1
    
    Integer foo = Integer(1);
    

    不同于C++,Java的所有对象都在堆保存,要求用new操作来创建对象。局部变量仍然储存在栈中,但它们持有这个对象的指针而不是这个对象本身(更让C++程序员困惑的是这些指针被叫做“引用”)。下面这个Java方法,有一个Integer变量引用一个从String解析而来的值:

    1
    2
    3
    4
    
    public static void foo(String bar)
    {
        Integer baz = new Integer(bar);
    }
    

    下图显示了这个方法相应的堆和栈之间的关系。栈被分割为栈帧,用于保存调用树中各个方法的参数和局部变量。这些变量指向对象–这个例子中的参数bar和局部变量baz–指23向存在于堆中的变量。

    现在仔细看看foo()的第一行,创建了一个Integer对象。这种情况下,JVM会先试图去为这个对象找足够的堆空间–在32位JVM上大约12 bytes,如果可以分配出空间,就调用Integer的构造函数,Integer的构造函数会解析传入的String然后初始化这个新创建的对象。最后,JVM在变量baz中保存一个指向该对象的指针。

    这是理想的道路,还有一些不那么美好的道路,其中我们关心的是当new操作不能为这个对象找到12 bytes的情况。在这种情况下,JVM会在放弃并抛出OutOfMemoryError之前调用垃圾回收器尝试腾出空间。

    垃圾回收

    虽然Java给了你new操作来在堆上分配对象,但是没有给你对应的delete操作来移除它们。当方法foo()返回,变量baz离开了作用域,但是它指向的对象依然存在于堆中。如果只是这样的话,那所有的程序都会很快耗尽内存。Java提供了垃圾回收器来清理那些不再被引用的对象。

    垃圾回收器会在程序尝试创建一个新对象但堆没有足够的空间时工作。回收器在堆上寻找那些不再被程序使用的对象并回收它们的空间时,请求创建对象的线程会暂停。如果回收器无法腾出足够的空间,并且JVM无法扩展堆,new操作就会失败并抛出OutOfMemoryError,通常接下来你的应用会停止。

    标记-清除

    其中一个关于垃圾回收器的误区是,很多人认为JVM为每个对象保存了一个引用计数,回收器只会回收那些引用计数为0的对象。事实上,JVM使用被称为“标记-清除”的技术。标记-清除算法的思路很简单:所有不能被程序访问到的对象都是垃圾,都可以被收集。

    标记-清除算法有以下阶段:

    阶段一:标记

    垃圾回收器从“root”引用开始,标记所有可以到达的对象。

    阶段二:清除

    在第一阶段没有被标记的都是不可到达的,也就是垃圾。如果垃圾对象定义了finalizer,它会被加到finalization队列(后文详细讨论)。否则,它占用的空间就可以被重新分配使用(具体的情况视GC的实现而定,有很多种实现)。

    阶段三:压缩(可选)

    一些回收器有第三步——压缩。在这一步,GC会移动对象使回收的对象留下的空闲空间合并,这可以防止堆变得碎片化,避免大块相邻内存分配的失败。

    例如,Hotspot JVM,在新生代使用会压缩的回收器,而在老年代使用非压缩的回收器(至少在1.6和1.7的“server” JVM是这样)。想了解更多信息,可以看本文后面的参考文献。

    那么什么是“roots”呢?在一个简单的Java应用中,它们是方法参数和局部变量(保存在栈中)、当前执行的表达式操作的对象(也保存在栈中)、静态类成员变量。

    对于使用自己classloader的程序,例如应用服务器,情况复杂一些:只有被system classloader(JVM启动时使用这个loader)加载的类包含root引用。那些被应用创建的classloader一旦没有其他引用也会被回收。这是应用服务器可以热部署的原因:它们为每个部署的应用创建独立的classloader,当应用下线或重新部署时释放classloader引用。

    理解root引用很重要,因为这定义了“强引用”,即如果可以从root沿着引用链到达某个对象,那么这个对象就被“强引用”了,则不会被回收。

    回到foo()方法,参数bar和局部变量baz使用当方法执行时才是强引用,一旦方法结束,它们都超出了作用域,被他们引用的对象就可以回收。另一种可能是,foo()返回一个它创建的Integer引用,这意味着这个对象会被调用foo()的那个方法保持强引用。

    看下面这个例子:

    1
    2
    
    LinkedList foo = new LinkedList();
    foo.add(new Integer(123));
    

    变量foo是一个指向LinkedList对象的root引用,列表中有0个或多个元素,都指向其对象。当我们调用add()时,向列表中添加了一个指向值为123的Integer实例的元素,这是一个强引用,意味着这个Integer实例不会被回收。一旦foo超出了作用域,这个LinkedList和它里面的一切都可以被回收,当前前提是没有其他强引用指向它了。

    你也许想知道循环引用会发生什么,即对象A包含一个对象B的引用,同时对象B也包含对象A的引用。答案是标记-清除回收器并不傻,如果A和B都无法由强引用链到达,那么它们都可以被回收。

    Finalizers

    C++允许对象定义析构方法,当对象离开作用域或者被明确删除时,它的析构函数会被调用来清理它使用的资源,对大多数对象来说即释放通过newmalloc分配的内存。在Java中,垃圾回收器会为你处理内存清理,所以不需要明确的析构函数来做这些。

    然而,内存并不是唯一可能需要被清理的资源。例如FileOutputStream,当创建这个对象的实例时,会从操作系统分配一个文件操作符(文件句柄),如果你在关闭流之前让它的所有引用都离开作用域了,这个文件操作符会发生什么呢?答案是流有finalizer,这个方法会在垃圾回收器回收对象前被JVM调用。这个例子中的FileOutputStreamfinalizer方法中会关闭流,这样就会将文件操作符返回给操作系统,同时也会刷新缓存,确保所有数据被正确地写到磁盘。

    任何对象都可以有finalizer,你只需要定义finalize()方法即可:

    1
    2
    3
    4
    
    protected void finalize() throws Throwable
    {
        // 在这里释放你的对象
    }
    

    finalizers看上去是一个由你自己清理的简单方式,但实际上有严重的限制。首先,你永远也不要依赖它做重要的事,因为对象的finalizers可能不会被调用,应用可能在对象被回收之前就结束了。finalizers还有一些更微妙的问题,我会在虚引用时讨论。

    对象的生命周期(无引用对象)

    总结起来,对象的一生可以用下面的图总结:被创建、被使用、可回收、最终被回收。阴影部分表示对象是“强可达”的时期,这是与引用对象规定的可达性比较而言很重要的时期。

    进入引用对象的世界

    JDK 1.2引入了java.lang.ref包,对象的生命周期增加了3种阶段:软可达、弱可达、虚可达。这些阶段只用来可否被回收,换言之,那些不是强引用的对象,必须是其中一种引用对象的被引用者:

    • 软可达
      对象是SoftReference的被引用者,并且没有强引用指向它。垃圾回收器会尽可能地保留它,但会在抛出OutOfMemoryError之前回收它。

    • 弱可达
      对象是WeakReference的被引用者,并且没有强引用指向它。垃圾回收器可以在任何时间回收它,不会试图去保留它。通常这个对象会在Major GC被回收,可能在Minor GC中存活。

    • 虚可达
      对象是PhantomReference的被引用者,它已经被选择要回收并且finalizer(如果有)已经运行了。这里的“可达”有点用词不当,在这个时候你已经没有办法访问到原始的对象了。

    如你所想,把这三种新的可选状态加到对象生命周期图中会变得很复杂。尽管文档指出了一个逻辑上从强可达到软可达、弱可达、虚可达的回收过程,但实际过程取决于你的程序创建了哪种引用对象。如果你创建了一个WeakReference而不是一个SoftReference,那么对象回收的过程是直接从强可达到弱可达最后被回收的。

    还有一点需要清楚的是,不是所有的对象都需要与引用对象关联,事实上,只有极少部分对象需要。引用对象是一个间接层:你通过引用对象去访问它的被引用者,你肯定不希望你的代码中充斥着这些间接层。事实上大部分程序只会使用引用对象去访问很少一部分它创建的对象。

    引用和被引用者

    引用对象是在你程序代码和一些称为被引用者的对象之间的中间层。每个引用对象都是围绕它的被引用者创建,并且被引用者是不能修改的。

    引用对象提供了get()方法来获取被引用者的强引用。垃圾回收器可能在某些情况下回收被引用者,一旦回收了,get()会返回null。正确使用引用,你需要类似下面这样的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    SoftReference<List<Foo>> ref = new SoftReference<List<Foo>>(new LinkedList<Foo>());
    
    // 代码其他地方创建了`Foo`,你想把它添加到列表中
    List<Foo> list = ref.get();
    if (list != null)
    {
        list.add(foo);
    }
    else
    {
        // 列表已经被回收了,做一些恰当的事
    }
    

    换言之:

    1. 你必须总是检查看被引用者是否是null。
      垃圾回收器可能在任何时间回收被引用者,如果你无所顾忌地使用,很快就会收获NullPointerException
    2. 当你想使用被引用者时,你必须持有一个它的强引用。
      再次强调, 垃圾回收器可能在任何时间回收被引用者,甚至是在单个表达式之间。上面的例子如果我不定义list变量,我而是简单地调用ref.get().add(foo),被引用者可能在检查是否为null和实际使用之间被回收。牢记垃圾回收器是在它自己的线程运行的,它不关心你的代码在干什么。
    3. 你必须持有引用类型的强引用。
      如果你创建了一个引用对象,但超出了它的作用域,那么这个引用对象自己也会被回收。这是显然的,但很容易被忘记,特别是在用引用队列(qv)追踪引用对象的时候。

    同样要记住的是软引用、弱引用、虚引用只有在没有其他强引用指向被引用者时才有意义,它们让你可以在对象通常会成为垃圾回收器的食物时候获得该对象。这可能看起来很奇怪,如果不再持有强引用了,为什么我还关心这个对象呢?原因视特殊的引用类型而定。

    软引用

    我们先从软引用开始来回答这个问题。如果一个对象是SoftReference的被引用者,并且它没有强引用,那么垃圾回收器可以回收但尽量不去回收它。因此,只要JVM有足够的内存,软引用对象就会在垃圾回收中存活,甚至经历好几轮垃圾回收依然存活。

    JDK文档说软引用适用于内存敏感的缓存:每个缓存对象都通过SoftReference访问,如果JVM觉得需要内存,它就会清除一些或者所有引用并回收对应的被引用者。如果JVM不需要内存,被引用者就会留在堆中,并且可以被程序代码访问到。在这种方案下,被引用者在使用时是强引用的,其他情况是软引用的,如果软引用被清除了,你需要刷新缓存。

    想作为这种角色使用,被缓存的对象需要比较大,如每个几kB。比如说,你想实现一个文件服务相同的文件会被定期检索,或者有一些大的图片对象需要缓存时会有用。但如果你的对象很小,你只有在需要定义大量对象时情况才会不同,引用对象还会增加整个程序的负担。

    内存限制型缓存被认为是有害的
    我的观点是,可用内存绝对是最差的管理缓存的方式。如果你的堆很小,你不时需要重新加载对象,无论它们是不是被活跃地使用,你也无法知道这个,因为缓存会静默地处理它们。大的堆更糟:你会持有对象远大于它的正常寿命,当每次垃圾回收时会使你的应用变慢,因为需要检查这些对象。如果这些对象没有被访问,这一部分堆有可能被交换出去,回收过程中可能有大量页错误。
    底线:如果你要用缓存,详细它会如何被使用,选一个适合的缓存策略(LRU、timed LRU),在选择基于内存的策略前仔细考虑。

    软引用用于断路器

    用软引用为内存分配提供断路器是更好的选择:在你的代码和它分配的内存之间使用软引用,你就可以避免可怕的OutOfMemoryError。这个技巧可以正常运作是因为在应用里内存的分配是趋于局部的:从数据库中读取行、从一个文件中处理数据。

    例如,如果你写过很多JDBC的代码,你可能会有类似下面这样的方法以某种方式处理查询的结果并且确保ResultSet被正确地关闭。这只有一个小缺陷:如果查询返回了一百万行,你没有可用的内存去保存它们时会发生什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    public static List<List<Object>> processResults(ResultSet rslt)
    throws SQLException
    {
        try
        {
            List<List<Object>> results = new LinkedList<List<Object>>();
            ResultSetMetaData meta = rslt.getMetaData();
            int colCount = meta.getColumnCount();
    
            while (rslt.next())
            {
                List<Object> row = new ArrayList<Object>(colCount);
                for (int ii = 1 ; ii <= colCount ; ii++)
                    row.add(rslt.getObject(ii));
    
                results.add(row);
            }
    
            return results;
        }
        finally
        {
            closeQuietly(rslt);
        }
    }
    

    答案当然是会得到OutOfMemoryError。这是使用断路器的绝佳地方:如果在处理查询时JVM要耗尽内存了,那就释放所有已经使用的那些内存,抛出一个应用特殊的异常。

    你可能很奇怪,这种情况下这次查询将被忽略,为什么不直接让内存耗尽的错误来做这件事呢?原因是并不仅仅只有你的应用被内存耗尽影响。如果你在一个应用服务器上运行,你的内存使用可能干掉其他应用。即使是在一个独有的环境,断路器也能提升你的应用的健壮性,因为它能限制问题,让你有机会恢复并继续运行。

    要创建一个断路器,首先你需要做的是把结果的列表包装在SoftReference中(你在前面已经见过这个代码了):

    1
    2
    
    SoftReference<List<List<Object>>> ref
            = new SoftReference<List<List<Object>>>(new LinkedList<List<Object>>());
    

    然后,你遍历结果,在你需要更新这个列表时为它创建强引用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    while (rslt.next())
    {
        rowCount++;
        // store the row data
    
        List<List<Object>> results = ref.get();
        if (results == null)
            throw new TooManyResultsException(rowCount);
        else
            results.add(row);
    
        results = null;
    }
    

    这可以满足要求是因为这个方法几乎所有的内存分配都发生在2个地方:调用next()时和代码把行里的数据存放到它自己的列表中时。第一种情况当你调用next()时会发生很多事情:ResultSet一般会在包含多行的一大块二进制数据中检索,然后当你调用getObject(),它会取出一部分数据把它转成Java对象。

    当这些昂贵的操作发生时,这个list只有来自SoftReference的引用,如果内存耗尽,引用会被清除,list会变成垃圾。这意味着这个方法可能抛出异常,但抛出异常的影响是有限的,也许调用方能以一点数量限制重新进行查询。

    一旦昂贵的操作完成,你可以没有影响地拿到list的强引用。注意到我用LinkedList保存结果而不是ArrayListLinkedList增长时只会增加少量字节,不太可能引起OutOfMemoryError,而如果ArrayList需要增加容量,它需要创建一个新数组,对于大列表来说,这可能意味着数MB的内存分配。

    还注意到我在添加新元素后把results变量设置为null,这是少数几种这样做是合理的情形之一。尽管在循环的最后变量超出了作用域,但垃圾回收器可能并不知道(因为JVM没有理由去清除变量在调用栈中的位置)。因此如果我不清除这个变量的话,它会在随后的循环中成为隐藏的强引用。

    软引用不是万能的

    软引用可以预防很多内存耗尽的情况,但不能预防所有。问题在于:为了真正地使用软引用,你需要创建一个被引用者的强引用,即为了向results中添加一行,我们需要持有实际列表的引用。我们持有强引用的时候就会面临发生内存耗尽错误的风险。

    使用断路器的目标是把一些无用的东西的时间窗口减到最小:你持有对象强引用的时间,更重要的是在这段时间中分配内存的总量。在我们的例子中,我们限制强引用去添加一行到results中,我们使用LinkedList而不是ArrayList因为前者扩容时增长更小。

    我想重申的是,如果我一个变量持有强引用,但这个变量很快超出了作用域,语言细则没有说JVM需要清除超出作用域的变量,如果是像写的这样,Oracle/OpenJDK JVM都没有这样做,如果我不明确地清除results变量,在遍历期间会保持强引用,阻止软引用做它的工作。

    最后,仔细考虑那些隐藏的强引用。例如,你可能会想在使用DOM构造XML文档时加入断路器。在DOM中,每个节点都持有它父节点的引用,从而导致持有了树中每个其他节点的引用。如果你用递归去创建文档,你的栈中可能塞满了个别节点的引用。

    弱引用

    弱引用,正如它名字显示,是一个当垃圾回收器来敲门时不会反抗的引用对象。如果被引用者没有强引用或软引用而只有弱引用,那它就可以被回收。所以弱引用有什么用呢?有2个主要用途:关联没有内在联系的对象,或者通过canonicalizing map减少重复。

    ObjectOutputStream的问题

    第一个例子,我准备聚焦不使用弱引用的对象序列化。ObjectOutputStream以及它的伙伴ObjectInputStream提供了任意Java对象与字节流之间相互转换的方式。根据对象模型的观点,流和用这些流写的对象之间是没有联系的。流不是由这些被写的对象组成的,也不是它们的聚集。

    但是当你看这些流的说明时,你会看到事实上是有联系的:为了维持对象的唯一性,输出流会和每个被写的对象关联一个唯一的标识符,随后的写对象的请求被替换为写这个标识符。这个特征对于流序列号对象的能力来说绝对是很重要的,如果没有这个特征,自我引用的对象会变成一个无限的字节流。

    要实现这个特征,流需要持有每个写到流中的对象的强引用。对于决定在socket通信时用对象流作为消息协议的程序员来说,有这么一个问题:消息被设计为短暂的,但流会在内存中持有它们,不久之后,程序会耗尽内存(除非程序员知道在每次通信后调用reset())。

    这种非与生俱来的联系惊人的普遍。它们会在程序员为了使用对象而需要去维持必不可少的上下文时出现。有时这些联系被运行环境默默管理,例如servlet Session对象;有时这些联系需要被程序员明确地管理,例如对象流;还有些时候,这种联系只有当生产环境的服务抛出内存耗尽的错误时才会被发现,比如埋藏在程序代码深处的静态Map

    弱引用提供了一种维持这种联系的同时还能让垃圾回收器做它的工作的方式,弱引用只有在同时还有强引用时才保持有效。回到对象流的例子,如果你用流来通信,一旦消息被写完就可以被回收了。另一方面,当流用来RMI访问一个生命周期很长的数据结构时,它能保持它一致。

    不幸的是,尽管对象流通信协议在JDK 1.2时被更新了。虚引用也是这样被加入的,但JDK的开发者并没有选择把二者结合到一起,所以记得调用reset()

    Canonicalizing Maps消除重复数据

    尽管存在对象流这种情况,但我不认为有很多你应该关联两个没有内在关系的对象的情行。我所看到的一些例子,例如Swing监听器,它们会自我清理,看起来更像是黑客,而不是有效的设计选择。

    当我最初写这篇文章的时候,大约是在2007年,我提出了canonicalizing map作为String.intern()的替代物,是在假设被存入常量池的字符串永远不会被清理的前提下。后来我得知这种担心是毫无根据的。更重要的是,从JDK 8开始,OpenJDK已经完全去掉了永久代。因此,没有必要害怕intern(),但是canonicalizing map对于字符串以外的对象仍然有用。

    在我看来,弱引用的最佳用途是实现canonicalizing map,这是一种确保同时只存在一个值对象实例的办法。String.intern()是这种map的典型例子:当你把一个字符串存入常量池时,JVM会将它添加到一个特殊的map中,这个map也用于保存字符串文本。这样做的原因不是像一些人认为的那样为了更快地进行比较。这是为了最大限度地减少重复的非文字字符串(如从文件或消息队列中读取的字符串)占用的内存量。

    简单的canonicalizing map通过使用相同的对象作为key和value来工作:你用任意实例传给map,如果map中已经有一个值,你就返回它。如果map中没有值,则存储传入的实例(并返回它)。当然,这仅适用于可用作map的key的对象。如果我们不担心内存泄漏,下面可能是我们实现String.intern()的方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    private Map<String,String> _map = new HashMap<String,String>();
    
    public synchronized String intern(String str)
    {
        if (_map.containsKey(str))
            return _map.get(str);
        _map.put(str, str);
        return str;
    }
    

    如果你只有少量字符串要放入常量池,例如也许在处理一个文件的简单方法中,这个实现没什么问题。然而,假设你正在编写一个长期运行的应用程序,该应用程序必须处理来自多个来源的输入,其中包含范围广泛的字符串,但仍有高度的重复。例如,一台处理上传的邮政地址数据文件的服务:New York将会有很多条目,Temperanceville VA的条目就不多了。你会想要消除前者的重复,但是不想保留后者超过必要的时间。

    这就是弱引用的canonicalizing map有所帮助的地方:只有程序中的一些代码正在使用它,它才允许你创建一个规范的实例。最后一个强引用消失后,这个规范的字符串将被回收。如果稍后再次出现该字符串,它将成为新的规范的实例。

    为了改进我们的“规范化工具”,我们可以用WeakHashMap替换HashMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    private Map<String,WeakReference<String>> _map
        = new WeakHashMap<String,WeakReference<String>>();
    
    public synchronized String intern(String str)
    {
        WeakReference<String> ref = _map.get(str);
        String s2 = (ref != null) ? ref.get() : null;
        if (s2 != null)
            return s2;
    
        _map.put(str, new WeakReference(str));
        return str;
    }
    

    首先要注意的是,虽然map的key是字符串,但它的值是WeakReference<String>。这是因为WeakHashMap对其key使用弱引用,但对其value持有强引用。因为我们的key和value是相同的,所以entry永远不会被回收。通过包装条目,我们让GC回收它。

    其次,注意返回字符串的过程:首先我们检索弱引用,如果它存在,那么我们检索引用对象。但是我们也必须检查那个对象。存在引用仍在map中但已经被清除了的可能。只有当引用对象不为空时,我们才返回它;否则,我们认为传入的字符串是新的规范的版本。

    第三,请注意我对intern()方法用了synchronizedcanonicalizing map最有可能的用途是在多线程环境中,例如应用服务,WeakHashMap没有内部同步。这个例子中的同步实际上相当幼稚,intern()方法可能成为争论的焦点。在现实世界的实现中,我可能会使用ConcurrentHashMap,但是对于教程来说,这种幼稚的方法更有效。

    最后,WeakHashMap的文档关于条目何时从map中移除有些模糊。它指出,“WeakHashMap的行为可能就像一个未知线程正在无声地删除条目。”实际上没有其他线程。相反,每当map被访问时,它就会被清理。为了跟踪哪些条目不再有效,它使用了引用队列。

    引用队列

    虽然判断一个引用是不是null可以让你知道它的引用对象是不是已经被回收,但是这样做并不是很高效;如果你有很多引用,你的程序会花大部分时间寻找那些已经被清除的引用。

    更好的解决方案是引用队列:你在构建时将引用与队列相关联,并且该引用将在被清除后放入队列中。要发现哪些引用已被清除,你需要从队列拉取。这可以通过后台线程来完成,但是在创建新引用时从队列拉取通常更简单(WeakHashMap就是这样做的)。

    引用队列最常与虚引用一起使用,在后面会描述,但是可以与任何引用类型一起使用。下面的代码是一个弱引用的例子:它创建了一组缓冲区,通过WeakReference访问,并且在每次创建后查看哪些引用已经被清除。如果运行此代码,你会看到create消息的长时间出现,当垃圾回收器运行时偶尔会出现一些clear消息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    public static void main(String[] argv) throws Exception
    {
        Set<WeakReference<byte[]>> refs = new HashSet<WeakReference<byte[]>>();
        ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
        
        for (int ii = 0 ; ii < 1000 ; ii++)
        {
            WeakReference<byte[]> ref = new WeakReference<byte[]>(new byte[1000000], queue);
            System.err.println(ii + ": created " + ref);
            refs.add(ref);
            
            Reference<? extends byte[]> r2;
            while ((r2 = queue.poll()) != null)
            {
                System.err.println("cleared " + r2);
                refs.remove(r2);
            }
        }
    }
    

    一如既往,关于这个代码有一些值得注意的事情。首先,虽然我们创建的是WeakReference实例,但是队列会给我们返回Reference。这提醒你,一旦它们入队,你使用的是什么类型的引用就不再重要了,被引用者已经被清除。

    第二,我们必须对引用对象本身进行强引用。引用对象知道队列,但是队列在引用进入队列前不知道引用。如果我们没有维护对引用对象的强引用,它本身就会被回收,并且永远不会被添加到队列中。在这个例子中,我使用了一个Set,一旦引用被清除,就删除它们(将它们留在Set中是内存泄漏)。

    虚引用

    虚引用不同于软引用和弱引用,它们不用于访问它们的被引用者。相反,他们的唯一目的是当它们的被引用者已经被回收时通知你。虽然这看起来毫无意义,但它实际上允许你比finalizers更灵活地执行资源清理。

    Finalizers的问题

    这篇文章中我更详细地讨论了finalizers。简而言之,你应该依靠try/catch/finally清理资源,而不是finalizers或虚引用。

    在对象生命周期的描述中,我提到finalizers有一些微妙的问题,使得它们不适合清理非内存资源。还有一些非微妙的问题,为了完整起见,我将在这里讨论。

    • finalizer可能永远不会被调用

      如果你的程序从未用完可用内存,那么垃圾回收器不会运行,你的finalizer也不会运行。对于长时间运行的应用程序(例如服务)来说,通常不会出现这个问题,但是短时间运行的程序可能会在没有运行垃圾收集的情况下完成。虽然有一种方法可以告诉JVM在程序退出之前运行finalizers,但这是不可靠的,可能会与其他shutdown hooks冲突。

    • Finalizers可能创建一个对象的其他强引用

      例如,通过将对象添加到集合中。这基本上复活了这个对象,但是,就像Stephen King's Pet Sematary一样,返回的对象“不太正确”。尤其是,当对象再次符合回收条件时,它的finalizer不会运行。也许你会使用这种复活技巧是有原因的,但是我无法想象,而且在代码上看起来会非常模糊。

    现在这些都已经过时了,我相信finalizers的真正问题是它们在垃圾回收器首次识别要回收的对象的时间和实际回收其内存的时间之间引入了间隙,因为finalization发生在它自己的线程上,独立于垃圾回收器的线程。JVM保证在返回OutMemoryError之前执行一次full collection,但是如果所有符合回收条件的对象都有finalizers,则回收将不起作用:这些对象保留在内存中等待finalization。假设一个标准JVM只有一个线程来处理所有对象的finalization,一些长时间运行的finalization,你就可以看到问题可能会出现。

    以下程序演示了这种行为:每个对象都有一个finalizer休眠半秒钟。不会有很长时间,除非你有成千上万的对象要清理。每个对象在创建后都会立即超出作用域,但是在某个时候你会耗尽内存(如果你想运行这个例子,我建议使用-Xmx64m来使错误快速发生;在我的开发机器上,有3Gb堆,实际上需要几分钟才能失败)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public class SlowFinalizer
    {
        public static void main(String[] argv) throws Exception
        {
            while (true)
            {
                Object foo = new SlowFinalizer();
            }
        }
    
        // some member variables to take up space -- approx 200 bytes
        double a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;
    
        // and the finalizer, which does nothing by take time
        protected void finalize() throws Throwable
        {
            try { Thread.sleep(500L); }
            catch (InterruptedException ignored) {}
            super.finalize();
        }
    }
    

    虚引用知晓之事

    当对象不再被使用时虚引用允许应用程序知晓,这样应用程序就可以清理对象的非内存资源。然而,与finalizers不同的是,当应用程序知道到这一点时,对象本身已经被收集了。

    此外,与finalizers不同,清理由应用程序而不是垃圾回收器来调度。您可以将一个或多个线程专用于清理,如果对象数量需要,可以增加线程数量。另一种方法——通常更简单——是使用对象工厂,并在创建新实例之前清理所有回收的实例。

    理解虚引用的关键点是,你不能使用引用来访问对象: get()总是返回null,即使对象仍然是强可达的。这意味着引用对象持有的不能是要清理的资源的唯一引用。相反,你必须维持对这些资源的至少一个其他强引用,并使用引用队列来通知被引用者已被回收。与其他引用类型一样,您的程序也必须拥有对引用对象本身的强引用,否则它将被回收,资源将内存泄露。

    用虚引用实现连接池

    数据库连接是任何应用程序中最宝贵的资源之一:它们需要时间来建立,并且数据库服务器严格限制它们将接受的同时打开的连接的数量。尽管如此,程序员对它们非常粗心,有时会为每个查询打开一个新的连接,或者忘记关闭它,或者不在finally块中关闭它。

    大多数应用服务部署使用连接池,而不是允许应用直接连接数据库:连接池维护一组打开的连接(通常是固定的),并根据需要将它们交给程序。用于生产环境的连接池提供了几种防止连接泄漏的方法,包括超时(识别运行时间过长的查询)和恢复被垃圾回收器回收的连接。

    下面这个连接池旨在演示虚引用,不能用于生产环境。Java有几个可用于生产环境的连接池,如Apache Commons DBCPC3P0

    后一个特性是虚引用的一个很好的例子。为了使它工作,连接池提供的Connection对象只是实际数据库连接的包装,可以在不丢失数据库连接的情况下回收它们,因为连接池保持对实际连接的强引用。连接池将虚引用与“包装成的”连接相关联,如果引用最终出现在引用队列中,则会将实际连接返回给连接池。

    连接池中最不有趣的部分是PooledConnection,如下所示。正如我说过的,它是一个包装,委派对实际连接的调用。不同的是我使用了反射代理来实现。JDBC接口随着Java的每一个版本而发展,其方式既不向前也不向后兼容;如果我使用了具体的实现,除非你使用了与我相同的JDK版本,否则你将无法编译。反射代理解决了这个问题,也使代码变得更短。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    
    public class PooledConnection
    implements InvocationHandler
    {
        private ConnectionPool _pool;
        private Connection _cxt;
    
        public PooledConnection(ConnectionPool pool, Connection cxt)
        {
            _pool = pool;
            _cxt = cxt;
        }
    
        private Connection getConnection()
        {
            try
            {
                if ((_cxt == null) || _cxt.isClosed())
                    throw new RuntimeException("Connection is closed");
            }
            catch (SQLException ex)
            {
                throw new RuntimeException("unable to determine if underlying connection is open", ex);
            }
    
            return _cxt;
        }
    
        public static Connection newInstance(ConnectionPool pool, Connection cxt)
        {
            return (Connection)Proxy.newProxyInstance(
                       PooledConnection.class.getClassLoader(),
                       new Class[] { Connection.class },
                       new PooledConnection(pool, cxt));
        }
        
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable
        {
            // if calling close() or isClosed(), invoke our implementation
            // otherwise, invoke the passed method on the delegate
        }
    
        private void close() throws SQLException
        {
            if (_cxt != null)
            {
                _pool.releaseConnection(_cxt);
                _cxt = null;
            }
        }
    
        private boolean isClosed() throws SQLException
        {
            return (_cxt == null) || (_cxt.isClosed());
        }
    }
    

    需要注意的最重要的一点是,PooledConnection同时引用了底层数据库连接和连接池。后者用于那些确实记得关闭连接的应用程序:我们希望立即告知连接池,以便底层连接可以立即被重用。

    getConnection()方法也值得一提:它的存在是为了捕捉那些在显式关闭连接后试图使用该连接的应用程序。如果连接已经交给另一个消费者,这可能是一件非常糟糕的事情。因此close()显式清除引用,getConnection()会检查该引用,并在连接不再有效时抛出异常。invocation handler用于所有委托调用。

    现在让我们将注意力转向连接池本身,从它用来管理连接的对象开始。

    1
    2
    3
    4
    5
    6
    
    private Queue<Connection> _pool = new LinkedList<Connection>();
    
    private ReferenceQueue<Object> _refQueue = new ReferenceQueue<Object>();
    
    private IdentityHashMap<Object,Connection> _ref2Cxt = new IdentityHashMap<Object,Connection>();
    private IdentityHashMap<Connection,Object> _cxt2Ref = new IdentityHashMap<Connection,Object>();
    

    当连接池被构造并存储在_pool中时,可用连接被初始化。我们使用引用队列_refQueue来标识已回收的连接。最后,我们有连接和引用之间的双向映射,在将连接返回到连接池时使用。

    正如我之前说过的,实际的数据库连接将在提交给应用程序代码之前被包装在PooledConnection中。这发生在wrapConnection()函数中,也是我们创建虚引用和连接-引用映射的地方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    private synchronized Connection wrapConnection(Connection cxt)
    {
        Connection wrapped = PooledConnection.newInstance(this, cxt);
        PhantomReference<Connection> ref = new PhantomReference<Connection>(wrapped, _refQueue);
        _cxt2Ref.put(cxt, ref);
        _ref2Cxt.put(ref, cxt);
        System.err.println("Acquired connection " + cxt );
        return wrapped;
    }
    

    wrapConnection对应的是releaseConnection(),该函数有两种变体。当应用程序代码显式关闭连接时,PooledConnection调用第一个,这是“快乐的道路”,它将连接放回连接池中供以后使用。它还会清除连接和引用之间的映射,因为它们不再需要。请注意,此方法具有默认(包)同步:它由PooledConnection调用,因此不能是私有的,但通常不可访问。

    1
    2
    3
    4
    5
    6
    7
    
    synchronized void releaseConnection(Connection cxt)
    {
        Object ref = _cxt2Ref.remove(cxt);
        _ref2Cxt.remove(ref);
        _pool.offer(cxt);
        System.err.println("Released connection " + cxt);
    }
    

    另一个变体使用虚引用来调用,这是“可悲的道路”,当应用程序不记得关闭连接时才会调用。在这种情况下,我们得到的只是虚引用,我们需要使用映射来检索实际连接(然后使用第一个变体将其返回到连接池中)。

    1
    2
    3
    4
    5
    6
    
    private synchronized void releaseConnection(Reference<?> ref)
    {
        Connection cxt = _ref2Cxt.remove(ref);
        if (cxt != null)
            releaseConnection(cxt);
    }
    

    有一种边缘情况:如果引用在应用程序调用close()之后进入队列,会发生什么?这种情况不太可能发生:当我们清除映射时,虚引用应该已经有资格被回收,这样它就不会进入队列。然而,我们必须考虑这种情况,这导致上面的空检查:如果映射已经被移除,那么连接已经被显式返回,我们不需要做任何事情。

    好了,您已经看到了底层代码,现在是时候让应用程序调用唯一的方法了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    public Connection getConnection()
    throws SQLException
    {
        while (true)
        {
            synchronized (this) 
            {
                if (_pool.size() > 0)
                    return wrapConnection(_pool.remove());
            }    
    
            tryWaitingForGarbageCollector();
        }
    }
    

    getConnection()的最佳路径是在_pool中有可用的连接。在这种情况下,一个连接被移除、包装并返回给调用者。不信的情况是没有任何连接,在这种情况下,调用者希望我们阻塞直到有一个连接可用。这可以通过两种方式发生:要么应用程序关闭连接并返回到_pool中,要么垃圾回收器找到一个已被放弃的连接,并将其关联的虚引用加入队列。

    为什么我使用synchronized(this)而不是显式锁?简而言之,这个实现是作为教学辅助工具,我想用最少的样板来强调同步点。在生产环境使用的连接池中,我实际上会避免显式同步,而是依赖并行数据结构,如ArrayBlockingQueueConcurrentHashMap

    在走这条路之前,我想谈谈同步。显然,对内部数据结构的所有访问都必须同步,因为多个线程可能会尝试同时获取或返回连接。只要_pool中有连接,同步代码就能快速执行,竞争的可能性就很低。然而,如果我们必须循环直到连接变得可用,我们希望最大限度地减少同步的时间:我们不希望在请求连接的调用者和返回连接的另一个调用者之间造成死锁。因此,在检查连接时,使用显式同步块。

    那么,如果我们调用getConnection(),并且池是空的,会发生什么呢?这是我们检查引用队列以找到被废弃的连接的时机。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    private void tryWaitingForGarbageCollector()
    {
        try
        {
            Reference<?> ref = _refQueue.remove(100);
            if (ref != null)
                releaseConnection(ref);
        }
        catch (InterruptedException ignored)
        {
            // we have to catch this exception, but it provides no information here
            // a production-quality pool might use it as part of an orderly shutdown
        }
    }
    

    这个函数强调了另一组相互冲突的目标:如果引用队列中没有任何引用,我们不想浪费时间,但是我们也不想在一个循环中重复检查_pool_refQueue。所以我在轮询队列时使用了一个短暂的超时时间:如果没有准备好,它会给另一个线程返回连接的机会。当然,这也带来了一个公平性问题:当一个线程正在等待引用队列时,另一个线程可能会返回一个被第三个线程立即占用的连接。理论上,等待线程可能会永远等待。在现实世界中,由于不太需要数据库连接,这种情况不太可能发生。

    虚引用带来的问题

    前面我提到到finalizers不能保证被调用。虚引用也是这样,原因相同:如果回收器不运行,不可达的对象不会被回收,对这些对象的引用也不会进入队列。考虑一个程序只在循环中调用getConnection(),让返回的连接超出作用域,如果它没有做任何其他事情来让垃圾回收器运行,那么它会很快耗尽连接池然后阻塞,等待永远无法恢复的连接。

    当然,有办法解决这个问题。最简单的方法之一是在tryWaitingForGarbageCollector()中调用System.gc()。尽管围绕这种方法有一些争议,但这是促使JVM回到理想状态的有效方式。这是一种既适用于finalizers也适用于虚引用的技术。

    这并不意味着你应该忽略虚引用,只使用finalizer。例如,在连接池的情况下,你可能希望显式关闭该连接池并关闭所有底层连接。你可以用finalizer来完成,但是需要和虚引用一样多的工作。在这种情况下,通过引用获得的可控因素(相对于任意终结线程)使它们成为更好的选择。

    最后一些思考:有时候你只需要更多内存

    虽然引用对象是管理内存消耗的非常有用的工具,但有时它们是不够的,有时又是过度的。例如,假设你正在构建一些大型对象,其中包含从数据库中读取的数据。虽然你可以使用软引用作为读取的断路器,并使用弱引用将数据规范化,但最终您的程序需要一定量的内存来运行。如果你不能给它足够的内存来实际完成任何工作,那么不管你的错误恢复能力有多强都无济于事。

    应对OutOfMemoryError时你首先应该搞清楚它为什么会发生。可能你有内存泄露,可能仅仅是你内存的设置太低了。

    开发过程中,你应该指定大的堆内存大小——1G或更多——关注程序到底用了多少内存(这种情况jconsole是一个有用的工具)。大多数应用会在模拟的负载下达到一个稳定的状态,这将指引你的生产环境堆配置。如果你的内存使用随时间增长,那很可能你在对象不再使用后仍持有强引用,引用类型可能会有用,但更可能的是有bug需要修复。

    底线是你需要理解你的应用。如果没有重复,canonicalizing map对你没有帮助。如果你希望定期执行数百万行查询,软引用是没有用的。但是在可以使用引用对象的情况下,它们会是你的救命恩人。

    其他信息

    你可以下载这篇文章中的示例代码:
    CircuitBreakerDemo通过模拟数据库的结果集引出内存驱动的断路器。
    WeakCanonicalizingMap 用WeakHashMap创建了典范字符串。这个demo 可能更有趣: 它用极端的长度来触发垃圾回收(注意:在大的堆内存下运行可能不凑效,试试-Xmx100m).
    SlowFinalizer展示了如何在垃圾回收器运行的情况下耗尽内存。
    ConnectionPool 和 PooledConnection 实现了一个简单的连接池。ConnectionPoolDemo 通过内存型的HSQLDB数据库来运用这个连接池(这里 是构建这个和其他例子的Maven POM)。
    “string canonicalizer” 类可以在这下到SourceForge, licensed为Apache 2.0.
    Sun有许多关于调整他们JVM的内存管理的文章。这篇 文章是一篇精彩的介绍,并提供了其他文档的链接。
    Brian Goetz在IBM developerWorks网站上有一个极好的专栏,叫做”Java Theory and Practice”。几年前他写了关于软引用 和 弱引用 的专栏. 这些文章对一些我看过的议题影响很深,例如使用WeakHashMap来用不同生命时期关联对象。

    我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

    展开全文
  • 前言: 整体上翻译了 ...这篇论文,便于英文基础不好的...(1)为了使 1∗11∗1 和 3∗33∗3 filter输出的结果又相同的尺寸,在expand modules中,给3∗33∗3 filter的原始输入添加一个像素的边界(zero-padding)。...
  • LaTeX参考文献引用

    千次阅读 2021-01-14 19:52:56
    二、参考文献引用1.引入库2.Latex的几种参考文献排序总结 前言 随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。 提示:这里可以添加本文要...
  • 超硬核!数据结构学霸笔记,考试面试吹牛就靠它

    万次阅读 多人点赞 2021-03-26 11:11:21
    1、引用和函数调用: 1.1引用:对一个数据建立一个“引用”,他的作用是为一个变量起一个别名。这是C++对C语言的一个重要补充。 用法很简单: int a = 5; int &b = a; b是a别名,b与a代表的是同一个变量,占内存...
  •  早在介绍JS的数据类型的时候就提到过基本类型和引用类型,不过在说两种类型之前,我们先来了解一下变量的 值的类型。在ECMAScript中,变量可以存在两种类型的值,即原始值和引用值。  (1)原始值  存储在栈中...
  • C++中常引用做形参

    千次阅读 2018-07-30 20:53:23
    2.还可以用定义常引用的方式,实现用字面引用赋值,字面没有内存空间,所以在初始化时C++编译器会偷偷地给引用分配内存空间,但是字面还是没有内存空间。 #include "iostream" using namespace ...
  • java 强 软 弱 虚引用的理解正确使用

    千次阅读 2018-12-02 12:52:01
    1、强引用(StrongReference) 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如:Object o=new Object(); // 强引用 当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,...
  • 五、G1收集器(-XX:+UseG1GC... 以极高概率满足GC停顿时间要求的同时,还具备高吞吐性能特征(面对大容量内存的机器)。 G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。 ...
  • 33条:以弱引用避免循环引用 最简单的循环引用由两个对象构成,他们相互引用对方。如图所示: 这样的引用,导致谁也不会被释放掉。还有多个对象的循环应用,如图: 避免循环引用的最佳方式...
  • Java中的四种引用类型

    千次阅读 2017-05-15 17:29:49
    StrongReference(强引用),SoftReference(软引用),WeakReference(弱引用),PhantomReference(虚引用)。这四种引用的强度按照上面的顺序依次减弱.引用类型对比 序号 引用类型 取得目标对象方式 垃圾回收条
  • python的引用计数机制

    千次阅读 2018-11-06 20:17:26
    python内部使用引用计数,来...,即引用计数,当对象被创建时就创建了一个引用计数,当对象不再需要时,这个对象的引用计数为0时,它被垃圾回收。 总结一下对象会在一下情况下引用计数加1: 1.对象被创建:x=4 2...
  • C++对C语言的扩展_引用

    千次阅读 2019-11-20 20:42:08
    在上一篇文章中,我们讲述了c++对c语言的加强,在这篇文章中,我们开始学习c++对c语言的扩展_引用 引用 变量名 变量名实质上是一段连续存储空间的别名,是一个标号(门牌号). 通过变量来申请并命名内存空间 通过变量的...
  • yolo系列之yolo v3【深度解析】

    万次阅读 多人点赞 2018-09-12 16:24:48
    版权申明:转载和引用图片,都必须经过书面同意。获得留言同意即可 本文使用图片多为本人所画,需要高清图片可以留言联系我,先点赞后取图 这篇博文比较推荐的yolo v3代码是qwe的keras版本,复现比较容易,代码...
  • 1、强引用(StrongReference)    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下: ? 1 Object o=new Object(); // 强引用 ...
  • 1、强引用(StrongReference)    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下: ?  1 Object o=new Object(); // 强引用 当内存空间不足,Java虚拟
  • 采用文献计量法,统计了枟宝鸡文理学院学报(自然科学版)枠2000~2004年刊载论文的引文、引文... 33%和 36. 67%;期刊引用率为 61. 53%,专著为 25. 76%;普赖斯指数为40. 69%;引用文献的半衰期为6. 70年;自引率2. 31%。
  • java - 四种强弱软虚引用使用到的场景
  • 级深度学习网络——ESPNet v2

    千次阅读 2019-07-03 18:33:29
    作者介绍了一种轻、效率高、通用的卷积神经网络ESPNet v2,用于对可视化数据和顺序数据进行建模。相比前一代网络,v2使用逐点群卷积和深度空洞可分离卷积。作者在四个不同的任务上使用该网络进行测试包括对象分类...
  • 面渣逆袭:JVM经典五十问,这下面试稳了

    万次阅读 多人点赞 2021-12-28 21:42:52
    大家好,我是老三,“面渣逆袭“系列继续,这节我们来搞定JVM。... 引用计数算法 引用计数器的算法是这样的:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减...
  • 网页中引用svg图片的一种方式

    千次阅读 2019-07-02 15:07:12
    我们前端用的框架是ant design,ant design本身有提供svg图片引进方式,但是需要一些配置,详情看ant design中的Icon目录 这里记录的只适合少量图片,大量图片还是乖乖... fill : rgba(33,33,33,1); } } over!
  • 这一次,彻底解决Java的值传递和引用传递

    千次阅读 多人点赞 2018-10-23 00:17:30
    学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的话题又是充满争议:有的论坛帖子说Java只有值传递,...
  • C++中引用之常引用

    千次阅读 2016-02-17 22:54:23
    引用&分为普通引用和常引用。 知识架构: void main() { //普通引用 int a = 10; int &b = a; printf("b:%d \n", b); //常引用 int x = 20; const int &y = x; //常引用 是 让变量 引用只读属性 不能通过y...
  • //普通引用 引用一个字面 请问字面有没有内存地址  //引用 就是给内存取多个门牌号 (多个别名)  //printf("&40:%d \n", &40);  const int &m = 43; //c++编译器 会 分配内存空间 ==const int * const m =...
  • 这一次,让你彻底明白Java的值传递和引用传递!

    万次阅读 多人点赞 2018-10-29 08:39:00
    本文旨在用最通俗的语言讲述最枯燥的基本知识学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运...
  • 进程间通信(IPC,InterProcess ...IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。以Linux中的C语言编程为例。
  • 7、强引用、软引用、弱引用、虚引用是什么,有什么区别? 1、 强引用,就是普通的对象引用关系,如 String s = new String("ConstXiong") 2、 软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 117,019
精华内容 46,807
关键字:

引用量33