蓝胖子

蓝胖子 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

蓝胖子 赞了文章 · 1月18日

🔥 万字精美图文带你掌握JVM垃圾回收

本篇内容干货太多,耗费作者大量心力,强烈建议读者朋友们先收藏后观看😉🙇🙏

现在正值年底,估计有很多兄弟们在准备面试,希望本篇能给各位带来帮助。

转载请注明出处,原创不易!

前言

往期文章:

垃圾回收( Garbage Collection 以下简称 GC)诞生于1960年 MIT 的 Lisp 语言,有半个多世纪的历史。在Java 中,JVM 会对内存进行自动分配与回收,其中 GC 的主要作用就是清楚不再使用的对象,自动释放内存

GC 相关的研究者们主要是思考这3件事情。

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

本文也大致按照这个思路,为大家描述垃圾回收的相关知识。因为会有很多内存区域相关的知识,希望读者先学习完精美图文带你掌握 JVM 内存布局再来阅读本文。

在这里先感谢周志明大佬的新鲜出炉的大作:《深入理解Java 虚拟机》- 第3版

拜读之后对JVM有了更深的理解,强烈推荐大家去看。

本文的主要内容如下(建议大家在阅读和学习的时候,也大致按照以下的思路来思考和学习):

  • 哪些内存需要回收?即GC 发生的内存区域?
  • 如何判断这个对象需要回收?即GC 的存活标准

    这里又能够引出以下的知识概念:

    • 引用计数法
    • 可达性分析法
    • 引用的种类和特点、区别 (强引用、软引用、弱引用、虚引用)
    • 延伸知识:(WeakHashMap) (引用队列)
  • 有了对象的存活标准之后,我们就需要知道GC 的相关算法(思想)

    • 标记-清除(Mark-Sweep)算法
    • 复制(Copying)算法
    • 标记-整理(Mark-Compact)算法
  • 在下一步学习之前,还需要知道一些GC的术语📖,防止对一些概念描述出现混淆
  • 知道了算法之后,自然而然我们到了JVM中对这些算法的实现和应用,即各种垃圾收集器(Garbage Collector)

    • 串行收集器
    • 并行收集器
    • CMS 收集器
    • G1 收集器

一、GC 的 目标区域

一句话:GC 主要关注 堆和方法区

精美图文带你掌握 JVM 内存布局一文中,理解介绍了Java 运行时内存的分布区域和特点。

其中我们知道了程序计数器、虚拟机栈、本地方法栈3个区域是随线程而生,随线程而灭的。栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化,但在本章基于概念模型的讨论中,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。GC 关注的也就是这部分的内存区域。

image.png

二、GC 的存活标准

知道哪些区域的内存需要被回收之后,我们自然而然地想到了,如何去判断一个对象需要被回收呢?(回收对象...没对象的我听着怎么有点怪怪的😂)

对于如何判断对象是否可以回收,有两种比较经典的判断策略。

  • 引用计数算法
  • 可达性分析算法

1. 引用计数法

在对象头维护着一个 counter 计数器,对象被引用一次则计数器 +1;若引用失效则计数器 -1。当计数器为 0 时,就认为该对象无效了。

主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。发生循环引用的对象的引用计数永远不会为0,结果这些对象就永远不会被释放。

image.png

2. 可达性分析算法 ⭐

GC Roots 为起点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

Java 中,GC Roots 是指:

  • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象

image.png

3. Java 中的引用 ⭐

Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱

这样子设计的原因主要是为了描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

也就是说,对不同的引用类型,JVM 在进行GC 时会有着不同的执行策略。所以我们也需要去了解一下。

a. 强引用(Strong Reference)

MyClass obj = new MyClass(); // 强引用

obj = null // 此时‘obj’引用被设为null了,前面创建的'MyClass'对象就可以被回收了

只要强引用存在,垃圾收集器永远不会回收被引用的对象,只有当引用被设为null的时候,对象才会被回收。但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,会产生内存泄漏。

b. 软引用(Soft Reference)

软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

SoftReference<MyClass> softReference = new SoftReference<>(new MyClass());

c. 弱引用(Weak Reference)

弱引用的强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

WeakReference<MyClass> weakReference = new WeakReference<>(new MyClass());
弱引用可以引申出来一个知识点, WeakHashMap&ReferenceQueue

ReferenceQueue 是GC回调的知识点。这里因为篇幅原因就不细讲了,推荐引申阅读:ReferenceQueue的使用

d. 幻象引用/虚引用(Phantom References)

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

PhantomReference<MyClass> phantomReference = new PhantomReference<>(new MyClass(), new ReferenceQueue<>());

三、GC 算法 ⭐

有了判断对象是否存活的标准之后,我们再来了解一下GC的相关算法。

  • 标记-清除(Mark-Sweep)算法
  • 复制(Copying)算法
  • 标记-整理(Mark-Compact)算法

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

标记-清除算法在概念上是最简单最基础的垃圾处理算法。

该方法简单快速,但是缺点也很明显,一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

image.png

后续的收集算法都是基于这种思路并对其不足进行改进而得到的。

2. 复制(Copying)算法

复制算法改进了标记-清除算法的效率问题。

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点也是明显的,可用内存缩小到了原先的一半

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

在前面的文章中我们提到过,HotSpot默认的Eden:survivor1:survivor2=8:1:1,如下图所示。

延伸知识点:内存分配担保

当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)

内存的分配担保就好比我们去银行借款,如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

image.png

3. 标记-整理算法

前面说了复制算法主要用于回收新生代的对象,但是这个算法并不适用于老年代。因为老年代的对象存活率都较高(毕竟大多数都是经历了一次次GC千辛万苦熬过来的,身子骨很硬朗😎)

