技术管理
管理好你的技术

“Keras之父”亲传,一个软件工程师的成长自查清单

创造了Keras的 Francois Chollet近期在博客上分享了他个人的提醒清单,老司机带我们少走弯路。

在开发过程中

代码不仅仅是为了被执行的,也是一种团队内共同交流的方式。我们可以使用代码来向别人描述问题的解决方案。代码的可读性包括清晰的分段,易懂的变量名,以及描述隐含内容的注释。可读性并不是锦上添花,而是写代码必备的属性。

不要去想这次工作对自己下一次升值有什么帮助,而要去努力思考可以为自己产品的用户和社区做些什么。不惜一切代价避免“显眼的贡献”。不是真正对产品有帮助的功能,就不要添加。

个人品味也会反映在代码上。品味是一种约束-在简单性的渴望将这种可约束性满足的品味变得系统化。请保持对简单性的不懈追求。

味道也适用于代码。品味是一种约束-对于过程的满足感和对于代码简约的欲望之间的平衡。请保持你对简约的倾向。

学会拒绝。如果有人要求给代码添加新的功能,我们有权利拒绝。实际上,每个功能的成本都超出了最初的设想:维护成本,文档成本和用户的认知成本。每当被要求添加代码功能的时候,记得问问自己:我们真的应该这样做吗?通常来说,答案是否定的。

当你决定为产品添加新的用例,需要注意的是,如果你按照用户请求的字面意思去添加内容,这样反而达不到最佳效果。用户专注于他们自己的特定用例,而你必须从项目的整体和原则来考虑。通常,我们应该扩展现有功能而不是增加一个非常零碎的模块。

以持续集成和全面的代码单元测试为目标做出投资。始终确保自己处于一个能够自信地编码的环境中;如果不能够拥有靠谱的环境,首先要去建立合适的基础设施。

你可以不用事事都提前做好准备。有时候可以先尝试一下,看看结果如何,然后尽早更改错误的尝试。确保你建立了一个可以执行上述一切的环境。

好的软件让事情变得简单。虽然问题看起来很困难,但是这并不意味着解决方案必须复杂或难以使用。很多时候,我们有一个更容易并且并那么耀眼的办法,工程师却会选择使用更加复杂的有副作用的解决方案(画外音:“让我们使用机器学习吧”!“让我们构建一个应用!”“让我们添加区块链!”)在编写任何代码之前,请确保自己选择的解决方案是最简单的。从简单第一的原则着手,给出解决方案。

避免一切隐含规则。如果你发现自己开发的代码中有一些隐含规则,请用注释注明并与他人共享或自动化。每当你发现一个可重复的,类似于算法的工作流程时,应该设法将其文档化,以便其他团队成员从你的发现中受益。此外,你还应该设法在软件中自动化该工作流程的任何部分(例如正确性检查)。

从整个产品的设计过程去考虑你代码改动将会带来的影响,而不仅仅是关注你想要的部分-例如收入或增长。除了你正在监控的特定指标之外,您的软件对全球用户的总体影响是什么?是否存在任何副作用?在保留软件实用性的同时,你可以做些什么来解决这些问题?

关于API(应用程序接口)设计

你的API拥有用户,因此涉及到用户体验。在你做出的每一个决定中,始终牢记用户。请设身处地去理解你的用户,无论他们是初学者还是经验丰富的开发人员。

尽量减少用户在使用API时的认知负担。自动化可自动化的内容,最大限度地减少用户所需的操作和选择量,不要暴露不重要的选项,设计简单一致的工作流程,以反映简单一致的心智模型。

把简单的功能保持简单,让复杂的功能变成可能。不要为了实现某个特定功能而增加常用功能的认知成本。

如果工作流的认知成本足够低,那么用户应该可以在完成一次或两次之后就可以记住(而无需再次查看教程或文档)。

寻求与领域专家和从业者的心智模型相匹配的API。有领域经验但没有API经验的人应该能够使用最少的文档来直观地理解你的API。他们主要会查看代码示例,可用对象及其签名。

即使没有关于底层实现的任何材料,参数的含义也应该是容易被理解的。而由用户指定的参数应该与用户对问题的心智模型有关,而不是与代码中的实现细节有关。API只和它解决的问题相关,而与软件如何在后台运行无关。

最强大的心智模型是模块化和层次化的:总体很简洁,当你查看细节时又很精确。同样,一个好的API也是模块化和分层的:易于着手,但具有表现力。在更少对象上使用复杂签名,和在更多对象上使用简单签名之间存在一个平衡。一个好的API具有合理数量的对象,而且具有合理简单的签名。

你的API不可避免地反映了你的实现选择,特别是你选择的数据结构。要实现直观的API,你必须选择自然适合该领域的数据结构。这与该领域专家的心智模型相匹配。

深思熟虑地去设计从始到终的工作流程,而不仅仅是一系列原子功能。大多数开发人员去开发API的时候,会通过询问“应该提供哪些功能?让我们为他们配置选项。”而不是,请问“这个工具有什么使用场景?对于每个使用场景,用户操作的最佳顺序是什么?什么是最简单的API可以支持这个工作流程?”API中的原子选项应该满足高级工作流程中出现的明确需求。“因为有人可能需要它”,并不是添加理由。

错误消息,以及通常在与API交互过程中向用户提供的任何反馈,都是API的一部分。交互性和反馈是用户体验不可或缺的一部分。设计API的错误消息请深思熟虑。

因为代码是为了交流,所以命名很重要-无论是命名项目还是变量。名称反映了你对问题的看法。避免过于通用的名称(例如“x,变量,参数”),避免过度长和特定的命名,避免可能产生不必要纠纷的术语(例如“奴隶主/奴隶”),并确保在命名选择中保持一致。命名一致性意味着内部命名一致性(例如,不要命名“轴”的时候既用“dim”又用“axis”),以及与问题域已建立约定的一致性。在确定名称之前,请确保查找域专家(或其他API)使用的现有名称。

文档是API用户体验的核心。它不是附加组件。花时间去撰写高质量的文件,相较于花时间在其他的功能上,你会看到更高的回报。

用"展示"代替"叙述":你的文档不应该谈论软件如何工作,它应该展示如何使用它。展示从头到尾工作流程的代码示例;显示API的每个常见用例和关键功能的代码示例。

给软件工程师职业生涯的建议

职业进步并不是你管理的人数,而是你所产生的影响:你的工作是否为世界带来改变。

软件开发是团队合作,人际关系和技术能力同样重要。做个优秀的队友。当你走在职业道路上时,记得与他人保持联系。

技术永远不会中立。如果你的工作对世界有任何影响,那么这种影响就有道德方向。我们在软件产品中看似无害的技术选择调整了技术获取的条件,技术的使用激励,谁将受益以及谁将受到影响:技术选择也是道德选择。因此,始终谨慎而明确地表达你想通过选择支持的价值观。为道德而设计并将你的价值观融入到你的创作中。永远不要想“我只是在建立这种功能,这本身就是中立的“:它不是,因为你构建它的方式决定了它的使用方式。

建立世界需要的东西,而不仅仅是你希望拥有的东西。很多时候,技术人员过着远离常人的生活,专注于满足自身特定需求的产品。寻求扩大生活体验的机会,让你更好地了解世界需求。

在做出有着长期影响的任何选择时,将你的价值观置于短期的自身利益和情绪之上,例如贪婪或恐惧。了解你的价值观是什么,并让它们指导你。

当我们发现自己陷入冲突时,暂停一下,承认我们的共同价值观和共同目标是个好主意,并提醒自己,我们几乎肯定是站在同一边。

生产力归结为高速决策和行动偏见。这需要:1.)来自经验的良好直觉,以便在给出部分信息的情况下做出大体正确的决定;2.)敏锐地意识到何时更谨慎地行事并等待更多信息,因为错误决策比延迟决策的成本更高。在不同环境中,最佳速度/质量的决策权衡可以有很大差异。

更快地做出决定意味着你在职业生涯中做出更多决策,这将使你对可能选项的正确性有更强的直觉。经验是提高生产力的关键,更高的生产力将为您提供更多的经验。这是一个良性循环。

在你意识到缺乏直觉的情况下,坚持抽象的原则。在整个职业生涯中建立可靠且正确的原则列表:原则是形式化的直觉,和原始模式识别(需要直接和广泛的类似情境经验)相比,可以推广到更广泛的情境。

写好 shell 脚本的 13 个技巧

有多少次,你运行./script.sh,然后输出一些东西,但却不知道它刚刚都做了些什么。这是一种很糟糕的脚本用户体验。我将在这篇文章中介绍如何写出具有良好开发者体验的 shell 脚本。

产品的最终用户通常不懂技术,所以不管你怎么折腾产品代码都无所谓。但脚本代码不一样,它们是开发人员写给开发人员的。

这样会导致一些问题:

  1. 混乱的脚本 —— 我知道,我们都是工程师,读得懂代码,但即使这样,也请为我们这些对 Shell 脚本不是很熟练的人考虑一下(我们在写代码时也会为你们考虑的)。
  2. 满屏的日志和错误输出 —— 就算我们也是工程师,并不代表我们了解你所做的一切。
  3. 弄得一团糟却没有做好清理工作 —— 是的,我们可以顺着你的脚本手动撤销变更,但你真的会让那些信任你的脚本的人这么做吗?

