GoLand GC(垃圾回收机制)简介及调优

GC(Garbage Collector)垃圾回收机制及调优

简单理解GC机制

其实gc机制特别容易理解,就是物理内存的自动清理工。我们可以把内存想象成一个房间,程序运行时会在这个房间里存放各种东西,但有时候我们会忘记把不再需要的东西拿出去,这就会导致房间变得杂乱不堪。甚至会出现房间的空间不够用的情况,对应到计算机,就是OOM(out of memory, 内存再多用一点,就会爆炸)。
Go语言的GC机制会定期巡视这个房间,找出那些被遗忘的东西,并将它们清理出去,释放内存。这样,程序就能继续运行,不会因为内存不足而崩溃。
那么我们为什么要看gc机制呢,让gc自己在后台运行不好吗?其实看gc机制的主要目的就是为了内存或CPU占用率的优化,体现在两点:

  • 我需要知道房间里哪些东西最占地方,看看是不能能在代码上有所优化
  • 我要想要更加频繁的清理房间(减小内存占用)或更少频次的清理房间(减少CPU开销)

GC机制的原理

当然如果不想知道原理的话,直接转到第三节就好。(咳咳,其实我对细节也不是那么清楚,知道怎么用的感觉已经很不容易了>.<)

什么时候需要用到GC

其实并不是所有内存都需要GC来清理的,比如说有固定作用域的指针、地址等就无需gc,等这些对象的生命周期结束后,数据自动就会被销毁。换句人话说就是,在栈上开辟的空间会随自动释放,在堆上开辟的空间就需要gc机制来释放。如果学过C或C++就会很好理解,用make、new等操作创建的对象,都需要手动free掉,这些就是堆上开辟的内存空间。在Go语言中,就不用手动free了,这就是gc机制的作用,定期free这些手动创建(堆上)的动态内存分配的对象。
当然,专业点的话,gc是一个专门识别和清理动态内存分配的系统。如果分不清的话,可以看这个例子:

package mainimport "fmt"// 一个简单的结构体类型
type Person struct {Name stringAge  int
}func main() {// 栈上的对象分配// 创建一个名为 "Alice" 年龄为 25 的 Person 对象,分配在栈上alice := Person{Name: "Alice", Age: 25}// 动态内存分配// 创建一个名为 "Bob" 年龄为 30 的 Person 对象,使用 new 函数分配在堆上bob := new(Person)bob.Name = "Bob"bob.Age = 30// 输出栈上和堆上对象的信息fmt.Printf("Stack Object: Name: %s, Age: %d\n", alice.Name, alice.Age)fmt.Printf("Heap Object: Name: %s, Age: %d\n", bob.Name, bob.Age)
}

在程序运行结束后,alice会因为生命周期结束自动free掉。但是bob仍然在堆上,等待gc机制的回收。
对于Go语言的堆栈可以仿照C++做一个简单的理解。实际上go语言的堆栈上的内存处理,确实要比上述代码中描述的要复杂很多。go语言的逃逸分析,就是专门负责堆栈上内存空间开辟的。有兴趣的话,可以右转google一下。

回收内存的方法

gc机制通过标记-清除的方式回收内存。很好理解,我想要free这块内存,起码需要知道这块内存是不是没有用了。这就是标记的作用,被标记的,就是正在使用的(in-use),未被标记的都是要被free的。(gc标记了一块地点)
这个逻辑是不是很顺,但有一个问题,为什么不把标记清除放在一起呢,我既然找到了需要free的内存,为什么不直接清除掉,还要分两个环节来做呢?因为标记需要进行全局扫描,当gc扫描到一个内存,没有被使用时,但是此时很有可能存在一个未被扫描到的指针指向了内存,如果直接free掉就会造成“悬空指针”,影响后续运行,所以必须分为两个阶段,先全局扫描,进行标记,然后陆续进行“清除”

稍微细扣一下:

标记阶段