根据老年代的特点,提出了另外一种标记-整理(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

image.png

4. 分代收集算法

有没有注意到了,我们前面的表述当中就引入了新生代、老年代的概念。准确来说,是先有了分代收集算法的这种思想,才会将Java堆分为新生代和老年代。这两个概念之间存在着一个先后因果关系。

这个算法很简单,就是根据对象存活周期的不同,将内存分块。在Java 堆中,内存区域被分为了新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

就如我们在介绍上面的算法时描述的,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记—清理” 或者 “标记—整理” 算法 来进行回收。

  • 新生代:复制算法
  • 老年代:标记-清除算法、标记-整理算法

5. 重新回顾 创建对象时触发GC的流程

这里重新回顾一下精美图文带你掌握 JVM 内存布局里面JVM创建一个新对象的内存分配流程图。这张图也描述了GC的流程。

image.png

四、GC 术语 📖

在学习垃圾收集器知识点之前,需要向读者大大们科普一些GC的术语😊,方便你们后面理解。

  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

    • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
  • 并行(Parallel) :在JVM运行时,同时存在应用程序线程和垃圾收集器线程。 并行阶段是由多个GC 线程执行,即GC 工作在它们之间分配。
  • 串行(Serial):串行阶段仅在单个GC 线程上执行。
  • STW :Stop The World 阶段,应用程序线程被暂停,以便GC线程 执行其工作。 当应用程序因为GC 暂停时,这通常是由于Stop The World 阶段。
  • 并发(Concurrent):用户线程与垃圾收集器线程同时执行,不一定是并行执行,可能是交替执行(竞争)
  • 增量:如果一个阶段是增量的,那么它可以运行一段时间之后由于某些条件提前终止,例如需要执行更高优先级的GC 阶段,同时仍然完成生产性工作。 增量阶段与需要完全完成的阶段形成鲜明对比。

五、垃圾收集器 ⭐

知道了算法之后,自然而然我们到了JVM中对这些算法的实现和应用,即各种垃圾收集器(Garbage Collector)

首先要认识到的一个重要方面是,对于大多数JVM,需要两种不同的GC算法,一种用于清理新生代,另一种用于清理老年代

意思就是说,在JVM中你通常会看到两种收集器组合使用。下图是JVM 中所有的收集器(Java 8 ),其中有连线的就是可以组合的。

为了减小复杂性,快速记忆,我这边直接给出比较常用的几种组合。其他的要么是已经废弃了要么就是在现实情况下不实用的。

新生代老年代JVM options
SerialSerial Old-XX:+UseSerialGC
Parallel ScavengeParallel Old-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel NewCMS-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
G1G1-XX:+UseG1GC

接下去我们开始具体介绍上各个垃圾收集器。这里需要提一下的是,我这边是将垃圾收集器分成以下几类来讲述的:

  • Serial GC
  • Parallel GC
  • Concurrent Mark and Sweep (CMS)
  • G1 - Garbage First

理由无他,我觉得这样更符合理解的思路,你更好理解。

4.1 串行收集器

Serial 翻译过来可以理解成单线程。单线程收集器有Serial 和 Serial Old 两种,它们的唯一区别就是:Serial 工作在新生代,使用“复制”算法,Serial Old 工作在老年代,使用“标志-整理”算法。所以这里将它们放在一起讲。

串行收集器收集器是最经典、最基础,也是最好理解的。它们的特点就是单线程运行及独占式运行,因此会带来很不好的用户体验。虽然它的收集方式对程序的运行并不友好,但由于它的单线程执行特性,应用于单个CPU硬件平台的性能可以超过其他的并行或并发处理器。

“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(STW阶段)

image.png

STW 会带给用户恶劣的体验,所以从JDK 1.3开始,一直到现在最新的JDK 13,HotSpot虚拟机开发团队为消除或者降低用户线程因垃圾收集而导致停顿的努力一直持续进行着,从Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)和Garbage First(G1)收集器,最终至现在垃圾收集器的最前沿成果Shenandoah和ZGC等。

虽然新的收集器很多,但是串行收集器仍有其适合的场景。迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效。对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的,单线程没有线程交互开销。(这里实际上也是一个时间换空间的概念)

通过JVM参数 -XX:+UseSerialGC 可以使用串行垃圾回收器(上面表格也有说明)

4.2 并行收集器

按照程序发展的思路,单线程处理之后,下一步很自然就到了多核处理器时代,程序多线程并行处理的时代。并行收集器是多线程的收集器,在多核CPU下能够很好的提高收集性能。

image.png

这里我们会介绍:

  • ParNew
  • Parallel Scavenge
  • Parallel Old

这里还是提供太长不看版白话总结,方便理解。因为我知道有些人刚开始学习JVM 看这些名词都会觉得头晕。

  • ParNew收集器 就是 Serial收集器的多线程版本,基于“复制”算法,其他方面完全一样,在JDK9之后差不多退出历史舞台,只能配合CMS在JVM中发挥作用。
  • Parallel Scavenge 收集器 和 ParNew收集器类似,基于“复制”算法,但前者更关注可控制的吞吐量,并且能够通过-XX:+UseAdaptiveSizePolicy打开垃圾收集自适应调节策略的开关。
  • Parallel Old 就是 Parallel Scavenge 收集器的老年代版本,基于“标记-整理”算法实现。

a. ParNew 收集器

ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作

但是从G1 出来之后呢,ParNew的地位就变得微妙起来,自JDK 9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了『ParNew + Serial Old』 以及『Serial + CMS』这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了-XX:+UseParNewGC参数,这意味着ParNew 和CMS 从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。可以理解为从此以后,ParNew 合并入CMS,成为它专门处理新生代的组成部分。

