如何做技术选型?

曾经在文章中说过一句话,大意是说判断架构的牛逼与否,不是看它有多复杂,而是看它在解决同样问题时有多简单。有同学回复说,牛逼的架构是靠时间验证,跟当时解决问题的复杂和简单没啥关系。这话当然没有问题,任何牛逼的架构都需要时间来验证,但我的本意其实是说在做架构设计时,应当尽量追求简单,坚决砍掉一些可有可无的设计。

说句题外话,身边的工程师朋友比较多,所以倒闭的公司看到的也挺多。很多公司一上来就高并发、分布式、分库分表,但真的,到最后死的时候,数据也没能把一张 MySQL 表装满。可能是看得多了的缘故,不管是做架构还是写代码,就特别推崇 KISS 原则,所以才有了前面的那些话,但这仅是个人感悟,绝非通用的设计原则。

封面图 Photo by Daria Nepriakhina

作为架构设计的重要环节,技术选型的思路和原则应当与我们之前所学一样。那如何为业务场景选择适合的实现技术?又如何构建自己的技术选型方法论?

深入理解JVM运行时内存结构(续)

JVM 以方法作为执行的基本单位,栈帧(Stack Frame)则是用于支持 JVM 进行方法调用和执行的数据结构。每个方法在执行的同时都会创建一个栈帧用于存储方法的局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。每一个方法从调用开始至执行结束,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。当方法调用结束,栈帧即被销毁,局部变量表、操作数栈等也随之消失。

Java 代码在编译时就已经计算出栈帧需要多大的局部变量表,需要多深的操作数栈,并把它们写入到方法表的 Code 属性之中。换句话说,一个栈帧需要分配多少内存,在编译时已经确定,并不会受到程序运行期间变量数据的影响,仅取决于源码和具体的虚拟机是如何实现栈帧的。

深入理解 JVM 对象模型

深入理解JVM运行时内存结构 中,我们宏观地探讨了 JVM 运行时的内存布局。本文将深入分析每个内存区域的细节。由于不同虚拟机的内存管理实现不同,具体的讨论需要聚焦于特定虚拟机和内存区域。基于实用优先的原则,我们以最常用的 HotSpot 虚拟机和 Java 堆内存为例,深入探讨在 HotSpot 虚拟机里:

  1. 对象在堆内存中是如何布局的?
  2. JVM 是如何实现 Java 对象?
  3. 对象是如何创建出来的?

对象的内存布局

在虚拟机里,对象由三部分构成,分别是 对象头实例数据对齐填充。对象头的结构复杂,下面会详细介绍。实例数据是对象真正存储的有效信息,包含所有我们在代码中定义的各种类型的字段的内容,无论是继承自父类还是在子类中定义的。最后是对齐填充,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

接下来,我们详细说说对象头。HotSpot 虚拟机对象的对象头由三部分构成,分别是存储对象自身运行时数据的 Mark Word、指向类型元数据的指针、当对象是数组时,记录数组长度的 length。在 64 位系统中,对象头的大小是 16 个字节,可以通过指针压缩的方式,压缩到 12 个字节。当 JVM 中存在大量对象的时候,通过指针压缩减少对象内存占用是一个提升性能的手段。

需要注意的是,并不是所有虚拟机在实现对象时,都会在对象头中保留类型指针,还可以通过其它方式来查找对象类型的元数据信息,这点会在最后的「对象的访问」小节中补充讨论。

深入理解 JVM 垃圾回收机制 - 何为垃圾?

随着编程语言的发展,GC 的功能不断增强,性能也不断提高,作为语言背后的无名英雄,GC 离我们的工作似乎越来越远。作为 Java 程序员,对这一点也许会有更深的体会,我们不需要了解太多与 GC 相关的知识,就能很好的完成工作。那还有必要深入了解 GC 吗?学习 GC 的意义在哪儿?

何为垃圾题图

不管性能提高到何种程度,GC 都需要花费一定的时间,对于实时性要求较高的场景,就必须尽量压低 GC 导致的最大暂停时间 (GC 会导致应用线程处于暂停状态),举两个例子:

  • 实时对战游戏:如果因为 GC 导致玩家频繁卡顿,任谁都会想摔手机吧。
  • 金融交易:在某些对价格非常敏感的交易(比如:外汇)场景下,如果因为 GC 导致没有按照投资者指定的价格进行交易,相信我,这些投资者非生吃了你。

但也有许多场景,GC 的最大暂停时间没那么重要,比如,离线分析、视频网站等等。因此,知道 这个 GC 算法有这样的特征,所以它适合这个场景,对程序员来说非常有价值,这也是我们学习 GC 最重要的意义。

接下来,我们将一步步走进 GC 的世界。

深入理解JVM运行时内存结构

得益于 JVM 的自动内存管理机制,开发者在写代码时,很少再去关注内存分配与释放。多数情况下,应用不会出现内存泄漏和溢出问题。不过,由于开发者把内存的控制权交给了 JVM,一旦出现内存泄露和溢出问题,如果不了解 JVM 是怎样使用内存的,将很难排查和修正错误。本文从概念上介绍 JVM 运行时内存的各个区域及其作用。

JVM 在执行程序时会把其所管理的内存划分成多个不同的数据区域,每个区域的创建时间、销毁时间以及用途都各不相同。比如有的内存区域是所有线程共享的,而有的内存区域是线程隔离的。线程隔离的区域就会随着线程的启动和结束而创建和销毁。JVM 所管理的内存将会包含以下几个运行时数据区域,如下图所示。
JVM运行时内存数据区示意图

深入理解JVM常量池与字节码

在 JVM 中,常量池可以分成 Class 文件常量池、运行时常量池、字符串常量池三类。

Class 文件常量池