所以,我们可以通过一些方法来为自己和别人写出更好的 shell 脚本。这里给出的所有示例都可以使用与 POSIX 标准兼容的 shell 运行(#!/bin/sh),因为它是最常用的。嫌文章太长了可以只看以下总结部分:

  1. 提供 --help 标记
  2. 检查所有命令的可用性
  3. 独立于当前工作目录
  4. 如何读取输入:环境变量 vs. 标记
  5. 打印对系统执行的所有操作
  6. 如果有必要,提供 --silent 选项
  7. 重新开启显示
  8. 用动画的方式显示进度
  9. 用颜色编码输出
  10. 出现错误立即退出脚本
  11. 自己执行清理工作
  12. 在退出时使用不同的错误码
  13. 在结束时打印一个新行

有时间的话可以接着往下看具体内容:

1. 提供 --help 标记

安装在系统上的二进制文件通常带有 man 帮助文档,但对于脚本来说就不一定了。因此我们通常需要为脚本提供 - h 或 --help 标记来打印有关如何使用脚本的信息。如果其他工程师需要修改脚本,这也可以作为脚本的内联文档:

#!/bin/sh
if [ ${#@} -ne 0 ] && [ "${@#"--help"}" = "" ]; then
  printf -- '...help...\n';
  exit 0;
fi;

这段脚本先计算参数长度(${#@} -ne 0),只有当参数长度不为零时才会检查 --help 标记。下一个条件会检查参数中是否存在字符串 “--help” 。第一个条件是必需的,如果参数长度为零则不需要打印帮助信息。

2. 检查所有命令的可用性

脚本通常会调用其他脚本或二进制文件。在调用可能不存在的命令时,请先检查它们是否可用。可以使用 “command -v 二进制文件名称” 来执行此操作,看看它的退出代码是否为零。如果命令不可用,可以告诉用户应该如何获得这个二进制文件:

#!/bin/sh
_=$(command -v docker);
if [ "$?" != "0" ]; then
  printf -- 'You don\'t seem to have Docker installed.\n';
  printf -- 'Get it: https://www.docker.com/community-edition\n';
  printf -- 'Exiting with code 127...\n';
  exit 127;
fi;
# ...

3. 独立于当前工作目录

从不同的目录执行脚本可能会发生错误,这样的脚本没有人会喜欢。要解决这个问题,请使用绝对路径(/path/to/something)和脚本的相对路径(如下所示)。

可以使用 dirname $0 引用脚本的当前路径:

#!/bin/sh
CURR_DIR="$(dirname $0);"
printf -- 'moving application to /opt/app.jar';
mv "${CURR_DIR}/application.jar" /opt/app.jar;

4. 如何读取输入:环境变量 vs 标记

脚本通过两种方式接受输入:环境变量和选项标记(参数)。根据经验,对于不影响脚本行为的值,可以使用环境变量,而对于可能触发脚本不同流程的值,可以使用脚本参数。

不影响脚本行为的变量可能是访问令牌和 ID 之类的东西:

#!/bin/sh
# do this
export AWS_ACCESS_TOKEN='xxxxxxxxxxxx';
./provision-everything
# and not
./provisiong-everything --token 'xxxxxxxxxxx';

影响脚本行为的变量可能是需要运行实例的数量、是异步还是同步运行、是否在后台运行等参数:

#!/bin/sh
# do this
./provision-everything --async --instance-count 400
# and not
INSTANCE_COUNT=400 ASYNC=true ./provision-everything

5. 打印对系统执行的所有操作

脚本通常会对系统执行有状态的更改。不过,由于我们不知道用户何时会向发送 SIGINT,也不知道脚本错误何时可能导致脚本意外终止,因此很有必要将正在做的事情打印在终端上,这样用户就可以在不去查看脚本的情况下回溯这些步骤:

#!/bin/sh
printf -- 'Downloading required document to ./downloaded... ';
wget -o ./downloaded https://some.site.com/downloaded;
printf -- 'Moving ./downloaded to /opt/downloaded...';
mv ./downloaded /opt/;
printf -- 'Creating symlink to /opt/downloaded...';
ln -s /opt/downloaded /usr/bin/downloaded;

6. 在必要时提供 --silent 选项

有些脚本是为了将其输出传给其他脚本。虽说脚本都应该能够单独运行,不过有时候也有必要只让它们把输出结果传给另一个脚本。可以利用 stty -echo 来实现 --silent 标记:

#!/bin/sh
if [ ${#@} -ne 0 ] && [ "${@#"--silent"}" = "" ]; then
  stty -echo;
fi;
# ...
# before point of intended output:
stty +echo && printf -- 'intended output\n';
# silence it again till end of script
stty -echo;
# ...
stty +echo;
exit 0;

7. 重新开启显示

在使用 stty -echo 关闭脚本显示之后,如果发生致命错误,脚本将终止,而且不会恢复终端输出,这样对用户来说是没有意义的。可以使用 trap 来捕捉 SIGINT 和其他操作系统级别的信号,然后使用 stty echo 打开终端显示:

#!/bin/sh
error_handle() {
  stty echo;
}
if [ ${#@} -ne 0 ] && [ "${@#"--silent"}" = "" ]; then
  stty -echo;
  trap error_handle INT;
  trap error_handle TERM;
  trap error_handle KILL;
  trap error_handle EXIT;
fi;
# ...

9. 用动画的方式显示进度

有些命令需要运行很长时间,并非所有脚本都提供了进度条。在用户等待异步任务完成时,可以通过一些方式告诉他们脚本仍在运行。比如在 while 循环中打印一些信息:

#!/bin/sh
printf -- 'Performing asynchronous action..';
./trigger-action;
DONE=0;
while [ $DONE -eq 0 ]; do
  ./async-checker;
  if [ "$?" = "0" ]; then DONE=1; fi;
  printf -- '.';
  sleep 1;
done;
printf -- ' DONE!\n';

或者可以做一些更好玩的小玩意儿,比如 http://mywiki.wooledge.org/BashFAQ/034

10. 用颜色编码输出

在脚本中调用其他二进制文件或脚本时,对它们的输出进行颜色编码,这样就可以知道哪个输出来自哪个脚本或二进制文件。这样我们就不需要在满屏的黑白输出文本中查找想要的输出结果。

理想情况下,脚本应该输出白色(默认的,前台进程),子进程应该使用灰色(通常不需要,除非出现错误),使用绿色表示成功,红色表示失败,黄色表示警告。

#!/bin/sh
printf -- 'doing something... \n';
printf -- '\033[37m someone else's output \033[0m\n';
printf -- '\033[32m SUCCESS: yay \033[0m\n';
printf -- '\033[33m WARNING: hmm \033[0m\n';
printf -- '\033[31m ERROR: fubar \033[0m\n';

可以使用 \ 033[Xm,其中 X 代表颜色代码。有些脚本使用 e 而不是 033,但要注意 e 不适用于所有的 UNIX 系统。

可在. sh 中使用的所有颜色和修饰符 https://misc.flogisoft.com/bash/tip_colors_and_formatting

11. 出现错误立即退出脚本

set -e 表示从当前位置开始,如果出现任何错误都将触发 EXIT。相反,set +e 表示不管出现任何错误继续执行脚本。

如果脚本是有状态的(每个后续步骤都依赖前一个步骤),那么请使用 set -e,在脚本出现错误时立即退出脚本。如果要求所有命令都要执行完(很少会这样),那么就使用 set +e

#!/bin/sh
set +e;
./script-1;
./script-2; # does not depend on ./script-1
./script-3; # does not depend on ./script-2
set -e;
./script-4;
./script-5; # depends on success of ./script-4
# ...

12. 自己执行清理工作

大多数脚本在出现错误时不会执行清理工作,能够做好这方面工作的脚本实属罕见,但这样做其实很有用,还可以省下不少时间。前面已经给出过示例,让 stty 恢复正常,并借助 trap 命令来执行清理工作:

#!/bin/sh
handle_exit_code() {
  ERROR_CODE="$?";
  printf -- "an error occurred. cleaning up now... ";
  # ... cleanup code ...
  printf -- "DONE.\nExiting with error code ${ERROR_CODE}.\n";
  exit ${ERROR_CODE};
}
trap "handle_exit_code" EXIT;
# ... actual script...

13. 在退出时使用不同的错误码

在绝大多数 shell 脚本中,exit 0 表示执行成功,exit 1 表示发生错误。对错误与错误码进行一对一的映射,这样有助于脚本调试。

#!/bin/sh
# ...
if [ "$?" != "0" ]; then
  printf -- 'X happened. Exiting with status code 1.\n';
  exit 1;
fi;
# ...
if [ "$?" != "0" ]; then
  printf -- 'Y happened. Exiting with status code 2.\n';
  exit 2;
fi;

这样做有另一个额外的好处,就是其他脚本在调用你的脚本时,可以根据错误码来判断发生了什么错误。

14. 在结束时打印一个新行

如果你有在遵循脚本的最佳实践,那么可能会使用 printf 代替 echo(它在不同系统中的行为有所差别)。问题是 printf 在命令结束后不会自动添加一个新行,导致控制台看起来是这样的:

这样一点也不酷,可以通过简单的方式打印一个新行:

#!/bin/sh
# ... your awesome script ...
printf -- '\n';
exit 0;

这样就可以得到:

别人会感谢你这么做的。

总结

这篇文章大致总结了一些简单易用的技巧,让 shell 脚本更易于调试和使用。

英文原文https://codeburst.io/13-tips-tricks-for-writing-shell-scripts-with-awesome-ux-19a525ae05ae

原文地址 http://www.infoq.com/cn/articles/13-tips-tricks-for-writing-shell-scripts-with-awesome-ux?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global

为什么说 JSON 不适合做配置文件?

很多项目使用 JSON 作为配置文件,最明显的例子就是 npm 和 yarn 使用的 package.json 文件。当然,还有很多其他文件,例如 CloudFormation(最初只有 JSON,但现在也支持 YAML)和 composer(PHP)。

但是,JSON 实际上是一种非常糟糕的配置语言。别误会我的意思,我其实是喜欢 JSON 的。它是一种相对灵活的文本格式,对于机器和人类来说都很容易阅读,而且是一种非常好的数据交换和存储格式。但作为一种配置语言,它有它的不足。

为什么流行使用 JSON 作为配置语言?

将 JSON 用作配置文件有几个方面的原因,其中最大的原因可能是它很容易实现。很多编程语言的标准库都支持 JSON,开发人员或用户可能已经很熟悉 JSON,所以不需要学习新的配置格式就可以使用那些产品。现在几乎所有的工具都提供 JSON 支持,包括语法突出显示、自动格式化、验证工具等。

这些都是很好的理由,但这种无处不在的格式其实不适合用作配置。

JSON 的问题

缺乏注释

注释对于配置语言而言绝对是一个重要的功能。注释可用于标注不同的配置选项、解释为什么要配置成特定的值,更重要的是,在使用不同的配置进行测试和调试时需要临时注释掉部分配置。当然,如果只是把 JSON 当作是一种数据交换格式,那么就不需要用到注释。

我们可以通过一些方法给 JSON 添加注释。一种常见的方法是在对象中使用特殊的键作为注释,例如 “//” 或“__comment”。但是,这种语法的可读性不高,并且为了在单个对象中包含多个注释,需要为每个注释使用唯一的键。David Crockford(JSON 的发明者)建议使用预处理器来删除注释。如果你的应用程序需要使用 JSON 作为配置,那么完全没问题,不过这确实带来了一些额外的工作量。

一些 JSON 库允许将注释作为输入。例如,Ruby 的 JSON 模块和启用了 JsonParser.Feature.ALLOW_COMMENTS 功能的 Java Jackson 库可以处理 JavaScript 风格的注释。但是,这不是标准的方式,而且很多编辑器无法正确处理 JSON 文件中的注释,这让编辑它们变得更加困难。

过于严格

JSON 规范非常严格,这也是为什么实现 JSON 解析器会这么简单,但在我看来,它还会影响可读性,并且在较小程度上会影响可写性。

低信噪比

与其他配置语言相比,JSON 显得非常嘈杂。JSON 的很多标点符号对可读性毫无帮助,况且,对象中的键几乎都是标识符,所以键的引号其实是多余的。

此外,JSON 需要使用花括号将整个文档包围起来,所以 JSON 是 JavaScript 的子集,并在流中发送多个对象时用于界定不同的对象。但是,对于配置文件来说,最外面的大括号其实没有任何用处。在配置文件中,键值对之间的逗号也是没有必要的。通常情况下,每行只有一个键值对,所以使用换行作为分隔符更有意义。

说到逗号,JSON 居然不允许在结尾出现逗号。如果你需要在每个键值对之后使用逗号,那么至少应该接受结尾的逗号,因为有了结尾的逗号,在添加新条目时会更容易,而且在进行 commit diff 时也更清晰。

长字符串

JSON 作为配置格式的另一个问题是,它不支持多行字符串。如果你想在字符串中换行,必须使用 “n” 进行转义,更糟糕的是,如果你想要一个字符串在文件中另起一行显示,那就彻底没办法了。如果你的配置项里没有很长的字符串,那就不是问题。但是,如果你的配置项里包括了长字符串,例如项目描述或 GPG 密钥,你可能不希望只是使用 “n” 来转义而不是使用真实的换行符。

数字

此外,在某些情况下,JSON 对数字的定义可能会有问题。JSON 规范中将数字定义成使用十进制表示的任意精度有限浮点数。对于大多数应用程序来说,这没有问题。但是,如果你需要使用十六进制表示法或表示无穷大或 NaN 等值时,那么 TOML 或 YAML 将能够更好地处理它们。

{
  "name": "example",
  "description": "A really long description that needs multiple lines.\nThis is a sample project to illustrate why JSON is not a good configuration format. This description is pretty long, but it doesn't have any way to go onto multiple lines.",
  "version": "0.0.1",
  "main": "index.js",
  "//": "This is as close to a comment as you are going to get",
  "keywords": ["example", "config"],
  "scripts": {
    "test": "./test.sh",
    "do_stuff": "./do_stuff.sh"
  },
  "bugs": {
    "url": "https://example.com/bugs"
  },
  "contributors": [{
    "name": "John Doe",
    "email": "johndoe@example.com"
  }, {
    "name": "Ivy Lane",
    "url": "https://example.com/ivylane"
  }],
  "dependencies": {
    "dep1": "^1.0.0",
    "dep2": "3.40",
    "dep3": "6.7"
  }
}

JSON 的替代方案

选择哪一种配置语言取决于你的应用程序。每种语言都有各自的优缺点,下面列出了一些可以考虑的选项。它们都是为配置而设计的语言,每一种都比 JSON 这样的数据语言更好。

name = "example"
description = """
A really long description that needs multiple lines.
This is a sample project to illustrate why JSON is not a \
good configuration format. This description is pretty long, \
but it doesn't have any way to go onto multiple lines."""
version = "0.0.1"
main = "index.js"

# This is a comment
keywords = ["example", "config"]

[bugs]
url = "https://example.com/bugs"

[scripts]
test = "./test.sh"
do_stuff = "./do_stuff.sh"

[[contributors]]
name = "John Doe"
email = "johndow@example.com"

[[contributors]]
name = "Ivy Lane"
url = "https://example.com/ivylane"

[dependencies]
dep1 = "^1.0.0"

# Why we depend on dep2
dep2 = "3.40"
dep3 = "6.7"

HJSON

HJSON 是一种基于 JSON 的格式,但具有更大的灵活性,可读性也更强。它支持注释、多行字符串、不带引号的键和字符串,以及可选的逗号。如果你想要 JSON 结构的简单性,同时对配置文件更友好,那么可以考虑 HJSON。有一些可以将 HJSON 转换为 JSON 的命令行工具,如果你使用的工具是基于 JSON 的,可以先用 HJSON 编写配置,然后再转换成 JSON。JSON5 是另一个与 HJSON 非常相似的配置语言。

{
  name: example
  description: '''
  A really long description that needs multiple lines.
  This is a sample project to illustrate why JSON is 
  not a good configuration format.  This description 
  is pretty long, but it doesn't have any way to go 
  onto multiple lines.
  '''

  version: 0.0.1
  main: index.js

  # This is a a comment
  keywords: ["example", "config"]
  scripts: {
    test: ./test.sh
    do_stuff: ./do_stuff.sh
  }

  bugs: {
    url: https://example.com/bugs
  }

  contributors: [{
    name: John Doe
    email: johndoe@example.com
  } {
    name: Ivy Lane
    url: https://example.com/ivylane
  }]

  dependencies: {
    dep1: ^1.0.0
    # Why we have this dependency
    dep2: "3.40"
    dep3: "6.7"
  }
}

HOCON

HOCON 是为 Play 框架设计的配置格式,在 Scala 项目中非常流行。它是 JSON 的超集,因此可以使用现有的 JSON 文件。除了注释、可选逗号和多行字符串这些标准特性外,HOCON 还支持从其他文件导入和引用其他值的键,避免重复代码,并使用以点作为分隔符的键来指定值的路径,因此用户可以不必将所有值直接放在花括号对象中。

name = example
description = """
A really long description that needs multiple lines.
This is a sample project to illustrate why JSON is 
not a good configuration format.  This description 
is pretty long, but it doesn't have any way to go 
onto multiple lines.
"""

version = 0.0.1
main = index.js

# This is a a comment
keywords = ["example", "config"]

scripts {
  test = ./test.sh
  do_stuff = ./do_stuff.sh

}

bugs.url = "https://example.com/bugs"
contributors = [
  {
    name = John Doe
    email = johndoe@example.com
  }
  {
    name = Ivy Lane
    url = "https://example.com/ivylane"
  }
]

dependencies {
  dep1 = ^1.0.0
  # Why we have this dependency
  dep2 = "3.40"
  dep3 = "6.7"
}

YAML

YAML(YAML 不是标记语言)是一种非常灵活的格式,几乎是 JSON 的超集,已经被用在一些著名的项目中,如 Travis CI、Circle CI 和 AWS CloudFormation。YAML 的库几乎和 JSON 一样无处不在。除了支持注释、换行符分隔、多行字符串、裸字符串和更灵活的类型系统之外,YAML 也支持引用文件,以避免重复代码。

YAML 的主要缺点是规范非常复杂,不同的实现之间可能存在不一致的情况。它将缩进视为严格语法的一部分(类似于 Python),有些人喜欢,有些人不喜欢。这会让复制和粘贴变得很麻烦。

name: example
description: >
  A really long description that needs multiple lines.
  This is a sample project to illustrate why JSON is not a good 
  configuration format. This description is pretty long, but it 
  doesn't have any way to go onto multiple lines.

version: 0.0.1
main: index.js

# this is a comment
keywords:
  - example
  - config

scripts: 
  test: ./test.sh
  do_stuff: ./do_stuff.sh

bugs: 
  url: "https://example.com/bugs"

contributors:
  - name: John Doe
    email: johndoe@example.com
  - name: Ivy Lane
    url: "https://example.com/ivylange"

dependencies:
  dep1: ^1.0.0
 
  # Why we depend on dep2
  dep2: "3.40"
  dep3: "6.7"

脚本语言

如果你的应用程序是使用 Python 或 Ruby 等脚本语言开发的,并且你知道配置的来源是可靠的,那么最好的选择可能就是使用这些语言进行配置。如果你需要一个真正灵活的配置选项,也可以在编译语言中嵌入诸如 Lua 之类的脚本语言。这样可以获得脚本语言的灵活性,而且比使用不同的配置语言更容易实现。使用脚本语言的缺点是它可能过于强大,当然,如果配置来源是不受信任的,可能会引入严重的安全问题。

自定义配置格式

如果由于某种原因,键值配置格式不能满足你的要求,并且由于性能或大小限制而无法使用脚本语言,那么可以考虑自定义配置格式。如果是这种情况,那么在做出选择之前要想清楚,因为你不仅要编写和维护一个解析器,还要让你的用户熟悉另一种配置格式。

结论

有了这么多更好的配置语言,没有理由还要使用 JSON。如果要创建需要用到配置的新应用程序、框架或库,请选择 JSON 以外的其他选项。

英文原文:https://www.lucidchart.com/techblog/2018/07/16/why-json-isnt-a-good-configuration-language/

原文地址 https://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247488154&idx=1&sn=fa962f9a1813500dbb7b741598e88aa6&chksm=96c9a4faa1be2decbea14f96f14dda08640f1a7137057a2a2d148c32c66096ceea71bab5dc38#rd

投资的本质是认知的变现

前些日子跟投了摩拜的 VC 大佬头脑风暴,聊到投资的本质的时候,最后不约而同地总结为认知变现。今晚跟在上市公司做并购的老友吃饭,又谈到这一话题,觉得还是有进一步思考的空间,因此在此从以下几个方面做一些简单总结,跟各位探讨。

1、投资即决策,而决策差异背后的根本是认知水平(“认知是大脑的决策算法”):投资就是对未来的不确定性下注,投资的过程就是不断地在胜率和赔率之间做平衡。一人一世界,每个人看到的世界都是不一样的,每个人对同一投资机会的认知也分不同的维度,不同维度的认知意味着各自完全不同的胜率选择。没有高确定性为前提的高赔率,是赌博。但市场普遍认知到的高确定性,在通常情况下又不可能给你高赔率。所以问题的本质其实是你是否有超越市场大多数人的认知,适时的判断出市场的错误定价,以高胜率去做高赔率的决策。马云所说的,任何一次机会的到来,都必将经历四个阶段:“看不见”、“看不起”、“看不懂”、“来不及”,也是同样的逻辑。

2、认知是多维度的:成功的投资需要完整的体系支撑,多维度的认知可以从不同的层面优化你投资体系的胜率和赔率指标。比如对行业和企业经营的认知:行业空间、竞争格局、价值链分布、核心竞争力、增长驱动因素等;交易系统的认知:仓位管理、风险管理等;投资思维的认知:安全边际、复利等。当然,不同人在整个体系的不同环节的认知能力是有差异的,需要选择性的修炼边际收益最高的部分,也可以通过团队合作让自己的某一部分优质认知最大程度上发挥价值。

3、投资很难赚到你不信的那份钱:知道和相信之间有很大的距离,而真正的认知是相信,甚至信仰,我们先看几个数字:

a、王者荣耀刷遍朋友圈,我相信大部分人都知道,但截止到今天,腾讯股价今年的涨幅是 72.3%,涨幅超越绝大多数资产的收益率,而绝大多数人也只是感慨下大腾讯好牛逼啊而已;

b、相信大多数人通过各种渠道学习了很多关于 google、亚马逊、Facebook、苹果、腾讯、阿里巴巴等这些明星公司的核心竞争力、企业文化、商业模式、创始人风格等各方面的知识,都知道他们很牛,但如果把六支股票做一个组合去复盘,过去三年的年化的平均回报也高达百分之好几十;

这些现象背后移动互联网对人们生活方式、商业模式的深刻改变,新生一代的消费习惯的升级等原因,我们大多数人都知道,但能否真正的从知道到相信,进一步转化为可以投资的认知,则少之又少。

4、如何提高从认知到投资的转化率:

对于我们大多数非专业投资人来讲,对于自己所从事多年的行业认知水平肯定是超越市场上大多数的人,因此对多年在某种特定行业中的积累、独特认知和趋势判断放在投资的视角思考,完全可以把这种深度认知轻松变形。比如,“2010 年 google 退出中国,很容易判断最大的收益方是百度,事实上百度那一年股价确实翻倍了。比如爱玩游戏的人可以关注暴雪,了解手机行业的人可以关注高通,从事 k12 的人可以关注学而思、新东方。”

我一直说,在过去的 6-7 年时间里面,除了一线城市的房价(是否参与的背后也是认知的差异),我们还错过了两拨超过 10 倍的投资机会。一次是智能手机从 10% 不到的渗透率提高到 65% 的浪潮(2010-2013 年中),相关产业链的公司都是动辄几倍的涨幅,其中生产触摸屏的欧菲光短短一年上涨近 10 倍;另外一波是智能手机渗透率超过 65% 之后带来的移动互联网浪潮,在一二级市场都雨后春笋般的出现了大量高效率的投资机会,其中变现最快、同时也跟大多数人的生活紧密相关的手游行业规模短短 4 年时间就增长了 10 倍(2012 年 - 2016 年从 60 亿左右增长到了 600 多亿),其中诞生的牛股就不言而喻, 一直到今天的王者荣耀。

相信,这两拨大的浪潮和机会绝大多数的 80 后、90 后都是亲身经历、感同身受,因此如果你把这种身边的熟知现象,工作中对行业的积累通过深度思考转为真正的认知,再通过投资工具实现变现,那就是属于你的投资机会。

当然,创业也属于认知变现的范畴(李善友曾说,创始人的认知边界是企业真正的边界),只是其要求更高,除了需要具备超越市场的认知外,还需要战略、组织等一系列综合能力。而投资,特别是二级市场的投资,其交易成本基本可以忽略不计,是大多数人认知变现最佳的路径。(一级市场投资门槛更高,交易成本也更为复杂)

当然,真正的如何全方位的提高认知能力又是一个宏大的话题,下次有机会再探讨。

公司研究的核心是空间、结构、深度、角度;交易策略的核心是概率、赔率和预期差;投资体系的核心是对象,时机,力度;而投资最本质的核心是价值、安全边际和复利。这几个环节是同心圆,越后面的越靠内环。内环决定战略和原则,外环确定策略和技术;内环是道,外环是术;内环解决了 What 和 Why,外环实现了落地的 How;内环不牢靠是闷头乱撞,外环不扎实是夸夸其谈。

市场的高波动不代表高风险,反之市场的平静也不等于均衡。平静和激烈放在不同的尺度看完全不一样,日线的惊涛骇浪可能在月线来看只是个小浪花。均衡与失衡的关键不是短期的波幅,而是到底距离价值线有多远的问题。或者说,波动的程度仅仅体现情绪的烈度,但决定中长期方向的是价格与价值的偏离度。

当流动性泛滥远去之后,你还会投资吗?你的资产结构还健康吗?这是目前我们应该问自己的战略级别的问题。在 “钱多多” 趋势下推升出来的收益率是有很高欺骗性的,善于用趋势变现的是高手,被趋势迷惑并在趋势末段入套的就有点儿被动了。读了这么多书,其实提炼出来就两句话:和趋势作对,你很难发财;和均值回归作对,你很容易破产。

谨慎型投资人给人感觉好像总是很胆小,但其实相对于绝大多数天天追求涨停板年年挑战高收益的 “勇敢者” 来说,他们赌得是整个人生。只有当你赌得足够大的时候,你才会谨慎,只有你想赢得足够多的时候,你才会忍耐。很多时候,不理解只是因为大家不在一个游戏桌上而已。

所有的大亏都是贪和怕导致的,而对赚快钱和一夜暴富的期望又是贪和怕的本质原因。稳健说起来容易,但那意味着你要在别人快速赚钱的时候不眼红,在你的保守屡屡被疯狂的市场暴击的时候不变卦。根本上是需要对自己理念和方法的绝对信心。而这种信心又不能是盲目的。信心最终只能来自于知识、历史和经验。

不管哪种投资风格和所谓门派,你可以有不同的市场假设,不同的理论内涵,不同的价值主张,不同的操作体系,甚至不同的世界观。但所有的这些不同都是表象,它们要想取得成功都必须必然必定符合复利法则。一个在复利法则上无法自圆其说的方法论,无论您拽多高深的词,拿出多深奥的理论,用多牛的人来背书,都是无济于事的。

投资要想成功需要承认两个前提:第一,我们都是凡人,我们必将不断犯错;第二,投资的核心原理早已稳定,我们要做的不是创新,而是理解并执行。投资要想惨败可以建立在另外两个前提上:第一,我天赋异禀,我能做到别人做不到的;第二,我拥有或者发现了最新的炒股奥义,我在投资理论上已经帮人类跨出了新的一步。

无论是公司的经营变化,还是资金和情绪的变化,其实最终一定会在估值上反映出来。但估值反映不出来的,是这种变化到底是对还是错。而对于投资人来说,估值的意义除了 “对” 和“错”之外,更重要的是真相与其预期验证之后的结果分布。所以估值是乍看很直观,再看看好像很复杂,最后看时又没想象的那么复杂。

没有高赔率相伴的高确定性,是鸡肋。没有高确定性为前提的高赔率,是赌博。但市场普遍认知到的高确定性,在通常情况下又不可能给你高赔率。所以问题的本质其实是理解市场的有效性与局限性,理解错误定价在什么情况下最容易出现。

黑盒状态下,盒子里面装的是什么很重要,围观和出价的人认为盒子里装的是什么也很重要。大多数时候和放长时间来看,盒内物品本身更重要。但特定的时间内和氛围下,围观群众的判断和出价有可能造成一些极端情况。

公司的不确定性到底来自哪里?我想主要是:1,未来市场需求与当前预期之间的差异;2,公司战略制定及执行过程中出现的问题;3,对经营产生重要影响的各种不可控因素(如原材料波动,汇率波动,政策重大变化,颠覆性技术革新等);4,竞争烈度的大幅提升;5,随着时间的拉长使一切事物不可控进而不可测。

对于上述一到三的对策,其实是解铃还须系铃人。优秀的企业家和管理团队是提升这三项确定性的根本依靠;对于四,更重要的是生意属性上的根本特征,已非简单事在人为的范畴;对于五,唯一需要的就是对生命周期的敬畏和对安全边际的重视,其应对本质已在企业自身之外。

投资人的主动性努力不外乎以下几点:第一,寻找中长期经营提升确定性高的企业;第二,寻求中短期赔率好的价格;第三,构建能平衡黑天鹅风险和研究的有效回报的投资组合;第四,持续学习争取每年进步一点点;第五,保持身心健康,做好以上循环并耐心等待。其它的,基本就交给国运了。

资产配置的重要性有多大?我觉得第一它是重要的,但它一定是在一个长期的刻度上才显示出这种优势;第二,所谓的配置一定要有足够的力度才有效果,优质的配置机会但你只配了很轻的资产,也没什么意义;第三,资产配置本身并不必然导致升值,它是分担风险,这两者区别很大。最后,配置的重要性与单一资产的规模成正比,大部分的人问题不是配置而是规模。

又惊闻一位年轻有为的投资人突然去世,让人叹息。投资真的不能只争朝夕,只要坚持做对的事情,资产加零只是时间问题而已。但身体归零,一切的努力还有什么意义呢?特别是上有老下有小的年龄,风险控制可不仅仅是在投资领域。保持健康的身心,别做高危的事,甚至别乱穿马路,都是对自己,对家庭,对客户最大的负责。

感恩节要说几句的话,我最感谢的是时代。很多所谓的成功和牛逼都是好时代给的,没有长长的雪道,大多数群众连瓜都没得吃。我推崇经济发展,是看到世界上最多的恶,其实本质上是因为贫穷。

中国经济现状表现为周期叠加结构。短中期而言周期压力是主要矛盾,中长期来看结构转型是主要矛盾。如果转型不成功,周期复苏其实救不了中国。如果转型成功,毛姑姑看应该还有十几年的好日子。15 年后,城镇化率基本达到 72% 以上,人均 GDP 迈入发达国家,人口结构完全进入老龄化,按传统经验看伟大的历时 50 余年的实力回归之旅告一段落。

届时是否能打破社会老龄经济萎靡的关键,是看有无新的技术突破。其中最关键的可能是人工智能技术,这是不再依赖于年轻劳动力人口的又一次生产力升级。那时的第一和第二产业将进一步被全球寡头垄断,老年化高技术国家未必竞争力下滑,但将以全球贫富差距的进一步拉大为代价。

经常在微信看到 “中国面临剧变” 之类的文章,其实客观的讲中国的剧变一直在进行时中,就没停过。只不过我们经常容易高估短期事件对中国变化的影响,而大大低估中国长期持续变化的驱动力和重要性。就像复利一样,渐变且持续不断的渐变是最好的路线图。又想起最近一本书上看到的一句话:荣总是静悄悄的发生,而灾难总是在大吵大闹中降临。

市场时刻都热点繁多,新闻每天都有惊悚头条,但咱做投资的真要有点儿满清太祖那 “任尔几路来我只一路去” 的劲头。与真实的价值为伍,站在概率和赔率的有利面,透彻理解并执行复利法则,其它的何必管它那么多?大多数的所谓对策都没有存在的必要,大部分刺激的新闻都与投资没关系。其实过度的关注反映的是内心的焦虑,这是种病,而且不好治。

投资成长中有两个临界点,在第一个临界点之前是艰辛的学习,用勤奋都不足以描述,简直是废寝忘食的恶补。完成这个阶段的时间因人而异,但 1 万小时定律是最起码的。过了这个基础临界点后,反思和质疑开始产生价值(之前别瞎反思),实践和总结推动着越过第二个临界点,但悟性和天资可能让一些人永远翻不过去。两次质变之后,将开始享受学习积累和资产积累的双重复利。

从估值等基本情况而言目前市场整体上与 15 年 2-3 月份相当。也就是说如果你判断牛市来了,那后面还有一段快速涨,如果你判断熊市还没结束,那后头还有得跌。但不管以上哪个判断成立,在目前基础上赚大钱的可能性都非常低。总之,现在的市场先生并不慷慨。

如果用最朴素的个人投资经验来看,我自己是如果一年不怎么赚钱,第二年大概率的就会有所收获,如果第二年还是不怎么赚钱,那意味着第三年一定是超级大丰收。当然这个规律也有对照的另一面,比如如果一年大赚第二年多是平淡,如果连续两年大赚那基本上要对未来一年有相当保守的预期了。那报告下,过去两年还不错…

有网友和我说:过去 10 年股票赚 10 倍其实没什么了不起,一线房产 10 年也 10 倍了。这话可以这么说,但这账这么算就糊涂了。为什么呢? 因为房产这 10 年你赚的不是能力的钱,是运气的钱,是超级景气的钱。未来 10 年您还打算靠房子再翻 10 倍?但投资扎实获得的可不仅仅是钱,更值钱的是经验和能力。再过 10 年比比看?

其实吧,资产增值了怎么都是好事,但人要善于分析自己的钱是能力还是运气得来的。而且,千万别用资产景气高位区域的数字当成自己的身家,最真实的身家一定要在你的主要资产处于熊市萧条期去计算。当然,在很多人眼里,自己的资产是永远只有涨潮没有退潮。讲实话,时间放长了看,德不配位比周期轮替对财富增值的杀伤力更大。

我经常和朋友说:懂得把自己放低一点儿,净值就容易更高一点儿。所谓放低点儿就是接受世界运行的复杂性和市场生态的多样性,接受自己有很多不懂的东西这个事实。放低点儿后,进,可保留扩张能力范围提升自身实力的余地;退,可更加坦然的专注于自己熟悉和能懂的领域。在投资这个人人自命不凡的世界里,老天其实很多时候更关照笨小孩。

看几个公司的历史研究报告,翻到 15 年上半年期间的报告时,纷纷出现 “公司正在搭建互联网 + 大平台,应价值重估” 的内容。要不是这些历史证据,还真忘了那时的 “互联网 +” 大潮。仅仅 1 年半后,曾炙手可热的互联网 + 概念已经在市场上销声匿迹,各类报告也不再提及。一切就像个梦,一个昂贵而可笑的梦。

某以绝对收益为目标的基金,自 12 年 10 月底至今的收益率是 120% 左右,看起来尚可。但这种策略的另一面,是其产品成立的 09 年 4 月到 12 年 10 月底的 3 年半里净值几乎都盈亏 10% 以内上下波动,产品成立的 7 年里其收益的 90% 都是来自去年的牛市。这种只做强趋势其它时间近乎空仓的策略,长期看赚钱肯定没问题,但代价也很高昂。

公司的性格和投资人的性格一样分为很多种,比如有一种就是手里现有的主业一塌糊涂,但跟着产业风口(甚至是跨行业)投资制造想象力的本领却是一绝,这就像有一类股民把 “当下” 处理得一团糟,却总是忙于追逐热点希望抓住“未来”。后者最喜欢的追求对象还真就是前者,而且还就愿意给这类公司高估值,我觉得这除了爱情别的都没法解释。

两个同行业的公司。A 在更高负债率下的 ROE 只有 6%,毛利率比 B 低 50%,作为重资产公司 A 的总资产周转率也比 B 低 30%,行业萧条时亏得多景气回升时恢复的慢。另外体现技术水平的海外订单,A 海外占比不足 5% 且毛利率比国内还低,而 B 达到 25% 且海外毛利率更高。作为未来重要看点的项目,B 公司全是自主研发且打破海外公司垄断,A 则是到处投资参股尚处于概念。而市场给予 A 的估值是 B 的 1 倍,我只能笑笑。

我个人估计,未来十几年中国在半导体、汽车和商用客机领域会有惊人的发展。与之相伴,台湾和日韩的相关产业将遭受重大冲击。现在这看起来似乎痴人说梦,不过别忘了,仅仅 20 年前的 90 年代,松下东芝索尼的家电在我们看来也是神一样不可企及的存在。商用技术不是魔法,只要有庞大内需,又具备了充分资本后盾和基本的人才技术基础和产业决心,不成功其实才是奇怪的。

以中国未来能达到的经济体量而言,千亿市值的公司其实是不大的(按美元计只有 140 多亿)。目前两市的千亿级公司共 59 个,其中技术密集类的高端制造业勉强算也就是 7-8 个,剩下的都是金融地产和大国企垄断类。从逻辑上推导,未来中国如果要成功,目前数百亿市值的民营中高技术类企业批量上千亿是必要条件之一。

(17 年 3 月)现在这局面,就是战术上轻视战略上重视。战术上轻视是因为以天为时间轴看股市,将还有较为漫长的无聊期。战略上重视是因为以年为时间轴看大类资产,证券类资产将是下个周期最佳的财富进阶选择。从阶级流动的角度来看,房产已经固化,未来主要的机会一个是证券投资一个是风口创业。这些相比买房子真的难多了,但随着社会经济越来越高阶和成熟,跨越阶级流动的阻力和壁垒将越来越高是必然的。

(17 年 4 月)现在的局面,一方面是未来确定无疑的 IPO 持续大放量(实质注册制),另一方面是货币和利率大概率走向大周期的拐点,而目前房产价格处于泡沫顶端股票价格处于中高区间。所以从供需和估值两个维度看,现在应是保持平常心降低心理预期的时候。要想避免被动,大约第一是别把自己负债搞那么高,第二是远离高风险的资产,第三是留点后手。

对成长型公司来说,利润表永远是最被关注的,但其实资产负债表和现金流表是其必不可少的体检报告。一方面,相对而言利润表经常是财务操纵的重灾区,但三张表都勾兑得逻辑严密就很难;另一方面,资产负债表和现金流表也侧面反映了其增长的成色和潜力。换句话说,千疮百孔的资产和现金表之上的高增长往往很脆弱,就像满是空腔的沙滩上很难建起高楼大厦。

短期有业绩的高确定性,中期有增长的高弹性,远期有预期的可扩展性,是我个人最喜欢的类别。短中期的可预测性能为估值提供了基准,而中长期的成长逻辑不但能提升胜率还可降低机会成本。这两者的结合,才是更可靠的安全边际。其中短中期的决定因素是供需和产品节奏,中长期决定因素是跑道和经营者的层次。

如果说短期的重点是物有所值,那么中期衡量的重点就是价值释放的弹性和节奏,而远期关注的核心是能否持续的创造价值?短中期可以有机会主义的获利者,但中长期的大赢家一定是同时符合社会发展前进大趋势的 “天时”、有壁垒的行业和良好生意属性的“地利”,和强烈的产业雄心加企业家精神的“人和” 这三大必要条件。

上面说的短中长期成长驱动,是以时间为轴以规模弹性为判断点。与之搭配的,还有以结构为轴从利润弹性判断的视角。如果带动公司未来增长的业务具有更高的利润率,或者未来中期出现能降低成本和费用的趋势性有利环境,那么这种成长就是规模和利润率的双击。而如果又能在市场认识到这些之前把握住,那就又加了估值的第三击。如果持续期 n 再够长,其完整过程就是复利的暴击。什么叫成长投资?这就是。

快行业里的强公司与慢行业里的快公司,其成功本质都需要建立差异化竞争优势。但行业景气氛围下一方面市场给予的预期太高,另一方面受热捧的公司自己也容易头脑发热。而慢行业恰恰相反,市场关注度低溢价低,优秀公司也更踏实专注。但慢行业必须同时是大行业,整体增速低没关系,建立起差异化优质的公司靠集中度提升和掌握定价权,同样可以走出漂亮的成长股曲线。

一般价值投资理论认为,不需要研发和资本支出,且管理因素也不重要的生意是最好的。但我觉得随着现代社会需求变化频率升高,基本需求趋于饱和,这种躺着赚钱的生意越来越少,很难构成投资的主体了。靠持续性技术和资本投入,用供给创造新需求,并靠高效经营的量变到质变构建壁垒的机会会越来越重要。

有一天我惊讶的发现投资获利的方法,居然与骗子得手的秘诀高度一致:那就是都是从最傻最弱的人群身上得到钱。这个发现沉重的打击了我的职业荣誉感 ^^。

很多股民面对正确投资的态度,经常让我想起《江湖》里的一句台词: “说你又不听, 听又不懂, 懂又不做, 做又做错, 错又不认, 认又不改, 改又不服。”《人民的名义》里侯亮平也说过一句很经典的话:“他们的心里要么是恐惧,要么是贪婪,要么兼而有之,但唯独没有一点儿敬畏之心”。

一部经典谍战剧里有个特工说过一句话:” 在我们这个行当里,怀疑,是最好的品质”。那么,在投资这个行当里,最好的品质又是什么呢?如果一定要选择一个的话,我想是理性。

理性让世界有序,感性让世界多彩。理性让我们区别于动物,感性让人区别于机器。理性催生了科学,感性孕育了艺术。理性可以培养和训练,感性多是自然的萌发。失去理性很难美好的生活,失去感性很难拥有真正的生命。

从历史来看,中华民族最大的特点可能是韧性。从好的方面来说,这个民族只要还剩一口气都有可能翻盘,危亡之际却可能爆发最强反弹并创新高。从坏的方面说,由于特别能忍耐所以往往寻底之路异常漫长,不到危难的极端很难有改错的历史自觉。

日耳曼真是一个挺奇葩的民族,相对不约而同都要在股票和房地产泡沫上狠摔一跤(甚至是 N 跤)的那么多国家,德国的股市和房地产居然都能始终基本保持平稳甚至冷静的状态,服吧?可你要说是因为他们极端理性,纳粹的狂热历史和现今的圣母病又作何解释?我觉得只能说,老天爷不会让一个民族把技能树都点全,金钟罩练成了也得留个罩门。

有两种对手是比较可怕的,一种是胸怀大志又特别隐忍的,另一种是意志坚定还不安套路出牌的。川普恰好是后者,而中国又正好是前者。未来算是有好戏看了…… 历史的潮流到底是走向更进一步的全球化,还是重新回到关门自己垂直一体化?这可能是决定美国和中国未来实力变化的更深层力量。在历史关口时,逆流还是顺流比领导人能力甚至比体制本身的力量还大。

今年没少跑医院,和 10 年前相比真的变化很大。十几年前工资水平比现在低得多,但医疗费用几乎和现在没太大区别,医保还只是少数体制内人才能享有,要赶上个大病真的分分钟消灭一个当时的中等收入家庭。今年感觉医保真的能帮大忙,普通人的费用覆盖范围报销比例都远超预期。另外,我感觉当你尊重医生信任专业人士的时候,医生通常也更愿意和你坦率真诚的沟通。

有时候看得越多,就越感到资本市场真是太可爱了。在投资的世界里,你的智商不必被各种奇怪的观点强奸,你也不需要 “懂事儿”。所有的一切都归结为“对” 和“错”,如果短期内对错还不那么分明的话,还有 “时间” 这个大杀器肯定可以还你公道。在这个黑白分明的世界里,傻逼就得承担傻逼的后果,是简单粗暴了点儿,但总比黑白不分的操蛋状况强得多。

人之所以平庸,是在平庸的机会上花费了太多的精力。当然如果连这点儿努力都没付出,那就不是平庸而是惨淡了。但要想超越平均水平,就需要把精力用来寻找和把握人生级别的大机会。而这又是以长期的思索、通透的判断体系为前提,并以异乎寻常的耐心和超乎常人的利益计算模式为基础的。说起来容易,但要真想超越思维和决策的舒适区,还是很艰难的。

投资的窍门就是巧方法 + 笨功夫。巧方法是指方法论体系,让你事半功倍,做到高效聪明的整合信息碎片形成完整认识;笨功夫,就是踏踏实实花时间花精力一点点去阅读、 搜集和积累。市场里 80% 的人被笨功夫这层就淘汰了,基本失去超额收益的入围资格。剩下的又有一半儿被巧方法挡住了,原地打转收益与付出无法匹配。所以相当赢家,既要甘于 “笨” 又要善于“巧”。

相比世界上的很多东西来说,人性都算是相当可靠的。遗憾的是,人性恶的那部分,总是更可靠一些。

翻翻这些年的微博热点事件,大体可以得出一个结论:朋友圈里有三种虽然不常用但一旦用到就能帮大忙的朋友,一种医生,一种是投资人,一种是律师。这三个领域都是水特深,对人生影响特大,又靠谱资源高度稀缺的。再往后看 10 年,这三个专业领域只会越来越吃香,未来的丈母娘青睐指数大概率是大牛股形态。

巴菲特用指数基金与对冲基金的 10 年 PK,证明了对普通人来说最好的长期投资途径就是指数基金。但是然并卵,这个结果有一个非常高的壁垒就是你必须承认自己是 “普通人”,这对每一个来到股市的玩家都是很难接受的前提(包括我自己)。这个游戏最讽刺的一点在于,赚那些最自命不凡的普通人的钱是让我们自己从普通人里脱引而出的最好的办法。

巴菲特最可怕的就是永远在学习进化,中国的 “巴式投资总结(不碰科技只买消费)” 被某些人奉为圣经,可人家转眼就重仓了苹果和航空公司。看看巴菲特这一生的投资进化,从早年间看图玩技术,到最纯正的格雷厄姆捡烟头,再到创立新的商业模式并融合了芒格的伟大公司论,到快 90 了还有改变自己的能力,就这点都足以封神称圣了。

芒格曾说 “长期持有的收益约等于公司的 ROE”。但这并不简单。长期持有会熨平阶段波动,最终其收益取决于公司的 ROE。但这个 ROE 锚定在哪儿是个问题。其应该是各个周期跌宕后或者从增长期到均衡期的均值。所以用当前或者最近区间的 ROE 来衡量长期持有收益肯定是错的。也正因为如此,弱周期以及均衡期后 ROE 依然可保持中高水平的,获得估值溢价就显得合理了。

总有朋友对我说:看你每天晃来晃去的,投资就那么轻松啥都不用干?我说当然不是,只不过我们工作的形态不同。一般工作的繁忙很容易识别,但投资工作其实就是三件事:学习、思考、做决定。除了学习研究是较容易看到的,更重要的 “思考” 和“做决定”完全可以一点儿外在表现都没有,但其实它们才是最重要的可积累无形资产。

职业投资人的社交很单纯,不需要维系多余的社会关系。在主动筛选后能沉淀下来的是三种人:第一种是有料。某一方面的学识好,能弥补自己的知识短板开拓思维,与之交流有益;第二种是有趣。或风趣豁达或古灵精怪,与之交往总能给生活带来一抹亮色;第三种是有品。就是能信任靠得住,价值观相近可坦率的说心里话。当然如果能多条兼顾,自然是交友的精品。

很多次被问到过”职业投资会不会让人的社会交往能力退化 “?我是这样觉得:与人真诚交往的能力永远不会退化,除非你从没学会过真诚。但社交的技术套路确实有可能退化,比如不管喜不喜欢都要学会周旋赔笑的能力,看人脸色揣摩心意的能力,复杂人际间” 懂事儿“的敏感性等等。确切的说也不是退化,而是没必要和懒得那么累。

投资很难赚到你不信的那份钱。往小了说,对一个公司没有充分的信心,最多只能赚到财务数据直接相关的那点儿钱,再远点长点的钱是不可能赚到的。往大了说,对国家未来从心底悲观,那么最多赚到几个波段的小聪明的钱,要赚大钱也很难了。格局、历史感这些东西在 99% 的时候都很虚,但在 1% 的重要决策时刻往往就是强大信念的真正支撑点。

原文地址 http://blog.chenpeng.info/html/3867

JavaScript 的 this 原理

一、问题的由来

学懂 JavaScript 语言,一个标志就是理解下面两种写法,可能有不一样的结果。


var obj = {
  foo: function () {}
};

var foo = obj.foo;

// 写法一
obj.foo()

// 写法二
foo()

上面代码中,虽然obj.foofoo指向同一个函数,但是执行结果可能不一样。请看下面的例子。


var obj = {
  foo: function () { console.log(this.bar) },
  bar: 1
};

var foo = obj.foo;
var bar = 2;

obj.foo() // 1
foo() // 2

这种差异的原因,就在于函数体内部使用了this关键字。很多教科书会告诉你,this指的是函数运行时所在的环境。对于obj.foo()来说,foo运行在obj环境,所以this指向obj;对于foo()来说,foo运行在全局环境,所以this指向全局环境。所以,两者的运行结果不一样。

这种解释没错,但是教科书往往不告诉你,为什么会这样?也就是说,函数的运行环境到底是怎么决定的?举例来说,为什么obj.foo()就是在obj环境执行,而一旦var foo = obj.foofoo()就变成在全局环境执行?

本文就来解释 JavaScript 这样处理的原理。理解了这一点,你就会彻底理解this的作用。

二、内存的数据结构

JavaScript 语言之所以有this的设计,跟内存里面的数据结构有关系。


var obj = { foo:  5 };

上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj

也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。


{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo属性的值保存在属性描述对象的value属性里面。

三、函数

这样的结构是很清晰的,问题在于属性的值可能是一个函数。


var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。


{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。


var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()

四、环境变量

JavaScript 允许在函数体内部,引用当前环境的其他变量。


var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。


var f = function () {
  console.log(this.x);
}

上面代码中,函数体里面的this.x就是指当前运行环境的x


var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x

obj环境执行,this.x指向obj.x

回到本文开头提出的问题,obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行。

原文地址 http://www.ruanyifeng.com/blog/2018/06/javascript-this.html

记住,永远不要在MySQL中使用“utf8”编码

最近工作中我遇到了一个 bug,我试着通过 Rails 在以 “utf8” 编码的 MariaDB 中保存一个 UTF-8 字符串,然后出现了一个离奇的错误:

Incorrect string value: ‘\xF0\x9F\x98\x83 <…’ for column ‘summary’ at row 1

我用的是 UTF-8 编码的客户端,服务器也是 UTF-8 编码的,数据库也是,就连要保存的这个字符串 “xxxx <…” 也是合法的 UTF-8。

问题的症结在于,MySQL 的 “utf8” 实际上不是真正的 UTF-8。

“utf8” 只支持每个字符三个字节,而真正的 UTF-8 是每个字符最多四字节

MySQL 一直没有修复这个 bug,他们在 2010 年发布了一个叫作 “utf8mb4” 的字符集,绕过了这个问题。

当然,他们并没有对新的字符集广而告之(可能是因为这个 bug 让他们觉得很尴尬),以致于现在网络上仍然在建议开发者使用 “utf8”,但这些建议都是错误的。

简单概括如下:

  • MySQL 的 “utf8mb4” 是真正的“UTF-8”。
  • MySQL 的 “utf8” 是一种“专属的编码”,它能够编码的 Unicode 字符并不多。

我要在这里澄清一下:所有在使用 “utf8” 的 MySQL 和 MariaDB 用户都应该改用“utf8mb4”,永远都不要再使用“utf8”。

那么什么是编码?什么是 UTF-8?

我们都知道,计算机使用 0 和 1 来存储文本。比如字符 “C” 被存成“01000011”,那么计算机在显示这个字符时需要经过两个步骤:

  1. 计算机读取 “01000011”,得到数字 67,因为 67 被编码成 “01000011”。
  2. 计算机在 Unicode 字符集中查找 67,找到了 “C”。

同样的:

  1. 我的电脑将 “C” 映射成 Unicode 字符集中的 67。
  2. 我的电脑将 67 编码成 “01000011”,并发送给 Web 服务器。

几乎所有的网络应用都使用了 Unicode 字符集,因为没有理由使用其他字符集。

Unicode 字符集包含了上百万个字符。最简单的编码是 UTF-32,每个字符使用 32 位。这样做最简单,因为一直以来,计算机将 32 位视为数字,而计算机最在行的就是处理数字。但问题是,这样太浪费空间了。

UTF-8 可以节省空间,在 UTF-8 中,字符 “C” 只需要 8 位,一些不常用的字符,比如 “xxxx” 需要 32 位。其他的字符可能使用 16 位或 24 位。一篇类似本文这样的文章,如果使用 UTF-8 编码,占用的空间只有 UTF-32 的四分之一左右。

MySQL 的 “utf8” 字符集与其他程序不兼容,它所谓的“xxxx”,可能真的是一坨……

MySQL 简史

为什么 MySQL 开发者会让 “utf8” 失效?我们或许可以从提交日志中寻找答案。

MySQL 从 4.1 版本开始支持 UTF-8,也就是 2003 年,而今天使用的 UTF-8 标准(RFC 3629)是随后才出现的。

旧版的 UTF-8 标准(RFC 2279)最多支持每个字符 6 个字节。2002 年 3 月 28 日,MySQL 开发者在第一个 MySQL 4.1 预览版中使用了 RFC 2279。

同年 9 月,他们对 MySQL 源代码进行了一次调整:“UTF8 现在最多只支持 3 个字节的序列”。

是谁提交了这些代码?他为什么要这样做?这个问题不得而知。在迁移到 Git 后(MySQL 最开始使用的是 BitKeeper),MySQL 代码库中的很多提交者的名字都丢失了。2003 年 9 月的邮件列表中也找不到可以解释这一变更的线索。

不过我可以试着猜测一下。

2002 年,MySQL 做出了一个决定:如果用户可以保证数据表的每一行都使用相同的字节数,那么 MySQL 就可以在性能方面来一个大提升。为此,用户需要将文本列定义为 “CHAR”,每个“CHAR” 列总是拥有相同数量的字符。如果插入的字符少于定义的数量,MySQL 就会在后面填充空格,如果插入的字符超过了定义的数量,后面超出部分会被截断。

MySQL 开发者在最开始尝试 UTF-8 时使用了每个字符 6 个字节,CHAR(1) 使用 6 个字节,CHAR(2) 使用 12 个字节,并以此类推。

应该说,他们最初的行为才是正确的,可惜这一版本一直没有发布。但是文档上却这么写了,而且广为流传,所有了解 UTF-8 的人都认同文档里写的东西。

不过很显然,MySQL 开发者或厂商担心会有用户做这两件事:

  1. 使用 CHAR 定义列(在现在看来,CHAR 已经是老古董了,但在那时,在 MySQL 中使用 CHAR 会更快,不过从 2005 年以后就不是这样子了)。
  2. 将 CHAR 列的编码设置为 “utf8”。

我的猜测是 MySQL 开发者本来想帮助那些希望在空间和速度上双赢的用户,但他们搞砸了 “utf8” 编码。

所以结果就是没有赢家。那些希望在空间和速度上双赢的用户,当他们在使用 “utf8” 的 CHAR 列时,实际上使用的空间比预期的更大,速度也比预期的慢。而想要正确性的用户,当他们使用 “utf8” 编码时,却无法保存像 “xxxx” 这样的字符。

在这个不合法的字符集发布了之后,MySQL 就无法修复它,因为这样需要要求所有用户重新构建他们的数据库。最终,MySQL 在 2010 年重新发布了 “utf8mb4” 来支持真正的 UTF-8。

为什么这件事情会让人如此抓狂

因为这个问题,我整整抓狂了一个礼拜。我被 “utf8” 愚弄了,花了很多时间才找到这个 bug。但我一定不是唯一的一个,网络上几乎所有的文章都把 “utf8” 当成是真正的 UTF-8。

“utf8” 只能算是个专有的字符集,它给我们带来了新问题,却一直没有得到解决。

总结

如果你在使用 MySQL 或 MariaDB,不要用 “utf8” 编码,改用 “utf8mb4”。这里提供了一个指南用于将现有数据库的字符编码从“utf8” 转成“utf8mb4”。链接如下:

https://mathiasbynens.be/notes/mysql-utf8mb4#utf8-to-utf8mb4

原文地址 https://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247487968&idx=1&sn=2ff7b511f6727c7816ab02fc0e1c0361&chksm=96c9a780a1be2e961cd5e7c5e5ff32961cd2b6c1bac480f8f1c3f281e5bf1504fecebdd59d48#rd

开源分布式监控 CAT 系统的高可用实践

线上发布了服务,怎么知道它一切正常?为什么一个低级错误,需要花一个通宵、十几个人来排错?某个核心服务挂了,导致大量报错,如何确定到底是哪里出了问题?应用程序有性能瓶颈,如何提供一些有效工具发现?该主题主要分享 CAT 系统的高可用架构设计思路、应用实践以及如何提高业务系统的敏捷性和伸缩性。


PPT下载 :http://res.infoqstatic.com/downloads/pdfdownloads/presentations-ch%2F201804_Xnode_wuqimin.pdf?expire=1524447250&digest=56ead3b80795acbdc946a8b13cf1e81f

原文地址:http://www.infoq.com/cn/presentations/the-practice-of-open-source-distributed-monitoring-cat-system?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global

JVM原理解析

java 程序运行

.java 文件 --> 编译器 -->.class 文件 --> 线程启动(main)-->jvm--> 操作系统 --> 硬件

通过上面的流程我们可以看出 java 程序的执行顺序,那么 jvm 到底是什么,class 文件到底是如何在 jvm 中运行就显得很重要了。

jvm 原理

什么是 jvm

openjdk 源码地址 http://hg.openjdk.java.net/jdk9

JVM 是一个计算机模型,JVM 对 Java 可执行代码,即字节码(Bytecode)的格式给出了明确的规格。这一规格包括操作码操作数的语法(也就是 cpu 指令集)和数值、标识符的数值表示方式、以及 Java 类文件中的 Java 对象、常量缓冲池在 JVM 的存储映象。

JVM 的组成

JVM 指令系统、JVM 寄存器、JVM 栈结构、JVM 碎片回收堆、JVM 存储区

JVM 指令

Java 指令也是由操作码和操作数两部分组成,与 RISC CPU 采用的编码方式是一致的,也就是精简指令集,目前 UNIX、Linux、MacOS 系统使用 RISC,我们目前知道的 x86 架构 CPU 使用的 CISC 编码,也就是复杂指令集。

JVM 寄存器

1.pc 程序计数器

2.optop 操作数栈顶指针

3.frame 当前执行环境指针

4.vars 指向当前执行环境中第一个局部变量指针

jvm 的装载

windows 操作系统装入 JVM 是通过 jdk 中 Java.exe 来完成, 通过下面 4 步来完成 JVM 环境。

1. 创建 JVM 装载环境和配置

2. 装载 JVM.dll(C:Program FilesJavajre1.8.0_151binserver linux 在 jre/lib/server 下)

3. 初始化 JVM.dll 并挂接到 JNIENV(JNI 调用接口) 实例

4. 调用 JNIEnv 实例装载并处理 class 类

JVM 虚拟机相当于 x86 计算机系统,Java 解释器相当于 x86CPU

JVM 运行数据

JVM 定义了若干个程序执行期间使用的数据区域。这个区域里的一些数据在 JVM 启动的时候创建,在 JVM 退出的时候销毁。而其他的数据依赖于每一个线程,在线程创建时创建,在线程退出时销毁。分别有程序计数器,堆,栈,方法区,运行时常量池**

  • 程序计数器:每个线程一旦被创建就拥有了自己的程序计数器。当线程执行 Java 方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那么程序计数器的值就不会被定义。
  • 常量缓冲池和方法区:常量缓冲池用于存储类名称、方法和字段名称以及串常量。方法区则用于存储 Java 方法的字节码。对于这两种存储区域具体实现方式在 JVM 规格中没有明确规定。这使得 Java 应用程序的存储布局必须在运行过程中确定,依赖于具体平台的实现方式。
  • 栈:Java 栈是 JVM 存储信息的主要方法。当 JVM 得到一个 Java 字节码应用程序后,便为该代码中一个类的每一个方法创建一个栈框架,以保存该方法的状态信息。每个栈框架包括以下三类信息:

    局部变量对应 vars 寄存器指向该变量表中的第一个局部变量,用于存储一个类的方法中所用到的局部变量。

    执行环境:对应 frame 寄存器的当前执行环境指针,用于保存解释器对 Java 字节码进行解释过程中所需的信息。它们是:上次调用的方法、局部变量指针和操作数栈的栈顶和栈底指针。执行环境是一个执行一个方法的控制中心。例如:如果解释器要执行 iadd(整数加法),首先要从 frame 寄存器中找到当前执行环境,而后便从执行环境中找到操作数栈,从栈顶弹出两个整数进行加法运算,最后将结果压入栈顶。

    操作数栈:对应 optop 寄存器的操作数栈顶指针,操作数栈用于存储运算所需操作数及运算的结果。

  • 堆:JVM 中最大的,应用的对象和数据都是存在这个区域,这块区域也是线程共享的,也是 gc 主要的回收区,一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。

       垃圾回收机制(GC)只发生在线程共享区,也就是堆和方法区, 栈不需要回收,线程销毁则栈也销毁,         也就是上图的 heap space 与 method area 会发生 gc。

        通过上图可以发现 heap space 被分为两部分:

  • Young Generation:又分为 Eden space 所有的类都是在 Eden space 被 new 出来的。From 区(Survivor 0 space)和 To 区(Survivor 1 space)。当 Eden space 空间用完时,程序又需要创建对象,JVM 的垃圾回收器将对 Eden space 进行垃圾回收(Minor GC), 将 Eden space 中的剩余对象移动到 From 区。若 From 区也满了,再对该区进行垃圾回收,然后移动到 To 区。那如果 To 区也满了呢,再移动到 Old 区。
  • Old Generation:若该区也满了,那么这个时候将产生 Major GC(FullGCC),进行 Tenured 区的内存清理。若该区执行 Full GC 之后发现依然无法进行对象的保存,产生异常 java.lang.OutOfMemoryError: Java heap space。
  1. Java 虚拟机的堆内存设置不够,可以通过参数 - Xms、-Xmx 来调整。
  2. 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
  • Permanent Generation:是一个常驻内存区域,用于存放 JDK 自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。产生异常 java.lang.OutOfMemoryError: PermGen space jdk1.8 之后已经不会再报报这个错误了。因为类信息的卸载几乎很少发生,这样会影响 GC 的效率。于是 PermGen 便被拆分出去了。
  1. 程序启动需要加载大量的第三方 jar 包。例如:在一个 Tomcat 下部署了太多的应用。
  2. 大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。

jvm 的算法

由于算法篇幅太长具体算法可自行查阅资料,主要介绍 gc 算法发生在什么区。

分代搜集算法:是由复制算法、标记 / 整理、标记 / 清除算法共同组成

复制算法发生在 Young Generation

标记 / 整理和标记 / 清除算法发生在 Old Generation 和 Permanent Generation

java 验证 jvm

栈中一般存放的都是对象的指针和基本类型,存取速度比堆要快,仅次于直接位于 CPU 中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

栈数据可以共享

/**
 * Created by liustc on 2018/4/20.
 */
public class JvmTest {

    public static void main(String[] args) {
        int a=0;
        int b=0;
        System.out.print(a==b);
    }
}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"
true
Process finished with exit code 0

编译器先处理 int a = 0;首先它会在栈中创建一个变量为 a 的引用,然后查找有没有字面值为 0 的地址,没找到,就开辟一个存放 0 这个字面值的地址,然后将 a 指向 0 的地址。接着处理 int b = 0;在创建完 b 的引用变量后,由于在栈中已经有 0 这个字面值,便将 b 直接指向 0 的地址。这样,就出现了 a 与 b 同时均指向 0 的情况

/**
 * Created by liustc on 2018/4/20.
 */
public class JvmTest {

    public static void main(String[] args) {
        int a=0;
        int b=0;
        a=1;
        System.out.print("a="+a);
        System.out.print("b="+b);
        System.out.print(a==b);
    }
}
"C:\Program Files\Java\jdk1.8.0_151\bin\java" 
a=1b=0false
Process finished with exit code 0

再令 a=1;那么,b 不会等于 1,还是等于 0。在编译器内部,遇到 a=1;时,它就会重新搜索栈中是否有 1 的字面值,如果没有,重新开辟地址存放 1 的值;如果已经有了,则直接将 a 指向这个地址。因此 a 值的改变不会影响到 b 的值。 

String str = "abc" 的工作原理

/**
 * Created by liustc on 2018/4/20.
 */
public class JvmTest {

    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        System.out.println(str1==str2);
    }
}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"
true

Process finished with exit code 0
/**
 * Created by liustc on 2018/4/20.
 */
public class JvmTest {

    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        str1 = "bcd";
        System.out.println(str1 + "," + str2);
        System.out.println(str1==str2);
    }
}
"C:\Program Files\Java\jdk1.8.0_151\bin\java" 
bcd,abc
false

Process finished with exit code 0

赋值的变化导致了类对象引用的变化,str1 指向了另外一个新对象!而 str2 仍旧指向原来的对象。上例中,当我们将 str1 的值改为 "bcd" 时,JVM 发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。

/**
 * Created by liustc on 2018/4/20.
 */
public class JvmTest {

    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";

        str1 = "bcd";

        String str3 = str1;
        System.out.println(str3);

        String str4 = "bcd";
        System.out.println(str1 == str4);
    }
}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"
bcd
true

Process finished with exit code 0

str3 这个对象的引用直接指向 str1 所指向的对象 (注意,str3 并没有创建新对象)。当 str1 改完其值后,再创建一个 String 的引用 str4,并指向因 str1 修改值而创建的新的对象。可以发现,这回 str4 也没有创建新的对象,从而再次实现栈中数据的共享。

堆验证

String 类 

/**
 * Created by liustc on 2018/4/20.
 */
public class JvmTest {

    public static void main(String[] args) {
        String str1 = new String("abc");
        String str2 = "abc";
        System.out.println(str1==str2);
    }
}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"
false

Process finished with exit code 0

以上代码说明,只要是用 new() 来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

使用 String str = "abc";的方式,可以在一定程度上提高程序的运行速度,因为 JVM 会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于 String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是 享元模式的思想。

由于 String 类的性质,当 String 变量需要经常变换其值时,应该考虑使用 StringBuffer 类,以提高程序效率。

执行时间上寄存器 < 堆栈 < 堆 

/**
 * Created by liustc on 2018/4/20.
 */
public class JvmTest {

    public static void main(String[] args) {
        String s1 = "ja";
        String s2 = "va";
        String s3 = "java";
        String s4 = s1 + s2;
        System.out.println(s3 == s4);
        System.out.println(s3.equals(s4));
    }
}
"C:\Program Files\Java\jdk1.8.0_151\bin\java"
false
true

Process finished with exit code 0

是不是很矛盾啊!是不是又懵逼了?

打印 false 的原因是,java 重载了 “+”,查看 java 字节码可以发现“+” 其实是调用了 StringBuilder 所以使用了 “+” 其实是生成了一个新的对象。所以 (s3 == s4) 打印 false

/**
 * Created by liustc on 2018/4/20.
 */
public class JvmTest {

    public static void main(String[] args){
        long maxMemory = Runtime.getRuntime().maxMemory();//返回Java虚拟机试图使用的最大内存量。
        Long totalMemory = Runtime. getRuntime().totalMemory();//返回jvm实例占用的内存。
        System.out.println("MAX_MEMORY ="+maxMemory +"(字节)、"+(maxMemory/(double)1024/1024) + "MB");
        System.out.println("TOTAL_ MEMORY = "+totalMemory +"(字节)"+(totalMemory/(double)1024/1024) + "MB");
    }
}
"C:\Program Files\Java\jdk1.8.0_151\bin\java" -XX:+PrintGCDetails
MAX_MEMORY =1868038144(字节)、1781.5MB
TOTAL_ MEMORY = 126877696(字节)121.0MB
Heap
 PSYoungGen      total 37888K, used 3932K [0x00000000d6400000, 0x00000000d8e00000, 0x0000000100000000)
  eden space 32768K, 12% used [0x00000000d6400000,0x00000000d67d7320,0x00000000d8400000)
  from space 5120K, 0% used [0x00000000d8900000,0x00000000d8900000,0x00000000d8e00000)
  to   space 5120K, 0% used [0x00000000d8400000,0x00000000d8400000,0x00000000d8900000)
 ParOldGen       total 86016K, used 0K [0x0000000082c00000, 0x0000000088000000, 0x00000000d6400000)
  object space 86016K, 0% used [0x0000000082c00000,0x0000000082c00000,0x0000000088000000)
 Metaspace       used 3325K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 363K, capacity 386K, committed 512K, reserved 1048576K

Process finished with exit code 0

将 jvm 堆初始值改小,触发 gc 回收

import java.util.Random;

/**
 * Created by liustc on 2018/4/20.
 */
public class JvmTest {

    public static void main(String[] args){
        long maxMemory = Runtime.getRuntime().maxMemory();//返回jvm试图使用的最大内存量。
        Long totalMemory = Runtime. getRuntime().totalMemory();//返回jvm实例的内存大小。
        System.out.println("MAX_MEMORY ="+maxMemory +"(字节)、"+(maxMemory/(double)1024/1024) + "MB");
        System.out.println("TOTAL_ MEMORY = "+totalMemory +"(字节)"+(totalMemory/(double)1024/1024) + "MB");
        String str = "www.baidu.com";
        while(true){
            str += str + new Random().nextInt(88888888) + new Random().nextInt(99999999);
        }
    }}
"C:\Program Files\Java\jdk1.8.0_151\bin\java" -XX:+PrintGCDetails
MAX_MEMORY =1868038144(字节)、1781.5MB
TOTAL_ MEMORY = 126877696(字节)121.0MB
[GC (Allocation Failure) [PSYoungGen: 32247K->2729K(37888K)] 32247K->10124K(123904K), 0.0045031 secs] [Times: user=0.01 sys=0.03, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 32912K->4469K(70656K)] 40307K->26638K(156672K), 0.0121112 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 66160K->776K(70656K)] 88329K->59879K(156672K), 0.0141096 secs] [Times: user=0.03 sys=0.02, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 776K->0K(70656K)] [ParOldGen: 59103K->37630K(116224K)] 59879K->37630K(186880K), [Metaspace: 3408K->3408K(1056768K)], 0.0143902 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 60370K->0K(70656K)] [ParOldGen: 96726K->74565K(172032K)] 157096K->74565K(242688K), [Metaspace: 3409K->3409K(1056768K)], 0.0598124 secs] [Times: user=0.08 sys=0.00, real=0.06 secs] 
[GC (Allocation Failure) [PSYoungGen: 60382K->32K(95744K)] 1257771K->1226968K(1463808K), 0.0227293 secs] [Times: user=0.06 sys=0.01, real=0.02 secs] 
[GC (Allocation Failure) [PSYoungGen: 32K->32K(131584K)] 1226968K->1226968K(1499648K), 0.0037586 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(131584K)] [ParOldGen: 1226936K->355271K(483840K)] 1226968K->355271K(615424K), [Metaspace: 3409K->3409K(1056768K)], 0.1616835 secs] [Times: user=0.19 sys=0.09, real=0.16 secs] 
[GC (Allocation Failure) [PSYoungGen: 2499K->32K(158208K)] 1303306K->1300838K(1526272K), 0.0037952 secs] [Times: user=0.06 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 32K->32K(158208K)] 1300838K->1300838K(1526272K), 0.0036491 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(158208K)] [ParOldGen: 1300806K->473463K(622080K)] 1300838K->473463K(780288K), [Metaspace: 3409K->3409K(1056768K)], 0.1641897 secs] [Times: user=0.30 sys=0.06, real=0.16 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(250880K)] 946230K->946230K(1618944K), 0.0027229 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(258560K)] 946230K->946230K(1626624K), 0.0027747 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(258560K)] [ParOldGen: 946230K->709846K(879104K)] 946230K->709846K(1137664K), [Metaspace: 3409K->3409K(1056768K)], 0.1013768 secs] [Times: user=0.28 sys=0.02, real=0.10 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(353280K)] 709846K->709846K(1721344K), 0.0049384 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
[PSYoungGen: 0K->0K(353280K)] [ParOldGen: 709846K->709816K(900608K)] 709846K->709816K(1253888K), [Metaspace: 3409K->3409K(1056768K)], 0.1792920 secs] [Times: user=0.39 sys=0.00, real=0.18 secs] 
Heap
    at java.util.Arrays.copyOf(Arrays.java:3332)
 PSYoungGen      total 353280K, used 14028K [0x00000000d6400000, 0x00000000ec700000, 0x0000000100000000)
    at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
  eden space 352768K, 3% used [0x00000000d6400000,0x00000000d71b3070,0x00000000ebc80000)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
  from space 512K, 0% used [0x00000000ec680000,0x00000000ec680000,0x00000000ec700000)
    at java.lang.StringBuilder.append(StringBuilder.java:208)
  to   space 4608K, 0% used [0x00000000ebe00000,0x00000000ebe00000,0x00000000ec280000)
    at JvmTest.main(JvmTest.java:15)
 ParOldGen       total 1368064K, used 709816K [0x0000000082c00000, 0x00000000d6400000, 0x00000000d6400000)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  object space 1368064K, 51% used [0x0000000082c00000,0x00000000ae12e1e8,0x00000000d6400000)
 Metaspace       used 3440K, capacity 4494K, committed 4864K, reserved 1056768K
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  class space    used 377K, capacity 386K, committed 512K, reserved 1048576K
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