b. Parallel Scavenge收集器

Parallel Scavenge收集器与ParNew收集器类似,也是使用复制算法的并行的多线程新生代收集器。但Parallel Scavenge收集器关注可控制的吞吐量(Throughput)

注:吞吐量是指CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /( 运行用户代码时间 + 垃圾收集时间 )

Parallel Scavenge收集器提供了几个参数用于精确控制吞吐量和停顿时间:

参数作用
--XX: MaxGCPauseMillis最大垃圾收集停顿时间,是一个大于0的毫秒数,收集器将回收时间尽量控制在这个设定值之内;但需要注意的是在同样的情况下,回收时间与回收次数是成反比的,回收时间越小,相应的回收次数就会增多。所以这个值并不是越小越好。
-XX: GCTimeRatio吞吐量大小,是一个(0, 100)之间的整数,表示垃圾收集时间占总时间的比率。
XX: +UseAdaptiveSizePolicy这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)

c. Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,多线程,基于“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的。

由于如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(Parallel Scavenge无法与CMS收集器配合工作),Parallel Old收集器的出现就是为了解决这个问题。Parallel Scavenge和Parallel Old收集器的组合更适用于注重吞吐量以及CPU资源敏感的场合

4.3 ⭐ Concurrent Mark and Sweep (CMS)

CMS(Concurrent Mark Sweep,并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

从名字就可以知道,CMS是基于“标记-清除”算法实现的。它的工作过程相对于上面几种收集器来说,就会复杂一点。整个过程分为以下四步:

1)初始标记 (CMS initial mark):主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。

2)并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞没有 STW

3)重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾

这里举一个很形象的例子:

就比如你和你的小伙伴(多个GC线程)给一条长走廊打算卫生,从一头打扫到另一头。当你们打扫到走廊另一头的时候,可能有同学(用户线程)丢了新的垃圾。所以,为了打扫干净走廊,需要你示意所有的同学(用户线程)别再丢了(进入STW阶段),然后你和小伙伴迅速把刚刚的新垃圾收走。当然,因为刚才已经收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。

4)并发清除(CMS concurrent sweep):

image.png

❔❔❔ 提问环节:为什么CMS要使用“标记-清除”算法呢?刚才我们不是提到过“标记-清除”算法,会留下很多内存碎片吗?

确实,但是也没办法,如果换成“标记 - 整理”算法,把垃圾清理后,剩下的对象也顺便整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了

对于上述的问题JVM提供了两个参数:

参数作用
--XX: +UseCMS-CompactAtFullCollection(默认是开启的,此参数从JDK 9开始废弃)用于在CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
--XX: CMSFullGCsBeforeCompaction(此参数从JDK 9开始废弃)这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)

另外,由于最后一步并发清除时,并不阻塞其它线程,所以还有一个副作用,在清理的过程中,仍然可能会有新垃圾对象产生,只能等到下一轮 GC,才会被清理掉

4.4 ⭐ G1 - Garbage First

JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。

鉴于 CMS 的一些不足之外,比如: 老年代内存碎片化,STW 时间虽然已经改善了很多,但是仍然有提升空间。G1 就横空出世了,它对于堆区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。具体什么意思呢,让我们继续看下去。

G1 将连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂

Region中还有一类特殊的Humongous区域,专门用来存储大对象G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中

Humongous,简称 H 区,是专用于存放超大对象的区域,通常 >= 1/2 Region SizeG1的大多数行为都把Humongous Region作为老年代的一部分来进行看待

image.png

认识了G1中的内存规划之后,我们就可以理解为什么它叫做"Garbage First"。所有的垃圾回收,都是基于 region 的。G1根据各个Region回收所获得的空间大小以及回收所需时间等指标在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大(垃圾)的Region,从而可以有计划地避免在整个Java堆中进行全区域的垃圾收集。这也是 "Garbage First" 得名的由来。

G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次GC。

❔❔❔ 提问环节:

一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?

这里就需要引入 Remembered Set 的概念了。

答案是不需要,每个 Region 都有一个 Remembered Set (记忆集)用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历

再提一个概念,Collection Set :简称 CSet,记录了等待回收的 Region 集合,GC 时这些 Region 中的对象会被回收(copied or moved)

G1 运作步骤

如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:

  • 初始标记(Initial Marking):Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
  • 并发标记(Concurrent Marking):使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。
  • 最终标记(Final Marking):Stop The World,使用多条标记线程并发执行。
  • 筛选回收(Live Data Counting and Evacuation):回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。(还会更新Region的统计数据,对各个Region的回收价值和成本进行排序)

image.png

从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量

G1 的 Minor GC/Young GC

在分配一般对象时,当所有eden region使用达到最大阈值并且无法申请足够内存时,会触发一次YGC。每次YGC会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。

image.png

下面是一段经过抽取的GC日志:

GC pause (G1 Evacuation Pause) (young)
  ├── Parallel Time
    ├── GC Worker Start
    ├── Ext Root Scanning
    ├── Update RS
    ├── Scan RS
    ├── Code Root Scanning
    ├── Object Copy
  ├── Code Root Fixup
  ├── Code Root Purge
  ├── Clear CT
  ├── Other
    ├── Choose CSet
    ├── Ref Proc
    ├── Ref Enq
    ├── Redirty Cards
    ├── Humongous Register
    ├── Humongous Reclaim
    ├── Free CSet  

由这段GC日志我们可知,整个YGC由多个子任务以及嵌套子任务组成,且一些核心任务为:Root Scanning,Update/Scan RS,Object Copy,CleanCT,Choose CSet,Ref Proc,Humongous Reclaim,Free CSet