如何确定一个内存已经没有被使用呢?gc好像有不同的方法,举个例子:引用计数(仿佛又回到了当年被八股的日子)。十分的简单,有指针指向,引用计数就加一,如果扫描到引用计数为0,房子就要没咯。这里边也涉及到一些比较专业点尔的词汇,对象、指针、对象图。对象图是由对象和指向其他对象的指针一起构成的。遍历对象图的过程称为扫描。
为了识别实时内存,GC 从程序的根部开始遍历对象图,这些指针标识程序确实正在使用的对象。当然,也不是时时刻刻都要扫描的,毕竟它也不想996。这个就涉及到频率了,后面会讲到。
还有一个很好玩的概念,因为要对“对象图”进行扫描、标记,gc会给它赋一个值,叫做活动值。trace完成后,GC 就会遍历堆中的所有内存,并使所有未标记为可供分配的内存。 这个过程称为扫掠(sweeping)(文明六直呼内行,从水下的从水下第一个生命的萌芽开始…)。

清除阶段

然后就需要扫掠了,逐个free掉,这个阶段没啥好说的,之所以单独拎出来,主要是为了标题的对称…

当然,说的有些简单了,大体上分为这两个阶段。如果细分的话,还可以分:SweepTermination、Mark、MarkTermination,还有一些标记算法比如三色标记法等等,感兴趣的可以自行查一查~

gc调优的原理和开销

GOGC 决定了 GC CPU 和内存之间的权衡。所谓的优化,也不过是在内存和CPU开销之间反复横跳,看更需要哪种资源了。gc的一些官方文档上提到:“GOGC 加倍会使堆内存开销加倍,并使 GC CPU 成本减半”。现在用数学公式来看一下二者的tradeoff

内存开销

首先,堆目标设置总堆大小的目标。超过堆目标或目标的百分比,就要执行gc了。这就是确定gc执行的时间的方案。堆目标就两部分构成:新开辟的内存和正在使用的内存(活动堆)。

# 这个是gc计算目标堆内存大小的公式,不用太纠结它的物理含义,超过这个值就需要进行gc了
Target heap memory = Live heap + (Live heap + GC roots) * GOGC / 100Total heap memory = Live heap + New heap memory
# 前两者推出:
New heap memory = (Live heap + GC roots) * GOGC / 100
  • Target heap memory(目标堆内存):这是垃圾回收器的目标,它表示希望控制整个堆的大小。这个值由Go运行时系统设置,并可以通过环境变量 GOGC 来调整。它主要影响新分配的堆内存的大小,而不是已经存在的堆内存。

  • Live heap(活动堆内存):这是当前正在使用的堆内存的大小。这包括程序中正在使用的对象和数据结构。

  • GC roots(垃圾回收根节点):这表示垃圾回收器需要考虑的根节点的数量。主要分为三部分,全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。

  • GOGC(Go语言的垃圾回收阈值):GOGC 是一个环境变量,表示垃圾回收器的触发阈值。当已分配的内存达到 Target heap memory 的一定百分比时,垃圾回收将被触发,默认值是100。也就是100%、

可能公式有一些绕,大概意思就是,我规定了一个目标堆内存大小的计算公式,然后,推出了新开辟的堆内存大小,只要新开辟的堆内存大小达到了New heap memory,就执行一次gc。这个新开辟的内存大小是受gc调控的。

CPU开销

来个简化版公式:

Total GC CPU cost = (Allocation rate) / (GOGC / 100) * (Cost per byte) * T

详细公式可以看:A Guide to the Go Garbage Collector
从这个简化版公式可以看出,cpu的开销适合GOGC的大小成反比的

CPU vs 内存图例

依然是上面的网站,有一个好玩的例子,应用程序总共分配 200 MiB,每次 1s分配20 MiB。它假设唯一要完成的相关 GC 工作来自活动堆,并且(不切实际地)应用程序不使用额外的内存。

  • GC=100(默认情况)时
    在这里插入图片描述
  • GC=50时
    在这里插入图片描述
  • GC=-1(关闭gc)时
    在这里插入图片描述

GC调优

同第一节所说,我们的目的有两个:

  • 需要知道什么东西最占内存
  • 修改gc机制的频次或阈值