Process finished with exit code 1
原文地址 https://my.oschina.net/u/3829817/blog/1798201

《Docker In Action》简明手册

Docker学习笔记

容器

1. 唯一标识符ID:

16进制编码的1024位数字

2. 创建和启动一个容器:

docker run --detach --name web nginx:latest

3. 创建但不启动容器:

docker create nginx

4. 将创建的容器ID写入文件:

docker create --cidfile /tmp/web.cid nginx

5. 运行交互式容器:

docker run --interactive --tty --link web:web --name web_test busybox:latest /bin/sh

docker run -it --link web:web --name web_test busybox:latest /bin/sh

6. 获取容器ID:

docker ps  -q

7. 获取最后创建的那个容器的截断ID:

    CID=$(docker ps -l -q)
    echo $CID

8. 查看正在运行的容器:

docker ps
docker ps -a

9. 重启web容器:

docker restart web

10. 停止web容器

docker stop web

11. 查看web容器的日志:

docker logs web
docker logs -f web

12. 运行web容器中的其它命令:

docker exec web ls
docker run --pid host busybox:latest ps

13. 查看Docker容器正在运行的进程:

docker top lamp-test
docker exec lamp-test ps

14. 创建没有PID命名空间的容器:

设置 --pid=host

docker run --pid host busybox:latest ps