Java 源文件经编译后得到存储字节码的 Class 文件,Class 文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中。也就是说,哪个字节代表什么含义,长度多少,先后顺序如何都是被严格限定的,是不允许改变的。比如:开头的 4 个字节存放魔数,用于确定这个文件是否能够被 JVM 接受,接下来的 4 个字节用于存放版本号,再接着存放的就是常量池。常量池的长度是不固定的,所以,在常量池的入口存放着常量池容量的计数值。

常量池主要用于存放两大类常量:字面量和符号引用量,字面量相当于 Java 语言层面常量的概念,比如:字符串常量、声明为 final 的常量等等。符号引用是用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。

深入理解 ContextClassLoader

类的加载器大多都遵循双亲委托机制,它不仅可以避免一个类被重复加载,也能够避免 Java 核心类被篡改,越基础的类由越上层的类加载器加载。但程序设计往往没有绝对不变的规则,如果有核心类想要调用用户的代码,那该怎么办?

深入理解 ClassLoader

ClassLoader 源码初探

类加载器用于加载 Java 类到虚拟机中,其实现类 java.lang.ClassLoader 是一个抽象类,其职责是通过指定类的完全限定名 ( binary name ),找到或生成这个类对应的字节码,这些字节码中包含类的定义数据,通过字节码就可以构造出一个 java.lang.Class 对象。

每个 Class 对象都包含一个定义它的类加载器的引用,而数组的 Class 对象不是用类加载器创建的,而是在 Java 运行时根据需要自动创建的,如果调用数组的 Class 对象的 getClassLoader() 方法返回的类加载器与其元素类型的类加载器相同,如果数组元素是基本数据类,则没有类加载,返回空。比如:

// C为自定义类
C[] cs = new C[2];
// jdk.internal.loader.ClassLoaders$AppClassLoader@3d4eac69
System.out.println(cs.getClass().getClassLoader());

int[] is = new int[2];
// null
System.out.println(is.getClass().getClassLoader());
深入理解JVM类加载机制

Java 代码在编译过后,想要被运行和使用,经过的第一个步骤就是将编译后的字节码文件加载的虚拟机,那虚拟机是如何把字节码文件加载到虚拟机的呢,接下来以一系列实例对这一流程作简要分析与介绍。

类编译过程

Java 源码通过 JDK 自带的 Javac 编译器编译成 class 文件,我们把 class 文件中包含的代码称为字节码。但字节码并不能直接在计算机上运行,所以 JVM 需要将字节码翻译成机器码,其大致流程如下图所示。
Java类编译过程示意图
一般会把.java文件编译成.class文件的过程称为前端编译,其由 JDK 自带的 javac 工具完成。前端编译是一个非常复杂的过程,包含词法分析、注解处理、语义分析等,因此,代码中的大部分语法错误,在这个阶段就能够被识别出来。

编译后的字节码文件中主要包含常量池和方法表集合两个部分,可以通过 Javap 命令来查看,后面的文章也会详细的讲解。

接下来,JVM 会把 class 文件中的字节码加载到内存中,经过连接和初始化以后,类在调用执行的过程中,会继续把字节码翻译成机器码,这个翻译过程在 Hotspot 中有两种形式:

  • 解释执行:逐条将字节码翻译成机器码并执行
  • 即时编译:将一个方法中所包含的所有字节码编译出机器码后再执行

在起始阶段,JVM 中的字节码是由解释器(Interpreter)完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁时,这些代码块就会被认定为热点代码。为了提高热点代码的执行效率,在运行时,JIT 会把这些代码编译成本地平台相关的机器码,并进行各层次的优化,然后保存到内存中,这一过程就被称为即时编译。

这种将两种编译方式混合执行的策略,在上图中被描述为“运行时编译”,也是 Hotspot 默认采用的策略。

非奋斗减肥:关于运动和减脂的一些小误区

最近一段时间想控制下体重,随手翻的健康类书籍比较多,在这本「非奋斗减肥」中确实收获到不少与常规认知有差异的知识。

Part ONE

首先,介绍几种日常生活中随处都能听到,但又说不明白的几种物质。

胰岛素

当我们摄入糖类食物时,食物中的糖开始被不同的酶分解,最终被小肠吸收后转化成为葡萄糖,通过血液输送到全身。血液中的葡萄糖转化为糖原后,会优先储存在肝脏和肌肉当中,但是肝脏和肌肉中储存糖原的空间十分有限。剩余的大量糖原就会进入脂肪细胞,转化成为脂肪储存在人体内,于是肥胖就形成了。

当血液中的葡萄糖升高时,胰腺就开始工作了。胰腺会分泌胰岛素,胰岛素的作用就是促进血液中的葡萄糖转移到细胞中,帮助调节血糖,保证血糖值维持在一定的安全水平。因此,你摄入的糖越多,胰腺就要分泌更多的胰岛素来调节血糖,那么就会有更多的糖原进入脂肪细胞中。更重要的是,如果血液中的胰岛素水平长期居高不下,那么细胞对胰岛素的敏感度就会降低,形成胰岛素抵抗

胰岛素抵抗,跟抗生素的耐药性是一样的道理,对于有胰岛素抵抗的人来说,同等剂量的胰岛素无法使原先血液中的糖原进入细胞,于是胰腺就需要分泌更多的胰岛素才能把血糖降下来。而胰岛素又是促进脂肪合成的激素,这些大量的胰岛素会使身体合成更多的脂肪,所以胰岛素抵抗是现代人日渐肥胖的主要原因之一。胰岛素抵抗最容易导致腹部脂肪堆积。如果你的腰围和臀围的比例大于 0.9,就有较大可能存在明显的胰岛素抵抗。

因此,胰岛素又被称为肥胖激素,想要把胰岛素控制在很低的水平,关键就是控制糖的摄入。