查看内存,手动调优

举个例子,代码来自:go pprof

package mainimport ("fmt""net/http"_ "net/http/pprof""sync""time"
)func main() {// we need a webserver to get the pprof webservergo func() {http.ListenAndServe("localhost:6060", nil)}()fmt.Println("hello world")var wg sync.WaitGroupwg.Add(1)go leakyFunction(wg)wg.Wait()
}func leakyFunction(wg sync.WaitGroup) {defer wg.Done()s := make([]string, 3)for i := 0; i < 10000000; i++ {s = append(s, "magical pandas")if (i % 100000) == 0 {time.Sleep(500 * time.Millisecond)}}
}

leakyFunction基本上,这只是启动一个分配一堆内存的goroutine ,然后最终退出。在程序运行期间,通过以下命令查看内存的分配情况:

 go tool pprof http://localhost:6060/debug/pprof/heap

然后使用top查看内存用量的前几名,如下

# go tool pprof http://localhost:6060/debug/pprof/heap
Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
Saved profile in /Users/yang/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
Type: inuse_space
Time: Oct 28, 2023 at 11:31pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 11715.14kB, 100% of 11715.14kB total
Showing top 10 nodes out of 24flat  flat%   sum%        cum   cum%7104.01kB 60.64% 60.64%  7104.01kB 60.64%  main.leakyFunction2562.81kB 21.88% 82.52%  2562.81kB 21.88%  runtime.allocm1024.01kB  8.74% 91.26%  1024.01kB  8.74%  runtime.doaddtimer512.20kB  4.37% 95.63%   512.20kB  4.37%  runtime.malg512.11kB  4.37%   100%   512.11kB  4.37%  net/http.ListenAndServe (inline)0     0%   100%   512.11kB  4.37%  main.main.func10     0%   100%  1024.01kB  8.74%  runtime.bgscavenge0     0%   100%  1025.12kB  8.75%  runtime.mcall0     0%   100%  1024.01kB  8.74%  runtime.modtimer0     0%   100%  1537.69kB 13.13%  runtime.mstart
(pprof) 

当然也可以用图片:比如这样:不过需要Graphviz包,没有的话会报错,安装也很简单

apt install graphviz ## debian/ubuntu
brew install graphviz ## mac
## 然后
go tool pprof -png http://localhost:6060/debug/pprof/heap > out.png

然后就可以看到内存的分布图了
在这里插入图片描述

这样你就可以清晰的看到内存使用最多的函数,leakyFunction。线越粗,代表内存用量越大,不过需要注意的是,这里的信息只是你执行代码时采的点,没有办法实时反应内存用量。

找到内存耗量最大的部分,然后就可以手动对代码的做一些修改。这就极大的考验代码能力了。如果不想整,可以移步下一小节,修改gc参数,自动搞定~
当然除了看内存,还可以看cpu开销等等(这些命令我忘记从哪里粘的了,侵删),虽然官网上也有: https://pkg.go.dev/net/http/pprof。(但实在是懒,就直接粘过来了)

#所有过去内存分配的采样
go tool pprof http://127.0.0.1:6060/debug/pprof/allocs#对活动对象的内存分配进行采样
go tool pprof http://127.0.0.1:6060/debug/pprof/heap# 下载 cpu profile,默认从当前开始收集 30s 的 cpu 使用情况,需要等待 30s
go tool pprof http://127.0.0.1:6060/debug/pprof/profile
# wait 120s
go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=120    #导致同步原语阻塞的堆栈跟踪
go tool pprof http://127.0.0.1:8080/debug/pprof/block#所有当前goroutine的堆栈跟踪
go tool pprof http://127.0.0.1:8080/debug/pprof/goroutine#争用互斥锁持有者的堆栈跟踪
go tool pprof http://127.0.0.1:8080/debug/pprof/mutex#当前程序的执行轨迹。
go tool pprof http://127.0.0.1:8080/debug/pprof/trace