15. 重命名已存在的web容器:

docker rename webid webid-old

16. 使用只读文件系统:

docker run -d --name wp --read-only wordpress:4

17. 检查容器元数据:

docker inspect --format "{{.State.Running}}" wp

18. 注入环境变量:

docker create \
    --env WORDPRESS_DB_HOST=<my database hostname> \
    --env WORDPRESS_DB_USER=site_admin \
    --env WORDPRESS_DB_PASSWORD=<password> \
    wordpress:4

19. 设置容器启动入口点命令:

docker run --entrypoint="act" wordpress:4 /entrypoint.sh

20. 清理容器:

docker rm wp

21. 强制删除容器:

docker rm -f wp
docker rm -vf $(docker ps -a -q)

22. 容器退出时自动清理:

docker run --rm --name auto-exit-test busybox:latest echo hello world

23. 运行特权容器:

docker run --rm --privileged ubuntu:latest id

镜像

1. 在仓库中查找镜像:

公开脚本构建的镜像在 AUTOMATD 列有一个OK标记
docker search mysql

2. 删除镜像:

docker rmi quay.io/dockerinaction/ch3_hello_registry
docker rmi busybox

3. 将镜像保存为文件:

docker save -o myfile.tar busybox:latest

4. 从Dockerfile 安装镜像:

docker build -t dia_ch3/dockerfile:latest ch3_dockerfile

5. 从文件中加载镜像:

docker load -i myfile.tar

6. 显示仓库里的镜像:

docker images
docker images -a

7. 从容器构建镜像:

docker commit hw_container hw_image

带上作者信息:

docker commit -a "@dockerinaction" -m "Added git" image-dev ubuntu-git

指定入口和命令:

docker run --name cmd-git --entrypoint git ubuntu-git
docker run --name rich-image_example-2 --entrypoint "/bin/sh" rie -c "echo \$ENV_EXAMPLE1 \$ENV_EXAMPLE2

8. 查看容器改动:

docker diff mod_ubuntu

9. 复制一个已有镜像:

docker tag myuser/myfirstrepo:mytag myuser/mod_ubuntu

10. 查看镜像的所有层:

docker history ubuntu-git:removed

11. 镜像导出:

docker export --output contents.tar export-test

12. 镜像导入:

docker import -c "ENTRYPOINT [\"/hello\"]" - dockerinaction/ch7_static < static_hello.tar

13. 使用Dockerfile:

docker build -tag ubuntu-git:auto .

存储卷

1. 绑定挂载存储卷:

docker run -d --name bmweb \
    -v ~/example-docs:/usr/local/apache2/htdocs \
    -p 80:80 \
    httpd:latest