推荐阅读:深入理解G1的GC日志

这篇文章通过G1 GC日志介绍了GC的几个步骤。对上面英文单词概念不清楚的可以查阅。

英文好的更推荐这篇:garbage-collection-algorithms-implementations

G1 的 Mixed GC

当越来越多的对象晋升到老年代Old Region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,是收集整个新生代以及部分老年代的垃圾收集。除了回收整个Young Region,还会回收一部分的Old Region ,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些Old Region 进行收集,从而可以对垃圾回收的耗时时间进行控制。

Mixed GC的整个子任务和YGC完全一样,只是回收的范围不一样。

image.png

注:G1 一般来说是没有FGC的概念的。因为它本身不提供FGC的功能。

如果 Mixed GC 仍然效果不理想,跟不上新对象分配内存的需求,会使用 Serial Old GC 进行 Full GC强制收集整个 Heap。

相比CMS,G1总结有以下优点:

  • G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行。
  • G1 能预测 GC 停顿时间, STW 时间可控(G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.)
关于G1实际上还有很多的细节可以讲,这里希望读者去阅读《深入理解Java虚拟机》或者其他资料来延伸学习,查漏补缺。

相关参数:

参数作用
-XX:+UseG1GC采用 G1 收集器
-XX:G1HeapRegionSize每个Region的大小
更多的参数和调优参考详见:分析和性能来调整和调优 G1 GC

后记

本系列关于JVM 垃圾回收的知识就到这里了。

因为篇幅的关系,也受限于能力水平,本文很多细节没有涉及到,只能算是为学习JVM的同学打开了一扇的门(一扇和平常看到的文章相比要大那么一点点的门,写了这么久允许我自恋一下吧😂😂)。希望不过瘾的同学能自己更加深入的学习。

如果本文有帮助到你,希望能点个赞,这是对我的最大动力🤝🤝🤗🤗。

参考

查看原文

赞 30 收藏 24 评论 7

蓝胖子 赞了文章 · 1月18日

🔥 精美图文带你掌握 JVM 内存布局

前言

JVM 系列:

本JVM系列属于本人学习过程当中总结的一些知识点,目的是想让读者更快地掌握JVM相关的知识要点,难免会有所侧重,若想要更加系统更加详细的学习JVM知识,还是需要去阅读专业的书籍和文档。

本文主题内容:

  • JVM 内存区域概览
  • 堆区的空间分配是怎么样?堆溢出的演示
  • 创建一个新对象内存是怎么分配的?
  • 方法区 到 Metaspace 元空间
  • 栈帧是什么?栈帧里有什么?怎么理解?
  • 本地方法栈
  • 程序计数器
  • Code Cache 是什么?
注:请 区分 JVM内存结构(内存布局) 和 JMM(Java内存模型)这两个不同的概念!

概览

内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略 ,保证了 JVM 的高效稳定运行。

image.png

上图描述了当前比较经典的JVM内存布局。(堆区画小了2333,按理来说应该是最大的区域)

如果按照线程是否共享来分类的话,如下图所示:

image.png

PS:线程是否共享这点,实际上理解了每块区域的实际用处之后,就很自然而然的就记住了。不需要死记硬背。

下面让我们来了解下各个区域。

一、Heap (堆区)

1.1 堆区的介绍

我们先来说堆。堆是 OOM故障最主要的发生区域。它是内存区域中最大的一块区域,被所有线程共享,存储着几乎所有的实例对象、数组。所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了

延伸知识点:JIT编译优化中的一部分内容 - 逃逸分析

推荐阅读:深入理解Java中的逃逸分析

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代。再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

1.2 堆区的调整

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以在运行时动态地调整。

如何调整呢?

通过设置如下参数,可以设定堆区的初始值和最大值,比如 -Xms256M -Xmx 1024M,其中 -X这个字母代表它是JVM运行时参数,msmemory start的简称,中文意思就是内存初始值,mxmemory max的简称,意思就是最大内存。

值得注意的是,在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,会形成不必要的系统压力 所以在线上生产环境中 JVM的XmsXmx会设置成同样大小,避免在GC 后调整堆大小时带来的额外压力。

1.3 堆的默认空间分配

另外,再强调一下堆空间内存分配的大体情况。

这里可能就会有人来问了,你从哪里知道的呢?如果我想配置这个比例,要怎么修改呢?

我先来告诉你怎么看虚拟机的默认配置。命令行上执行如下命令,就可以查看当前JDK版本所有默认的JVM参数。

java -XX:+PrintFlagsFinal -version

输出

对应的输出应该有几百行,我们这里去看和堆内存分配相关的两个参数

>java -XX:+PrintFlagsFinal -version
[Global flags]
    ...
    uintx InitialSurvivorRatio                      = 8
    uintx NewRatio                                  = 2
    ...
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

参数解释

参数作用
-XX:InitialSurvivorRatio新生代Eden/Survivor空间的初始比例
-XX:NewRatioOld区/Young区的内存比例

因为新生代是由Eden + S0 + S1组成的,所以按照上述默认比例,如果eden区内存大小是40M,那么两个survivor区就是5M,整个young区就是50M,然后可以算出Old区内存大小是100M,堆区总大小就是150M。

1.4 堆溢出 演示

/**
 * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 * @author Richard_Yi
 */
public class HeapOOMTest {

    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        List<byte[]> byteList = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            byte[] bytes = new byte[2 * _1MB];
            byteList.add(bytes);
        }
    }
}