当然本地开发的话这些都是有http窗口的,但貌似对linux上开发帮助不大。感兴趣大家可以右拐google一下,有很多帖子都是。

修改gc参数,自动调优

gc值暴露了一个接口,让我们修改gc的值,那就是:debug.SetGCPercent(),需要import runtime/debug。
ps:gc真好哇!知道我乱七八槽的不会用,只给了我一个接口。
比如:

package main
import ("runtime/debug"
)func main() {debug.SetGCPercent(30) //......
}

还是上面的代码,将gc改成30后,我查看了前六次gc的日志和gc为默认值100情况下:

# gc为30
# GODEBUG=gctrace=1 go run pprof.go 
gc 1 @0.505s 0%: 0.053+2.3+1.1 ms clock, 0.64+0/0.94/1.2+14 ms cpu, 3->4->2 MB, 4 MB goal, 12 P
gc 2 @0.509s 0%: 0.034+3.6+0.002 ms clock, 0.41+0/1.3/2.7+0.033 ms cpu, 4->4->2 MB, 5 MB goal, 12 P
gc 3 @0.513s 0%: 0.031+5.0+0.002 ms clock, 0.37+0.95/0.63/3.9+0.028 ms cpu, 3->3->2 MB, 4 MB goal, 12 P
gc 4 @0.519s 0%: 0.057+3.2+0.002 ms clock, 0.68+0/4.1/0.72+0.027 ms cpu, 4->4->3 MB, 5 MB goal, 12 P
gc 5 @1.024s 0%: 0.060+6.0+0.003 ms clock, 0.72+0/3.3/4.6+0.041 ms cpu, 5->5->4 MB, 6 MB goal, 12 P
gc 6 @1.031s 0%: 0.029+11+0.074 ms clock, 0.35+0.87/6.6/0.42+0.88 ms cpu, 7->10->10 MB, 8 MB goal, 12 P# gc为100
# GODEBUG=gctrace=1 go run pprof.go 
gc 1 @0.505s 0%: 0.062+2.4+0.002 ms clock, 0.74+0/1.1/1.6+0.033 ms cpu, 4->4->1 MB, 5 MB goal, 12 P
gc 2 @0.510s 0%: 0.027+6.0+0.002 ms clock, 0.33+0.18/3.9/0.55+0.032 ms cpu, 5->6->4 MB, 6 MB goal, 12 P
gc 3 @1.018s 0%: 0.12+11+0.002 ms clock, 1.5+0/11/0.29+0.030 ms cpu, 9->9->7 MB, 10 MB goal, 12 P
gc 4 @1.534s 0%: 0.044+12+0.002 ms clock, 0.53+0/13/2.4+0.030 ms cpu, 15->15->8 MB, 16 MB goal, 12 P
gc 5 @2.050s 0%: 0.046+16+0.002 ms clock, 0.55+0/3.6/13+0.032 ms cpu, 20->20->7 MB, 21 MB goal, 12 P
gc 6 @2.567s 0%: 0.075+16+0.002 ms clock, 0.90+0/6.2/15+0.034 ms cpu, 15->15->15 MB, 16 MB goal, 12 P

如何查看分析日志呢,见下一小节。
其实可以明显的看出来,gc为30时,目标堆大小,明显要比gc为100的时候要小得多。这样我们就省下来很多的内存,可以存一些自己想要的学习资料(好好好,拿内存当存储是吧…)

查看gc日志

查看gc日志信息(参考自:GODEBUG-GC):

# GODEBUG=gctrace=1 go run debug.go
gc 1 @0.049s 0%: 0.016+0.26+0.015 ms clock, 0.19+0.13/0.33/0.18+0.19 ms cpu, 4->4->0 MB, 5 MB goal, 12 P
gc 2 @0.816s 0%: 0.11+0.39+0.003 ms clock, 1.3+0.19/0.65/0.62+0.037 ms cpu, 4->4->0 MB, 5 MB goal, 12 P
gc 3 @0.824s 0%: 0.15+0.33+0.002 ms clock, 1.8+0/0.39/0.51+0.024 ms cpu, 4->4->0 MB, 5 MB goal, 12 P