2. 创建一个存储卷容器:

docker run -d \
    --volume /var/lib/cassandra/data \
    --name cass-shared \
    alpine echo Data Container
卷容器并不需要运行,因此停止时容器仍能保证存储卷的引用。

3. 继承存储卷:

docker run -d \
    --volumes-from cass-shared \
    --name cass1 \
    cassandra:2.2

4. 创建容器使用存储卷:

docker run -it --rm \
    --link cass1:cass \
    cassandra:22 cqlsh cass

5. 查看存储卷在主机上的真实路径:

docker inpsect -f "{{json .Volumes}}" cass-shared

6. 删除管理卷,使用-v:

docker rm -v student

运行个人Registry

docker run -d --name personal_registry -p 5000:5000 --restart=always registry:2

Dockerfile

1.忽略文件:

创建一个.dockerignore文件

2. 元数据指令:

FROMMAINTAINERRUNENVLABELENTRYPOINTEXPOSEWORKDIRUSERCMD

3. 文件系统指令:

COPYVOLUMEADDONBUILD

Run-as用户

1. 查看默认用户:

docker run --rm --entrypoint "" busybox:latest whoami
docker run --rm --entrypoint "" busybox:latest id

2. 查看镜像可用用户名列表:

docker run --rm busybox:latest awk -F: '$0=$1' /etc/passwd