输出

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid32372.hprof ...
Heap dump file created [7774077 bytes in 0.009 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at jvm.HeapOOMTest.main(HeapOOMTest.java:18)

-XX:+HeapDumpOnOutOfMemoryError 可以让JVM在遇到OOM异常时,输出堆内信息,特别是对相隔数月才出现的OOM异常尤为重要。

创建一个新对象 内存分配流程

看完上面对堆的介绍,我们趁热打铁再学习一下JVM创建一个新对象的内存分配流程。

image.png

绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发Young Garbage Collection,即YGC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区。Survivor区分为so和s1两块内存空间。每次YGC的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果YGC要移送的对象大于Survivor区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,就像人到了18岁就会成年一样,在JVM中-XX:MaxTenuringThreshold参数就是来配置一个对象从新生代晋升到老年代的阈值。默认值是15, 可以在Survivor区交换14次之后,晋升至老年代。

上述涉及到一部分垃圾回收的名词,不熟悉的读者可以查阅资料或者看下本系列的垃圾回收章节。

二、Metaspace 元空间

在 HotSpot JVM 中,永久代( ≈ 方法区)中用于存放类和方法的元数据以及常量池,比如ClassMethod。每当一个类初次被加载的时候,它的元数据都会放到永久代中。

永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即万恶的 java.lang.OutOfMemoryError: PermGen,为此我们不得不对虚拟机做调优。

那么,Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?(详见:JEP 122: Remove the Permanent Generation):

  1. 由于 PermGen 内存经常会溢出,引发恼人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM
  2. 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。

根据上面的各种原因,PermGen 最终被移除,方法区移至 Metaspace,字符串常量池移至堆区

准确来说,Perm 区中的字符串常量池被移到了堆内存中是在Java7 之后,Java 8 时,PermGen 被元空间代替,其他内容比如类元信息、字段、静态属性、方法、常量等都移动到元空间区。比如java/lang/Object 类元信息、静态属性System.out、整形常量 100000等。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。(和后面提到的直接内存一样,都是使用本地内存)

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.

对应的JVM调参:

参数作用
-XX:MetaspaceSize分配给Metaspace(以字节计)的初始大小
-XX:MaxMetaspaceSize分配给Metaspace 的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。
-XX:MinMetaspaceFreeRatio在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

延伸阅读:关于Metaspace比较好的两篇文章。

  1. Metaspace in Java 8
  2. http://lovestblog.cn/blog/201...

三、 Java 虚拟机栈

对于每一个线程,JVM 都会在线程被创建的时候,创建一个单独的栈。也就是说虚拟机栈的生命周期和线程是一致,并且是线程私有的。除了Native方法以外,Java方法都是通过Java 虚拟机栈来实现调用和执行过程的(需要程序技术器、堆、元空间内数据的配合)。所以Java虚拟机栈是虚拟机执行引擎的核心之一。而Java虚拟机栈中出栈入栈的元素就称为「栈帧」。

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

栈对应线程,栈帧对应方法

在活动线程中, 只有位于栈顶的帧才是有效的, 称为当前栈帧。正在执行的方法称为当前方法。在执行引擎运行时, 所有指令都只能针对当前栈帧进行操作。而StackOverflowError 表示请求的栈溢出, 导致内存耗尽, 通常出现在递归方法中。

虚拟机栈通过pop和push的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行的过程中,如果出现了异常,会进行异常回溯,返回地址通过异常处理表确定。

可以看出栈帧在整个JVM 体系中的地位颇高。下面也具体介绍一下栈帧中的存储信息。

1. 局部变量表

局部变量表就是存放方法参数和方法内部定义的局部变量的区域

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小

这里直接上代码,更好理解。

public int test(int a, int b) {
    Object obj = new Object();
    return a + b;
}

如果局部变量是Java的8种基本基本数据类型,则存在局部变量表中,如果是引用类型。如new出来的String,局部变量表中存的是引用,而实例在堆中。

2. 操作栈

操作数栈(Operand Stack)看名字可以知道是一个栈结构。Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。当JVM为方法创建栈帧的时候,在栈帧中为方法创建一个操作数栈,保证方法内指令可以完成工作。

还是用实操理解一下。

/**
 * @author Richard_yyf
 */
public class OperandStackTest {

    public int sum(int a, int b) {
        return a + b;
    }
}

编译生成.class文件之后,再反汇编查看汇编指令

> javac OperandStackTest.java
> javap -v OperandStackTest.class > 1.txt
  public int sum(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3 // 最大栈深度为2 局部变量个数为3
         0: iload_1 // 局部变量1 压栈
         1: iload_2 // 局部变量2 压栈
         2: iadd    // 栈顶两个元素相加,计算结果压栈
         3: ireturn
      LineNumberTable:
        line 10: 0

3. 动态连接

每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接

4. 方法返回地址

方法执行时有两种退出情况:

  • 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURNIRETURNARETURN
  • 异常退出

无论何种退出情况,都将返回至方法当前调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

  • 返回值压入上层调用栈帧
  • 异常信息抛给能够处理的栈帧
  • PC 计数器指向方法调用后的下一条指令
延伸阅读:JVM机器指令集图解

四、本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常

五、程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间。是线程私有的。它可以看作是当前线程所执行的字节码的行号指示器。什么意思呢?

白话版本:因为代码是在线程中运行的,线程有可能被挂起。即CPU一会执行线程A,线程A还没有执行完被挂起了,接着执行线程B,最后又来执行线程A了,CPU得知道执行线程A的哪一部分指令,线程计数器会告诉CPU。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,CPU 只有把数据装载到寄存器才能够运行。寄存器存储指令相关的现场信息,由于CPU 时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令

因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。此区域也不会发生内存溢出异常。

六、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。如果内存区域总和大于物理内存的限制,也会出现OOM。

Code Cache

简而言之, JVM代码缓存是JVM将其字节码存储为本机代码的区域 。我们将可执行本机代码的每个块称为 nmethod 。该 nmethod可能是一个完整的或内联Java方法。

实时(JIT)编译器是代码缓存区域的最大消费者。这就是为什么一些开发人员将此内存称为JIT代码缓存的原因。

这部分代码所占用的内存空间成为CodeCache区域。一般情况下我们是不会关心这部分区域的且大部分开发人员对这块区域也不熟悉。如果这块区域OOM了,在日志里面就会看到 java.lang.OutOfMemoryError code cache

诊断选项

选项默认值描述
PrintCodeCachefalse是否在JVM退出前打印CodeCache的使用情况
PrintCodeCacheOnCompilationfalse是否在每个方法被JIT编译后打印CodeCache区域的使用情况
延伸阅读 Introduction to JVM Code Cache

参考

  1. 《深入理解Java虚拟机》 - 周志明
  2. 《码出高效》
  3. Metaspace in Java 8
  4. JVM机器指令集图解
  5. Introduction to JVM Code Cache
如果本文有帮助到你,希望能点个赞,这是对我的最大动力。
查看原文

赞 103 收藏 75 评论 2

蓝胖子 关注了专栏 · 1月9日

Ric.Studio 进击的程序员笔记

学习记录分享。

关注 514

蓝胖子 关注了专栏 · 1月9日

阿里云栖号

汇集阿里技术精粹-yq.aliyun.com

关注 11821

蓝胖子 关注了标签 · 1月9日

程序员

一种近几十年来出现的新物种,是工业革命的产物。英文(Programmer Monkey)是一种非常特殊的、可以从事程序开发、维护的动物。一般分为程序设计猿和程序编码猿,但两者的界限并不非常清楚,都可以进行开发、维护工作,特别是在中国,而且最重要的一点,二者都是一种非常悲剧的存在。

国外的程序员节

国外的程序员节,(英语:Programmer Day,俄语:День программи́ста)是一个俄罗斯官方节日,日期是每年的第 256(0x100) 天,也就是平年的 9 月 13 日和闰年的 9 月 12 日,选择 256 是因为它是 2 的 8 次方,比 365 少的 2 的最大幂。

1024程序员节,中国程序员节

1024是2的十次方,二进制计数的基本计量单位之一。程序员(英文Programmer)是从事程序开发、维护的专业人员。程序员就像是一个个1024,以最低调、踏实、核心的功能模块搭建起这个科技世界。1GB=1024M,而1GB与1级谐音,也有一级棒的意思。

从2012年,SegmentFault 创办开始我们就从网络上引导社区的开发者,发展成中国程序员的节日 :) 计划以后每年10月24日定义为程序员节。以一个节日的形式,向通过Coding 改变世界,也以实际行动在浮躁的世界里,固执地坚持自己对于知识、技术和创新追求的程序员们表示致敬。并于之后的最为临近的周末为程序员们举行了一个盛大的狂欢派对。

2015的10月24日,我们SegmentFault 也在5个城市同时举办黑客马拉松这个特殊的形式,聚集开发者开一个编程大爬梯。

特别推荐:

【SF 黑客马拉松】:http://segmentfault.com/hacka...
【1024程序员闯关秀】小游戏,欢迎来挑战 http://segmentfault.com/game/

  • SF 开发者交流群:206236214
  • 黑客马拉松交流群:280915731
  • 开源硬件交流群:372308136
  • Android 开发者交流群:207895295
  • iOS 开发者交流群:372279630
  • 前端开发者群:174851511

欢迎开发者加入~

交流群信息


程序员相关问题集锦:

  1. 《程序员如何选择自己的第二语言》
  2. 《如何成为一名专业的程序员?》
  3. 《如何用各种编程语言书写hello world》
  4. 《程序员们最常说的谎话是什么?》
  5. 《怎么加入一个开源项目?》
  6. 《是要精于单挑,还是要善于合作?》
  7. 《来秀一下你屎一般的代码...》
  8. 《如何区分 IT 青年的“普通/文艺/二逼”属性?》
  9. 程序员必读书籍有哪些?
  10. 你经常访问的技术社区或者技术博客(IT类)有哪些?
  11. 如何一行代码弄崩你的程序?我先来一发
  12. 编程基础指的是什么?
  13. 后端零起步:学哪一种比较好?
  14. 大家都用什么键盘写代码的?

爱因斯坦

程序猿崛起

关注 113591

蓝胖子 关注了标签 · 1月9日

mysql

MySQL是一个小型关系型数据库管理系统,开发者为瑞典MySQL AB公司。在2008年1月16号被Sun公司收购。而2009年,SUN又被Oracle收购。MySQL是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内。这样就增加了速度并提高了灵活性。MySQL的SQL“结构化查询语言”。SQL是用于访问数据库的最常用标准化语言。MySQL软件采用了GPL(GNU通用公共许可证)。由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,许多中小型网站为了降低网站总体拥有成本而选择了MySQL作为网站数据库。

关注 64267

蓝胖子 关注了标签 · 1月9日

java

Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaSE, JavaEE, JavaME)的总称。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