含义如下:

  • gc#:GC 执行次数的编号,每次叠加。
  • @#s:自程序启动后到当前的具体秒数。
  • #%:自程序启动以来在 GC 中花费的时间百分比。
  • #+…+#:GC 的标记工作共使用的 CPU 时间占总 CPU 时间的百分比。
  • #->#-># MB:分别表示 GC 启动时, GC 结束时, GC 活动时的堆大小.
  • #MB goal:下一次触发 GC 的内存占用阈值。
  • #P:当前使用的处理器 P 的数量

debug的其他api

有点跑题了,不过确实很有用,关于gc参数调整,goland只提供了一个接口,但是对于debug中还有其他的一些api也很有用比如说

// 强制gc 将尽可能多的内存返回给操作系统
func FreeOSMemory()
// 设置最大堆大小
func SetMaxStack(bytes int) int
// 设置最大线程数
func SetMaxThreads(threads int) int

和SetGCPercent的使用方法一样,更多api详见:https://pkg.go.dev/runtime/debug#FreeOSMemory

参考

A Guide to the Go Garbage Collector
gc问题集
debug.api
GODEBUG-GC
go pprof
gc机制和调优
ChatGPT[doge]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/174467.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Windows下Redis配置密码,并设置自启动

1. 解压redis修改配置文件&#xff08;设置密码&#xff09; 打开Redis的配置文件&#xff0c;通常位于Redis安装目录下的 redis.windows.conf 去掉"requirepass"前注释&#xff0c;修改"foobared"为要设置的密码 requirepass foobared2.注册服务&#x…

Mybatis-Plus通用枚举功能 [MyBatis-Plus系列] - 第493篇

历史文章&#xff08;文章累计490&#xff09; 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 《国内最全的Spring Boot系列之六》 S…

JAVA数据类型和变量

一、字面常量 常量即程序运行期间&#xff0c;固定不变,不可修改的量称为常量。 public class Demo{public static void main(String[] args){System.Out.println("hello world!");System.Out.println(100);System.Out.println(3.14);System.Out.println(A);System…

Linux--进程替换

1.什么是进程替换 在fork函数之后&#xff0c;父子进程各自执行代码的一部分&#xff0c;但是如果子进程想要执行一份全新的程序呢&#xff1f; 通过进程替换来完成&#xff0c;进程替换就是父子进程代码发生写时拷贝&#xff0c;子进程执行自己的功能。 程序替换就是通过特定的…

maven环境变量,安装源,本地仓库配置

1. maven环境变量 我这里用的是idea自带的maven 数值为&#xff1a; D:\software\computer_software\java\IDEAJ\IDEAJ2021.2.1\IntelliJ IDEA 2021.2.1\plugins\maven\lib\maven3\bin 2. 安装源更换为阿里云&#xff08;我不知道清华源是什么网址&#xff0c;网上也没查到&am…

目标检测算法发展史

前言 比起图像识别&#xff0c;现在图片生成技术要更加具有吸引力&#xff0c;但是要步入AIGC技术领域&#xff0c;首先不推荐一上来就接触那些已经成熟闭源的包装好了再提供给你的接口网站&#xff0c;会使用别人的模型生成一些图片就能叫自己会AIGC了吗&#xff1f;那样真正…

Linux学习第25天:Linux 阻塞和非阻塞 IO 实验(二): 挂起

Linux版本号4.1.15 芯片I.MX6ULL 大叔学Linux 品人间百味 思文短情长 为方便和上一节的衔接&#xff0c;在正式开始学习前&#xff0c;先把本节的思维导图引入&#xff1a; 二、阻塞IO实验 1.硬件原理图分析 2.实验程序 #define I…

【排序算法】 归并排序详解!分治思想!

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; 算法—排序篇 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言&#x1f324;️归并排序的思想☁️基本思想☁️归并的思想实现☁️分治法 &#x1f3…

国产芯片vs“国际水平”,有距离也有超越!

