JVM内存模型
根据Java虚拟机规范,Java数据区域分为五大数据区域。javascript
其中方法区和堆是全部线程共享的,虚拟机栈、本地方法栈和程序计数器则为线程私有的。java
有的博客称方法区是永久代,那是由于前者是JVM的规范,然后者则是JVM规范的一种实现,而且只有HotSpot才有永久代,web
JDK8中已经完全移除了方法区,JDK8中引入了一个新的内存区域叫metaspace(元空间),后边详细介绍。算法
栈区
栈分为虚拟机栈和本地方法栈。typescript
虚拟机栈
每一个方法执行都会建立一个栈帧,用于存放局部变量表,操做栈,动态连接,方法出口等。每一个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。
一般说的栈就是指局部变量表部分,存放编译期间可知的8种基本数据类型及对象引用和指令地址。局部变量表是在编译期间完成分配,当进入一个方法时,这个栈中的局部变量分配内存大小是肯定的。数组
常见的的两种异常StackOverFlowError和OutOfMemoneyError。当线程请求栈深度大于虚拟机所容许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展没法申请到足够的内存空间时候,抛出OutOfMemoneyError。缓存
本地方法栈
本地方法栈与虚拟机栈的做用十分相似,不过本地方法是为native方法服务的。部分虚拟机(好比Sun HotSpot虚拟机)直接将本地方法栈与虚拟机栈合二为一。与虚拟机栈同样,本地方法栈也会抛出StactOverflowError与OutOfMemoryError异常。服务器
程序计数器
当前线程所执行的行号指示器。经过改变计数器的值来肯定下一条指令,好比循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。微信
Java虚拟机多线程是经过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都须要一个独立的程序计数器,因此它是线程私有的。多线程
惟一一块Java虚拟机没有规定任何OutofMemoryError的区块。
方法区
方法区/永久代是被全部线程共享区域,用于存放已被虚拟机加载的类信息、常量、静态变量等数据。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。
在JDK1.7以前运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代
在JDK1.7字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之,这时候字符串常量池还在堆, 运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)
移除永久代的影响
永久代在JDK8中被删除,被一个叫作元空间的区域所替代了。这项改动是颇有必要的,由于对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。而且为永久代设置空间大小也是很难肯定的,由于这其中有不少影响因素,好比类的总数,常量池的大小和方法数量等。
默认状况下,元空间的最大可分配空间就是系统可用内存空间。所以,咱们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户能够为元空间设置一个可用空间最大值,若是不进行设置,JVM会自动根据类的元数据大小动态增长元空间的容量。
注意:永久代的移除并不表明自定义的类加载器泄露问题就解决了。还必须监控内存消耗状况,由于一旦发生泄漏,会占用大量的本地内存,
堆区
堆被全部线程共享区域,在虚拟机启动时建立,惟一目的是存放对象实例。
堆区是垃圾回收的主要区域,一般状况下分为两个区块年轻代和老年代。年轻代又分为Eden区(存放新建立对象),From survivor区和To survivor区(两个survivor区保存gc后幸存下的对象)。默认状况下各自占比 8:1:1。
java虚拟机规范对这块的描述是:全部对象实例及数组都要在堆上分配内存,但随着逃逸分析技术的成熟,这个说法也不是那么绝对,可是大多数状况都是这样的。
逃逸分析:经过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,若是开启了逃逸分析,便可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针能够被全局所引用,或者被其它线程所引用。
在JVM运行时,能够经过配置如下参数改变整个JVM堆的配置比例
JVM运行时堆的大小
-Xms,堆的最小值
-Xmx,堆空间的最大值新生代堆空间大小调整
-XX:NewSize新生代的最小值
-XX:MaxNewSize,新生代的最大值
-XX:NewRatio,设置新生代与老年代在堆空间的大小
-XX:SurvivorRatio,新生代中Eden所占区域的大小永久代大小调整
-XX:MaxPermSize 4.其余
-XX:MaxTenuringThreshold,设置将新生代对象转到老年代时须要通过多少次垃圾回收,可是仍然没有被回收
OutOfMemoryError报错及解决方法
java.lang.OutOfMemoryError:java heap space
这种是java堆内存不够,一个缘由是内存真不够,另外一个缘由是程序中有死循环。若是是java堆内存不够的话,能够经过调整JVM下面的配置来解决:-Xms
、-Xmx
java.lang.OutOfMemoryError:GC overhead limit exceeded
这是JDK6新增错误类型,当GC为释放很小空间占用大量时间时抛出;通常是由于堆过小,致使异常的缘由,没有足够的内存。解决方案:查看系统是否有使用大内存的代码或死循环;
经过添加JVM配置,来限制使用内存:-XX:-UseGCOverheadLimit
java.lang.OutOfMemoryError: PermGen space
这一部分用于存放Class和Meta的信息,Class在被Load的时候被放入PermGen space区域。因此若是你的APP会LOAD不少CLASS的话,就极可能出现PermGen space错误。这种是永久代内存不够,可经过调整JVM的配置:-XX:MaxPermSize
、-XXermSize
java.lang.OutOfMemoryError: Direct buffer memory
可能缘由是自己资源不够或者申请的太多内存。若是不是内存泄漏的话,可使用参数-XX:MaxDirectMemorySize
参数,或者-XX:MaxDirectMemorySize
java.lang.OutOfMemoryError: unable to create new native thread
可能缘由是系统内存耗尽,没法为新线程分配内存或者建立线程数超过了操做系统的限制。经过两个途径解决:排查应用是否建立了过多的线程。经过jstack肯定应用建立了多少线程
调整操做系统线程数阈值。操做系统会限制进程容许建立的线程数,使用ulimit -u命令查看限制。某些服务器上此阈值设置的太小,好比1024。一旦应用建立超过1024个线程,就会遇到java.lang.OutOfMemoryError: unable to create new native thread问题。若是是这种状况,能够调大操做系统线程数阈值。
增长机器内存。若是上述两项未能排除问题,多是正常增加的业务确实须要更多内存来建立更多线程。若是是这种状况,增长机器内存。
减少堆内存。一个老司机也常常忽略的很是重要的知识点:线程不在堆内存上建立,线程在堆内存以外的内存上建立。因此若是分配了堆内存以后只剩下不多的可用内存,依然可能遇到java.lang.OutOfMemoryError: unable to create new native thread。考虑以下场景:系统总内存6G,堆内存分配了5G,永久代512M。在这种状况下,JVM占用了5.5G内存,系统进程、其余用户进程和线程将共用剩下的0.5G内存,颇有可能没有足够的可用内存建立新的线程。若是是这种状况,考虑减少堆内存。
减少线程栈大小。线程会占用内存,若是每一个线程都占用更多内存,总体上将消耗更多的内存。每一个线程默认占用内存大小取决于JVM实现。能够利用-Xss参数限制线程内存大小,下降总内存消耗。例如,JVM默认每一个线程占用1M内存,应用有500个线程,那么将消耗500M内存空间。若是实际上256K内存足够线程正常运行,配置-Xss256k,那么500个线程将只须要消耗125M内存。(注意,若是-Xss设置的太低,将会产生java.lang.StackOverflowError错误)。
java.lang.StackOverflowError
这也内存溢出错误的一种,即线程栈的溢出,要么是方法调用层次过多(好比存在无限递归调用),要么是线程栈过小。能够经过优化程序设计,减小方法调用层次;调整-Xss
参数增长线程栈大小。
垃圾回收算法
新生代采用复制算法。老年代采用标记/清除算法或标记/整理算法。因为老年代存活率高,没有额外空间给他作担保,必须使用这两种算法。
标记-清除(Mark Sweep)算法
算法分为2个阶段:
标记处须要回收的对象
回收被标记的对象
标记算法分为两种:
引用计数算法(Reference Counting)
可达性分析算法(Reachability Analysis)
因为引用技术算法没法解决循环引用的问题,因此这里使用的标记算法均为可达性分析算法。下文将介绍两种标记算法。
如图所示,当进行过标记清除算法以后,出现了大量的非连续内存。当java堆须要分配一段连续的内存给一个新对象时,发现虽然内存清理出了不少的空闲,可是仍然须要继续清理以知足“连续空间”的要求。因此说,这种方法比较基础,效率也比较低下。
复制(Copying)算法
为了解决效率与内存碎片问题,复制(Copying)算法出现了,它将内存划分为两块相等的大小,每次使用一块,当这一块用完了,就将还存活的对象复制到另一块内存区域中,而后将当前内存空间一次性清理掉。这样的对整个半区进行回收,分配时按照顺序从内存顶端依次分配,这种实现简单,运行高效。不过这种算法将原有的内存空间减小为实际的一半,代价比较高。
从图中能够看出,整理后的内存十分规整,可是白白浪费通常的内存成本过高。然而这实际上是很重要的一个收集算法,由于如今的商业虚拟机都采用这种算法来回收新生代。IBM公司的专门研究代表,新生代中的对象98%都是“朝生夕死”的,因此不须要按照1:1的比例来划份内存。HotSpot虚拟机将Java堆划分为年轻代(Young Generation)、老年代(Tenured Generation),其中年轻代又分为一块Eden和两块Survivor。
全部的新建对象都放在年轻代中,年轻代使用的GC算法就是复制算法。其中Eden与Survivor的内存大小比例为8:2,其中Eden由1大块组成,Survivor由2小块组成。每次使用内存为1Eden+1Survivor,即90%的内存。因为年轻代中的对象生命周期每每很短,因此当须要进行GC的时候就将当前90%中存活的对象复制到另一块Survivor中,原来的Eden与Survivor将被清空。可是这就有一个问题,咱们没法保证每次年轻代GC后存活的对象都不高于10%。因此在当活下来的对象高于10%的时候,这部分对象将由Tenured进行担保,即没法复制到Survivor中的对象将移动到老年代。
标记-整理算法
复制算法在极端状况下(存活对象较多)效率变得很低,而且须要有额外的空间进行分配担保。因此在老年代中这种状况通常是不适合的。
因此就出现了标记-整理(Mark-Compact)算法。与标记清除算法同样,首先是标记对象,然而第二步是将存货的对象向内存一段移动,整理出一块较大的连续内存空间。
垃圾回收的几种形式
Minor GC
在年轻代(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC。Minor GC当年轻代中eden区分配满的时候触发,只会清理年轻代。通过此次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。无论怎样,都会保证名为To的Survivor区域是空的。
Full GC
full gc是收集整个堆,包括young gen、old gen、perm gen(若是存在的话)、元空间(1.8及以上)等全部部分的模式。
手动调用System.gc()方法 [增长了full GC频率,不建议使用而是让jvm本身管理内存,能够设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc]
发现perm gen(若是存在永久代的话)需分配空间但已经没有足够空间
老年代空间不足,好比说新生代的大对象大数组晋升到老年代就可能致使老年代空间不足。
mixed GC(G1特有)
混合GC 收集整个young gen以及部分old gen的GC。只有G1有这个模式
垃圾回收的两种断定方法
1. 引用计数算法
在JDK1.2以前,使用的是引用计数器算法,即当这个类被加载到内存以后,就会产生方法区,堆栈、程序计数器等一系列信息,当建立对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用时,引用计数器继续+1,而当其中一个引用销毁时,引用计数器-1,当引用计数器减为0的时候,标志着这个对象已经没有引用了,能够回收了!可是这样会有一个问题:当咱们的代码出现这样的状况时:
ObjA.obj=ObjB
ObjB.obj=ObjA
这样的代码会产生以下引用情形ObjA指向ObjB,而ObjB又指向objA,这样当其余全部的引用都消失了以后,ObjA和ObjB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已是垃圾了。
2.可达性分析算法
可达性分析算法是从离散数学中的图论引入的,程序把全部的引用关系看作一张图,从一个节点GC Root开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当全部的引用节点寻找完毕以后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
目前Java中可做为GC Root的对象有:
虚拟机栈中引用的对象(本地变量表)
方法区中静态属性引用的对象
方法区中常量引用的对象(final的常量值)
本地方法栈中引用的对象(Native对象)。
java中存在的四种引用
强引用:只要引用存在,垃圾回收器永远不会回收。
软引用:非必须引用,内存溢出以前进行回收。代码示例:
Object obj=new Object();SoftReference<Object> sf=new SoftRerence<Object>(obj);obj=null;sf.get();//有时会返回null
这时候sf是对obj的一个软引用,经过sf.get()方法能够取到这个对象,固然这个对象被标记为须要回收的对象时,则返回null;
软引用主要用于用户实现相似缓存的功能,在内存不足的状况下直接经过软引用取值,无需从繁忙的真实来源查询数据,提高速度;当内存不足时,自动删除这部分缓存数据,从真实的来源查询这些数据。
弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,不管内存是否充足,都会回收被弱引用关联的对象。能够经过以下代码实现
Object obj=new Object();WeakReference<Object> wf=new WeakReference<Object>(obj);obj=null;wf.get();//有时会返回nullwf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短期内经过弱引用取对应的数据,能够取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被标记为即将回收的垃圾,能够经过弱引用的isEnQueues方法返回对象是否被垃圾回收器标记。
虚引用:虚引用和前面的软引用、弱引用不一样,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。若是一个对象与虚引用关联,则跟没有引用与之关联同样,在任什么时候候均可能被垃圾回收器回收。
要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,若是发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序能够经过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。若是程序发现某个虚引用已经被加入到引用队列,那么就能够在所引用的对象的内存被回收以前采起必要的行动。能够经过以下代码实现
import java.lang.ref.ReferenceQueue; public class Main { public static void main(String[] args) { ReferenceQueue<String> queue = new ReferenceQueue<String>(); PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue); System.out.println(pr.get()); } }
垃圾收集器
若是说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。jvm会结合针对不一样的场景及用户的配置使用不一样的收集器。
年轻代收集器:
Serial、ParNew、Parallel Scavenge
老年代收集器:
Serial Old、Parallel Old、CMS收集器
特殊收集器:
G1收集器(新型,不在年轻、老年代范畴内)
年轻代收集器
Serial
最基本、发展最久的收集器,在jdk3之前是gc收集器的惟一选择,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工做,在收集的时候必须得停掉其它线程,等待收集工做完成其它线程才能够继续工做。
虽然Serial看起来很坑,需停掉别的线程以完成本身的gc工做,可是也不是彻底没用的,好比说Serial在运行在Client模式下优于其它收集器(简单高效,不过通常都是用Server模式,64bit的jvm甚至没Client模式)
串行收集器组合 Serial + Serial Old
优势:对于Client模式下的jvm来讲是个好的选择。适用于单核CPU(如今基本都是多核了) 缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。
ParNew收集器
能够认为是Serial的升级版,由于它支持多线程[GC线程],并且收集算法、Stop The World、回收策略和Serial同样,就是能够有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同,若是cpu核数不少不想用那么多,能够经过-XX:ParallelGCThreads来控制垃圾收集线程的数量。
优势:
支持多线程,多核CPU下能够充分的利用CPU资源
运行在Server模式下新生代首选的收集器【重点是由于新生代的这几个收集器只有它和Serial能够配合CMS收集器一块儿使用】
缺点: 在单核下表现不会比Serial好,因为在单核能利用多核的优点,在线程收集过程当中可能会出现频繁上下文切换,致使额外的开销。
Parallel Scavenge
采用复制算法的收集器,和ParNew同样支持多线程。 可是该收集器重点关心的是吞吐量(吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 若是代码运行100min垃圾收集1min,则为99%) 对于用户界面,适合使用GC停顿时间短,否则由于卡顿致使交互界面卡顿将很影响用户体验。 对于后台高吞吐量能够高效率的利用cpu尽快完成程序运算任务,适合后台运算
并行收集器组合 Parallel Scavenge + Parallel Old
Parallel Scavenge注重吞吐量,因此也成为"吞吐量优先"收集器。
JDK7和8中,做为年轻代默认的收集器
老年代收集器
Serial Old
和新生代的Serial同样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。 若是是Server模式有两大用途:
jdk5前和Parallel Scavenge搭配使用,jdk5前也只有这个老年代收集器能够和它搭配。
做为CMS收集器的后备。
Parallel Old
支持多线程,Parallel Scavenge的老年版本,jdk6开始出现,采用"标记-整理算法"。 在jdk6之前,新生代的Parallel Scavenge只能和Serial Old配合使用,并且Serial Old为单线程Server模式下没法充分利用多核cpu,这种结合并不能让应用的吞吐量最大化。
Parallel Old的出现结合Parallel Scavenge,真正的造成“吞吐量优先”的收集器组合。 JDK7和8中,做为老年代默认的收集器
CMS收集器
CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。(重视响应,能够带来好的用户体验,被sun称为并发低停顿收集器)启用CMS:-XX:+UseConcMarkSweepGC
正如其名,CMS采用的是"标记-清除"(Mark Sweep)算法,并且是支持并发的。
它的运做分为4个阶段:
初始标记(initial mark):标记一下GC Roots能直接关联到的对象
并发标记(concurrent mark):并发标记就须要标记出GC roots 关联到的对象的引用对象有哪些。好比说 A -> B (A引用B,假设A是GC Roots关联到的对象),那么这个阶段就是标记出B对象,A对象会在初始标记中标记出来。这个过程是能够和用户线程并发执行的。所谓的并发的实现,能够有几种方式,好比说,标记了100个对象,那么就停一停,让用户线程跑一会;再好比说,标记了10ms,再停一停,之类的实现。
从新标记(remark):为了修正因并发标记期间用户程序运做而产生变更的那一部分对象的标记记录,会有些许停顿,时间上通常 初始标记 < 从新标记 < 并发标记
并发清除(sweep):将前面标记对象的内存回收,这个阶段GC线程与用户线程并发运行。
以上初始标记和从新标记须要停掉其它运行java线程。之因此说CMS的用户体验好,是由于CMS收集器的内存回收工做是能够和用户线程一块儿并发执行。 整体上CMS是款优秀的收集器,可是它也有缺点:
cms对cpu特别敏感,cms运行线程和应用程序并发执行须要多核cpu,若是cpu核数多的话能够发挥它并发执行的优点,可是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候好比说为为2核,若是这个时候cpu运算压力比较大,还要分一半给cms运做,这可能会很大程度的影响到计算机性能。
cms没法处理浮动垃圾,可能致使Concurrent Mode Failure(并发模式故障)而触发full GC
因为cms是采用"标记-清除“算法,所以就会存在垃圾碎片的问题,为了解决这个问题cms提供了-XX:+UseCMSCompactAtFullCollection选项,这个选项至关于一个开关(默认开启),用于CMS要进行full GC时开启内存碎片合并,内存整理的过程是没法并发的,且开启这个选项会影响性能(好比停顿时间变长)
Concurrent mode failure:若是CMS回收过程尚未执行完,老年代的剩余空间就用完了,或者,当前老年代空间不能知足一次内存分配请求(可能对象较大),那么此时将触发担保机制,停顿全部用户线程,串行老年代收集器将会以STW的方式进行一次GC,从而形成较大停顿时间;
浮动垃圾:因为cms支持运行的时候用户线程也在运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms没法当次处理,得等下次才能够。
假若有一个对象GC线程没有标记(用户线程以前没在用),而后轮到了用户线程,用户线程说,这个对象我从新又要用了,不要把这个对象GC掉,这个时候怎么办?假如这个时候处理不了,仍是GC了,那么程序就直接报错了,这个是不容许的,解决办法能够百度搜索“cms 三色标记法”获取答案
G1收集器
G1(garbage first)收集器是当前最为前沿的收集器之一(1.7之后才开始有),同cms同样也是关注下降延迟,是用于替代cms功能更为强大的新型收集器,由于它解决了cms产生空间碎片等一系列缺陷。
当G1肯定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先) g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。用到的算法为标记-清理、复制算法
G1是区域化的,它将java堆内存划分为若干个大小相同的区域"region“,JVM能够设置每一个region的大小(1-32m,大小得看堆内存大小,必须是2的幂),它会根据当前的堆内存分配合理的region大小。
g1经过并发(并行)标记阶段查找老年代存活对象,经过并行复制压缩存活对象(这样能够省出连续空间供大对象使用)。g1将一组或多组区域中存活对象以增量并行的方式复制到不一样区域进行压缩,从而减小堆碎片,目标是尽量多回收堆空间,且尽量不超出暂停目标以达到低延迟的目的。g1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。几个重要的默认值,更多的查看官方文档oracle官方g1中文文档 g1是自适应的回收器,提供了若干个默认值,无需修改就可高效运做
-XX:G1HeapRegionSize=n 设置g1 region大小,不设置的话本身会根据堆大小算,目标是根据最小堆内存划分2048个区域
-XX:MaxGCPauseMillis=200 最大停顿时间 默认200毫秒
JDK9中,G1做为默认的收集器
JDK7/8,默认关闭的,开启选项 -XX:+UseG1GC
更详细的G1垃圾回收介绍,请查看这篇文章: https://blog.csdn.net/coderlius/article/details/79272773

喜欢就点个“在看”呗^_^
本文分享自微信公众号 - 全菜工程师小辉(mseddl)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。