Java编程语言的风格十分接近 C++ 语言。继承了 C++ 语言面向对象技术的核心,Java舍弃了 C++ 语言中容易引起错误的指針,改以引用取代,同时卸载原 C++ 与原来运算符重载,也卸载多重继承特性,改用接口取代,增加垃圾回收器功能。在 Java SE 1.5 版本中引入了泛型编程、类型安全的枚举、不定长参数和自动装/拆箱特性。太阳微系统对 Java 语言的解释是:“Java编程语言是个简单、面向对象、分布式、解释性、健壮、安全与系统无关、可移植、高性能、多线程和动态的语言”。

版本历史

重要版本号版本代号发布日期
JDK 1.01996 年 1 月 23 日
JDK 1.11997 年 2 月 19 日
J2SE 1.2Playground1998 年 12 月 8 日
J2SE 1.3Kestrel2000 年 5 月 8 日
J2SE 1.4Merlin2002 年 2 月 6 日
J2SE 5.0 (1.5.0)Tiger2004 年 9 月 30 日
Java SE 6Mustang2006 年 11 月 11 日
Java SE 7Dolphin2011 年 7 月 28 日
Java SE 8JSR 3372014 年 3 月 18 日
最新发布的稳定版本:
Java Standard Edition 8 Update 11 (1.8.0_11) - (July 15, 2014)
Java Standard Edition 7 Update 65 (1.7.0_65) - (July 15, 2014)