3. 设置run-as用户:

docker run --rm  --user nobody busybox:latest id
docker run --rm -u nobody:default busybox:latest id
docker run --rm -u 10000:20000 busybox:latest id

资源分配

1. 内存限制:

docker run -d --name ch6_mariadb --memory 256m  dockerfile/mariadb

2. CPU限制:

docker run -d -P --name ch6_wordpress --cpu-shares 512 --link ch6_mariadb wordpress:4.1

3. 设备访问权:

docker -it --rm --device /dev/video0:/dev/video0 ubuntu:latest ls -al /dev

4. 共享内存:

docker -d --name ch6_ipc_consumer \
    --ipc container:ch6_ipc_producer \
dockerinaction/ch6_ipc -consumer

5. 开放内存容器:

docker -d --name ch6_ipc_producer --ipc host dockerinaction/ch6_ipc -producer

跨容器信赖

1. 示例:

docker run -d --name importantData --expose 3306 dockerinaction/mysql_noauth service mysql_noauth start
docker run -d --name importantWebApp --link importantData:db \
dockerinaction/ch5_web startapp.sh -db tcp://db:3306

2. 链接别名:

docker run --link a:alias-a --link b:alias-b --linkk c:alias-c ...

3. 创建链接并列出所有环境变量:

docker run -it --rm --link mydb:database dockerinaction/ch5_ff env