当前&#xff0c;国产芯片正在迎来全新的发展阶段。国产终端芯片性能怎么样&#xff0c;与国际主流产品相比&#xff0c;表现如何&#xff1f;今天笔者就针对目前热度较高的四款国产CPU进行参数分析与性能跑分横向对比。 此次国产芯片评测型号分别是海光C86-3250、龙芯3A5000H…

Python 读取 Word 详解(python-docx)

文章目录 1 概述1.1 第三方库&#xff1a;python-docx 2 新建文档2.1 空白文档2.2 标题2.3 段落2.4 文本2.5 字体2.6 图片2.7 表格 3 扩展3.1 修改文档3.2 读取文档 1 概述 1.1 第三方库&#xff1a;python-docx > pip install python-docx2 新建文档 2.1 空白文档 impo…

idea中Run/Debug Python项目报错 Argument for @NotNull parameter ‘module‘ of ...

idea中Run/Debug Python项目报错 Argument for NotNull parameter module of ... idea中运行Python项目main.py时报错&#xff1a; Error running main: Argument for NotNull parameter module of com/intellij/openapi/roots/ModuleRootManager.getInstance must not be nu…

【jenkins】centos7在线安装jenkins

一、系统要求 最低推荐配置 256MB可用内存 1GB可用磁盘空间(作为一个Docker容器运行jenkins的话推荐10GB) 软件配置 Java 8—​无论是Java运行时环境&#xff08;JRE&#xff09;还是Java开发工具包&#xff08;JDK&#xff09;都可以 二、安装jenkins 准备一台安装有ce…

洞察运营机会的数据分析利器

这套分析方法包括5个分析工具&#xff1a; 用“描述性统计”来快速了解数据的整体特点。用“变化分析”来寻找数据的问题和突破口。用“指标体系”来深度洞察变化背后的原因。用“相关性分析”来精确判断原因的影响程度。用“趋势预测”来科学预测未来数据的走势&#xff0c;

系列四十、请谈一下Spring中事务的传播行为

一、概述 事务的传播行为指的是当一个事务方法被另一个事务方法调用时&#xff0c;这个事务方法应该如何进行。事务的传播行为至少发生在两个事务方法的嵌套调用中才会出现。 二、传播行为分类

Mysql进阶-索引篇(下)

SQL性能分析 SQL执行频率 MySQL 客户端连接成功后&#xff0c;通过 show [session|global] status 命令可以提供服务器状态信息。通过如下指令&#xff0c;可以查看当前数据库的INSERT、UPDATE、DELETE、SELECT的访问频次&#xff0c;通过sql语句的访问频次&#xff0c;我们可…

使用Swift模拟用户登录当网获取数据并保存到MySQL中

前言 当当网作为中国最大的综合性网上商城之一&#xff0c;通过爬取当当网数据&#xff0c;我们可以获取商品信息、用户评价、销售数据等宝贵的信息资源。这些数据可以帮助企业了解市场趋势、分析竞争对手、优化产品定价等&#xff0c;从而做出更明智的决策。 为什么使用Swif…

3D网页游戏外包开发引擎

3D网页开发引擎是用于创建具有三维图形、虚拟现实和交互性的网页应用程序的工具。以下是一些常用的3D网页开发引擎以及它们的主要特点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.Three.js&…

Mysql设置了更新时间自动更新,指定更新部分sql时不进行时间更新

现象&#xff1a; 因为字段设置了自动更新&#xff0c;所以sql语句一进行修改此字段就会自动更新时间&#xff0c;但是呢我们的有部分定时任务是半夜执行&#xff0c;并且不能让这个任务修改到数据的更新时间 解决&#xff1a; <update id"updateCreative">ALT…

mac安装nodejs,跑vue程序

1. 下载node.js for mac&#xff0c;地址&#xff1a;Node.js。一路安装就可以了&#xff0c;无需修改。 2. mac终端&#xff0c;查看node和npm的版本。 3. 配置环境变量&#xff0c; vim .bash_profile增加PATH$PATH:/usr/local/bin/ 4. 但是毕竟npm安装一些东西还是太慢了所…