更详细的版本更新查看 J2SE Code NamesJava version history 维基页面

新手帮助

不知道如何开始写你的第一个 Java 程序?查看 Oracle 的 Java 上手文档

在你遇到问题提问之前,可以先在站内搜索一下关键词,看是否已经存在你想提问的内容。

命名规范

Java 程序应遵循以下的 命名规则,以增加可读性,同时降低偶然误差的概率。遵循这些命名规范,可以让别人更容易理解你的代码。

  • 类型名(类,接口,枚举等)应以大写字母开始,同时大写化后续每个单词的首字母。例如:StringThreadLocaland NullPointerException。这就是著名的帕斯卡命名法。
  • 方法名 应该是驼峰式,即以小写字母开头,同时大写化后续每个单词的首字母。例如:indexOfprintStackTraceinterrupt
  • 字段名 同样是驼峰式,和方法名一样。
  • 常量表达式的名称static final 不可变对象)应该全大写,同时用下划线分隔每个单词。例如:YELLOWDO_NOTHING_ON_CLOSE。这个规范也适用于一个枚举类的值。然而,static final 引用的非不可变对象应该是驼峰式。

Hello World

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

编译并调用:

javac -d . HelloWorld.java
java -cp . HelloWorld

Java 的源代码会被编译成可被 Java 命令执行的中间形式(用于 Java 虚拟机的字节代码指令)。

可用的 IDE

学习资源

常见的问题

下面是一些 SegmentFault 上在 Java 方面经常被人问到的问题:

(待补充)

关注 107911

蓝胖子 发布了文章 · 1月8日

Docker buildx构建多平台镜像并推送到私有仓库

引子

最近发现有ARM版Docker,hub.docker.com上也有ARM版本的镜像,但是ARM版本的Docker镜像构建是个问题。嵌入式程序可以在PC机上进行交叉编译,不知道Docker是否有交叉构建的方案。

方案

目前想到的Docker构建ARM镜像方法有如下几种。第三种就类似交叉编译。

  1. 使用ARM主机,安装ARM版本的Docker,docker build出来的就是ARM版本的镜像。
  2. 使用Linux的虚拟化软件,模拟ARM芯片+ Linux,例如qemu。
  3. 使用Docker试验功能buildx,可以构建多平台的镜像。

使用Docker buildx构建多个平台镜像

参考如下几个链接。
https://docs.docker.com/engine/reference/commandline/manifest/
https://docs.docker.com/buildx/working-with-buildx/
https://engineering.docker.com/2019/06/getting-started-with-docker-for-arm-on-linux/
用到了两个docker的试验功能,使用时需要开启试验功能。
docker manifest,manifest是一个包含了镜像信息的文件。manifest list是一个镜像清单列表,用于存放不同os/arch的镜像信息。我们可以创建一个manifest list来指向两个镜像,然后可以支持多平台。
docker buildx,buildx是docker的一个插件,是下一代docker镜像构建。该插件通过qemu-user-static翻译不同平台的指令集,达到在x64上运行其他平台的程序。buildx实际使用了moby/buildkit:buildx-stable-1镜像进行多平台构建。

搭建docker registry多平台版本

参考如下链接,构建docker registry镜像。
https://community.arm.com/developer/tools-software/tools/b/tools-software-ides-blog/posts/deploying-multi-architecture-docker-registry

搭建dns服务器,解决buildx bug

buildx插件不走本地hosts文件,必须走dns。这是个bug,https://github.com/docker/buildx/issues/218,社区也没人管。
解决方法:自建dns,把镜像的地址buildx.com指向registry的机器,后续用nginx。ubuntu有一个默认systemd-resolved,关闭之后在开启dnsmasq。

使用nginx代理解决命名问题

增加nginx代理同时支持HTTP和HTTPS。buildx这个插件强行使用了HTTPS,没有找到关闭的地方。
提示证书问题,证书不是这个域名的,解决方法: 重新生成一个证书,域名填自己的。
证书问题,不信任自签名证书,把自签名的证书加到buildx daemon容器的证书信任链中。https://github.com/docker/bui...
nginx增加两个配置,解决客户端push时的几个问题。

# nignx.conf 配置
proxy_ignore_client_abort on; #忽略客户端告警
client_max_body_size 0;   #上传文件大小不限制


# 虚拟主机配置
server {
    listen 443;
    server_name buildx.com;
    ssl on;
    ssl_certificate crt/server.crt;
    ssl_certificate_key crt/server.key;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置
    ssl_prefer_server_ciphers on;
    location / {
        proxy_pass http://192.168.1.11:81;
    }
}

server {
    listen  80;
    server_name buildx.com;
    location / {
        proxy_pass http://192.168.1.11:81;
    }
}

设置本地Docker环境