网络访问

单主机虚拟网络

Closed容器

docker run --rm --net none alpine:latest ip addr

Joined容器

docker run -d --name brady \
    -net none alphine:latest \
    nc -l 127.0.0.1:3333

docker run -it -net container:brady \
    alphine:latest netstat -al

Bridge容器

1. 访问外部网络:

docker run --rm  alpine:latest ping -w 2 8.8.8.8

2. 自定义命名解析:

docker run --rm --hostname barker alpine:latest nslookup barker

3. 自定义DNS服务器:

docker run --rm --dns 8.8.8.8 alpine:latest nslookup docker.com
docker run --rm --add-host test:10.10.10.255 alpine:latest nslookup test

4. 开放对容器的访问:

暴露端口:

docker run -p 3333 ...
docker run -p 3333:3333 ...
docker run -p 192.168.0.32::2222 ...
docker run -p 192.168.0.32:1111:1111 ...

暴露所有端口:

docker run -d --name dawson -p 5000 -p 6000 -p 7000 dockerinaction/ch5_expose
docker run -d --name dawson -P dockerinaction/ch5_expose

增加端口暴露:

docker run -d --name philbin --expose 8000 -P dockerinaction/ch5_expose

查看端口映射:

docker port philbin

5. 关闭容器之间的网络连接:

docker -d -icc=false ...

Open容器

docker run --rm --net host alpine:latest ip addr

阿里架构师分享的Java程序员需要突破的技术要点

一、源码分析

源码分析是一种临界知识,掌握了这种临界知识,能不变应万变,源码分析对于很多人来说很枯燥,生涩难懂。

源码阅读,我觉得最核心有三点:技术基础 + 强烈的求知欲 + 耐心。

我认为是阅读源码的最核心驱动力。我见到绝大多数程序员,对学习的态度,基本上就是这几个层次 (很偏激哦):

1、只关注项目本身,不懂就 baidu 一下。

2、除了做好项目,还会阅读和项目有关的技术书籍,看 wikipedia。

3、除了阅读和项目相关的书外,还会阅读 IT 行业的书,比如学 Java 时,还会去了解函数语言,如 LISP。

4、找一些开源项目看看,大量试用第三方框架,还会写写 demo。

5、阅读基础框架、J2EE 规范、Debug 服务器内核。

大多数程序都是第 1 种,到第 5 种不光需要浓厚的兴趣,还需要勇气:我能读懂吗?其实,你能够读懂的

耐心,真的很重要。因为你极少看到阅读源码的指导性文章或书籍,也没有人要求或建议你读。你读的过程中经常会卡住,而一卡主可能就陷进了迷宫。这时,你需要做的,可能是暂时中断一下,再从外围看看它:如 API 结构、框架的设计图。

下图是我总结出目前最应该学习的源码知识点:

二、分布式架构

分布式系统是一个古老而宽泛的话题,而近几年因为 “大数据” 概念的兴起,又焕发出了新的青春与活力。除此之外,分布式系统也是一门理论模型与工程技法并重的学科内容。相比于机器学习这样的研究方向,学习分布式系统的同学往往会感觉:“入门容易,深入难”。的确,学习分布式系统几乎不需要太多数学知识。

分布式系统是一个复杂且宽泛的研究领域,学习一两门在线课程,看一两本书可能都是不能完全覆盖其所有内容的。

总的来说,分布式系统要做的任务就是把多台机器有机的组合、连接起来,让其协同完成一件任务,可以是计算任务,也可以是存储任务。如果一定要给近些年的分布式系统研究做一个分类的话,我个人认为大概可以包括三大部分:

1. 分布式存储系统

2. 分布式计算系统

3. 分布式管理系统

下图是我总结近几年目前分布式最主流的技术:

三、微服务

当前微服务很热,大家都号称在使用微服务架构,但究竟什么是微服务架构?微服务架构是不是发展趋势?对于这些问题,我们都缺乏清楚的认识。

为解决单体架构下的各种问题,微服务架构应运而生。与其构建一个臃肿庞大、难以驯服的怪兽,还不如及早将服务拆分。微服务的核心思想便是服务拆分与解耦,降低复杂性。微服务强调将功能合理拆解,尽可能保证每个服务的功能单一,按照单一责任原则(Single Responsibility Principle)明确角色。 将各个服务做轻,从而做到灵活、可复用,亦可根据各个服务自身资源需求,单独布署,单独作横向扩展。

下图是我总结出微服务需要学习的知识点:

四、性能优化

不管是应付前端面试还是改进产品体验,性能优化都是躲不开的话题。

优化的目的是让用户有 “快” 的感受,那如何让用户感受到快呢?

  1. 加载速度真的很快,用户打开输入网址按下回车立即看到了页面
  2. 加载速度并没有变快,但用户感觉你的网站很快

性能优化取决于多个因素,包括垃圾收集、虚拟机和底层操作系统(OS)设置。有多个工具可供开发人员进行分析和优化时使用,你可以通过阅读 Java Tools for Source Code Optimization and Analysis 来学习和使用它们。

必须要明白的是,没有两个应用程序可以使用相同的优化方式,也没有完美的优化 java 应用程序的参考路径。使用最佳实践并且坚持采用适当的方式处理性能优化。想要达到真正最高的性能优化,你作为一个 Java 开发人员,需要对 Java 虚拟机(JVM)和底层操作系统有正确的理解。

下图是我总结性能优化应该学习理解的几大知识体系:

五、Java 工程化

工欲善其事,必先利其器,不管是小白,还是资深开发,都需要先选择好的工具。提升开发效率何团队协作效率。让自己有更多时间来思考。


原文地址 https://my.oschina.net/u/3771478/blog/1796443