本地Docker需要开启实验功能。

  1. 在/etc/docker/daemon.json中配置 "experimental": true,重启Docker。开启Docker daemon的实验功能。
  2. 在本地执行export DOCKER_CLI_EXPERIMENTAL=enabled,开启Docker Client的实验功能。
  3. 使用docker version查看实验功能是否开启。
  4. 执行docker run --rm --privileged docker/binfmt:820fdd95a9972a5308930a2bdfb8573dd4447ad3,开启内核binfmt_misc功能,可以在当前平台上执行多平台的程序。
  5. 查看是否支持aarch64程序。cat /proc/sys/fs/binfmt_misc/qemu-aarch64
  6. 此时本地的docker可以运行各种平台的docker容器。比如arm64。可以使用如下命令测试。
# 拉取arm64版本镜像并运行
docker pull --platform arm64 alpine:3.10
docker run --rm -it alpine:3.10 sh

制作基础镜像

可以从hub.docker.com中获取多个平台的版本,生成manifest list,上传的registry中。

# pull arm64版本、改名、上传。  具体镜像是否支持多平台,可以到hub.docker.com上看。
docker pull --platform arm64 centos:7
docker tag centos:7 buildx.com/base/centos-arm64:7
docker push buildx.com/base/centos-arm64:7
# pull amd64版本、改名、上传
docker pull --platform amd64 centos:7
docker tag centos:7 buildx.com/base/centos-amd64:7
docker push buildx.com/base/centos-amd64:7
# 创建manifest list、上传。
docker manifest create --insecure buildx.com/base/centos:7 buildx.com/base/centos-amd64:7 buildx.com/base/centos-arm64:7
docker manifest push --insecure buildx.com/base/centos:7

构建业务镜像

# buildx 可以指定多个平台,但是要求Dockerfile中的FROM镜像必须有对应版本的。
# buildx 打包的镜像不会在本地存储,加--push,上传docker仓。或者可以使用--output指定输出方式。
docker buildx build --platform linux/amd64,linux/arm64 -t buildx.com/base/java-base:openjdk-8-centos7 . --push
查看原文

赞 0 收藏 0 评论 0

蓝胖子 回答了问题 · 1月5日

nginx代理swoole之后,用任何浏览器访问session_id都一样

sessionid一样,感觉应该是代理或者缓存的问题,可以贴一下nginx配置看看。

关注 4 回答 4

蓝胖子 发布了文章 · 1月5日

使用Ambari搭建HDP版本Hadoop3集群

一、官方安装文档

Ambari安装文档
HDP集群安装文档

二、搭建yum源

参考网上教程,安装httpd,把下面安装包压后放到/var/www/html目录下。重启httpd。
访问http://127.0.0.1:80,看是否正常。

#下载ambari安装包和repo文件
wget http://public-repo-1.hortonworks.com/ambari/centos7/2.x/updates/2.7.3.0/ambari.repo
wget http://public-repo-1.hortonworks.com/ambari/centos7/2.x/updates/2.7.3.0/ambari-2.7.3.0-centos7.tar.gz
#下载HDP安装包和repo文件
wget http://public-repo-1.hortonworks.com/HDP/centos7/3.x/updates/3.1.0.0/HDP-3.1.0.0-centos7-rpm.tar.gz
wget http://public-repo-1.hortonworks.com/HDP/centos7/3.x/updates/3.1.0.0/hdp.repo
wget http://public-repo-1.hortonworks.com/HDP-UTILS-1.1.0.22/repos/centos7/HDP-UTILS-1.1.0.22-centos7.tar.gz
wget http://public-repo-1.hortonworks.com/HDP-GPL/centos7/3.x/updates/3.1.0.0/HDP-GPL-3.1.0.0-centos7-gpl.tar.gz
wget http://public-repo-1.hortonworks.com/HDP-GPL/centos7/3.x/updates/3.1.0.0/hdp.gpl.repo

三、各个机器配置

所有机器配置/etc/hosts,内容为所有机器和自定义的域名。
#检查hosts配置是否正常
cat /etc/hosts
各个机器配置对应的主机名。
#设计主机名
hostname host1.hdp.com
#检查主机名是否设置成功
hostname -f
各个机器配置dns,/etc/resolv.conf。
各个机器安装openjdk。
#安装openjdk
yum install java-1.8.0-openjdk  java-1.8.0-openjdk-devel
#增加环境变量,可以增加到其他配置文件中,例如~/.bashrc
echo JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.51.x86_64 >> /etc/profile
source /etc/profile
第一台机器上生成ssh key,拷贝到其他机器上,用作ssh免密登录。
#生成ssh key
ssh-keygen
#拷贝到其他的机器
ssh-copy-id root@otherXXXXX.hdp.com
第一台机器配置ambari.repo,用于安装Ambari Server。
所有机器配置hdp.repo、hdp-gpl。hdp-utils.repo。

四、集群安装

第一台机器安装、配置、启动ambari server。安装mysql驱动。master安装mysql-connect-java解决ambari error
yum install ambari-server
#安装mysql驱动
yum install mysql-connector-java*
ls -al /usr/share/java/mysql-connector-java.jar
cd /var/lib/ambari-server/resources/
#链接到ambari server的目录中,ambari server会用到
ln -s /usr/share/java/mysql-connector-java.jar mysql-connector-java.jar
# 配置ambari server,使用自带mysql
ambari-server setup
ambari-server restart
浏览器登录ambari server。http://host1.hdp.com:8080,默认用户名密码admin/admin。
在ambari安装步骤中配置机器信息、private.key(之前ssh-keygen生成的~/.ssh/id_rsa)、部署信息。

五、可能的错误

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 1 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-08-21
个人主页被 209 人浏览