码农戏码

新生代农民工的自我修养

0%

1、为什么需要自定义类加载器

  1. 《类加载器》中讲的,默认类加载器只能加载固定路径下的class,如果有特定路径下的class,需要自定义
  2. 安全性:系统自身需要一些jar,class,如果业务类代码中也有相同的class,破坏系统,类似双亲委托安全性

可以看看tomcat自定义类加载器的原因,别的就大同小异了

1
2
3
4
5
6
7
8
a)、要保证部署在tomcat上的每个应用依赖的类库相互独立,不受影响。
b)、由于tomcat是采用java语言编写的,它自身也有类库依赖,为了安全考虑,tomcat使用的类库要与部署的应用的类库相互独立。
c)、有些类库tomcat与部署的应用可以共享,比如说servlet-api,使用maven编写web程序时,servlet-api的范围是provided,
表示打包时不打包这个依赖,因为我们都知道服务器已经有这个依赖了。
d)、部署的应用之间的类库可以共享。这听起来好像与第一点相互矛盾,但其实这很合理,类被类加载器加载到虚拟机后,
会生成代表该类的class对象存放在永久代区域,这时候如果有大量的应用使用spring来管理,如果spring类库不能共享,
那每个应用的spring类库都会被加载一次,将会是很大的资源浪费。

2、自定义加载器

这儿主要说下我司的自定义类加载器;更复杂点的可以看看tomcat的类加载机制

为什么需要自定义类加载器?这可以参考章节1的答案

主要在于应用与基础平台的隔离,相对应用:可以有更大技术选型自由度,不用考虑基础平台的jar包版本、相对平台:更可靠安全,不被应用class影响

类加载器结构

虽然JAVA使用了类加载的委派机制,但并没严格要求开发者必须遵守该机制,我们可以打破这种”双亲委派”机制

目录结构

目录 说明
/servicesdir 业务实现jar包
/thirddir 业务依赖jar包
/platformdir 平台依赖jar包

类加载器

  • 1.PlatformClassLoader平台加载器
    • 1.1.加载/platformdir下的jar包
    • 1.2.在加载时,采用了默认的“双亲委派”
  • 2.AppClassLoader应用加载器
    • 2.1.加载/servicesdir,/thirddir下的jar
    • 2.2.该类加载器一定程度上打破了默认的“双亲委派”
      • 2.2.0.loadClass方法中,如果本加载器没有load到对应的类,则会检查该类是否处于平台类加载器白名单中:
      • 2.2.1.如果处于白名单中,则委派PlatformClassLoader加载
      • 2.2.2.否则,通过super.loadClass(String,boolean)走默认的双亲委派

此处白名单类:平台核心类,不能被同名业务类干扰

预加载

《类加载器》中说过,程序启动后,并不会加载所有类,在运行中实现到时,才会去加载。这儿就有性能损耗。

按类加载规则,一个类只加载一次

可以测试一下,加载需要的损耗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 类加载时间性能测试
*
* 看一下类加载需要消耗的时间
* Created by Jack on 2018/10/8.
*/
public class ClassLoaderTest1 {
public static void main(String[] args) throws SQLException {
long s = System.nanoTime();

LoaderClass loaderClass = new LoaderClass();
long e = System.nanoTime();
//第一次时间
System.out.println(e - s);
e = System.nanoTime();
//第二次实例,但已经加载过,不再需要加载
LoaderClass loaderClass1 = new LoaderClass();
long e1 = System.nanoTime();
//第二次时间
System.out.println(e1 - e);
}
}
//输出
2409737
396

可以从输出看到性能损耗是不小的,这部分损耗可以通过预加载来消除

随着程序运行时间越久,被触发的业务越多,那加载到的业务类越多。

预加载类的逻辑

ClassWarmUp

  • 1.在classloader中loadClass时,把className加入到LinkedBlockingDeque中
  • 2.为了性能,异步把deque中的class写入到文件中,需要起一个后台线程
    • 2.1 后台线程,从deque中取出class,写入到文件中
  • 3.下次从文件中预先加载class

打包

对于/servicesdir 与 /thirddir 都好处理,但对于platformdir是怎么打包的呢?毕竟在开发时,只是引入一个平台基础jar就行

使用

有了自定义类加载器,在应用主函数中,就不能直接new了,不然就会使用AppClassLoader

所以需要使用反射机制

1
2
3
Class<?> loadClass = platformClassLoader.loadClass("com.jack.Start");
Method startMethod = loadClass.getMethod("startUp");
startMethod.invoke(loadClass);

这样,通过Start加载的类也会通过platformClassLoader去加载

创建springcontext也一样,这儿还需使用到Thread.currentThread().getContextClassLoader()【下面有详解】

1
2
3
4
5
6
7
8
9
10
11
12
ClassLoader currentThreadLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(appClassLoader);

Class<?> contextClass = appClassLoader
.loadClass("org.springframework.context.support.FileSystemXmlApplicationContext");
Class<?>[] parameterTypes = new Class[] { String[].class };
Constructor<?> constructor = contextClass.getConstructor(parameterTypes);
return constructor.newInstance(new Object[] { xmlPaths.toArray(new String[0]) });


// switch back the thread context classloader
Thread.currentThread().setContextClassLoader(currentThreadLoader);

3、反常

“双亲委派”模型有优点,也有力不从心的地方

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。
而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

场景:

  1. 当高层提供了统一的接口让低层去实现,同时又要在高层加载(或者实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类
  2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管

解决方案:

从jdk1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader c1),分别用来获取和设置类加载器

一般使用模式:获取-使用-还原

1
2
3
4
5
6
7
8
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 
try{
Thread.currentThread().setContextClassLoader(targetTccl);
excute();
} finally {
Thread.currentThread().setContextClassLoader(classLoader);
}

jdbc

以jdbc看下场景1的情况

1
2
3
4
Class.forName("com.mysql.jdbc.Driver")
String url = "jdbc:mysql://localhost:3306/testdb";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
  • 1.Class.forName(“com.mysql.jdbc.Driver”); 在com.mysql.jdbc.Driver中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}

通过Class.forName(),主要就是执行初始化static代码块,也就是向DriverManager注册Driver

此时:应用类、Driver是由AppClassLoader加载,但由于双亲委派java.sql.DriverManager是由BootstrapClassLoader加载

  • 2.java.sql.DriverManager.getConnection 获取连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
private static Connection getConnection(
String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
java.util.Vector drivers = null;

synchronized(DriverManager.class) {
if(callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection(\"" + url + "\")");

if (!initialized) {
initialize();
}

synchronized (DriverManager.class){
drivers = readDrivers;
}

SQLException reason = null;
for (int i = 0; i < drivers.size(); i++) {
DriverInfo di = (DriverInfo)drivers.elementAt(i);

if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
println(" skipping: " + di);
continue;
}
try {
println(" trying " + di);
Connection result = di.driver.connect(url, info);
if (result != null) {
// Success!
println("getConnection returning " + di);
return (result);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}

if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}


private static Class getCallerClass(ClassLoader callerClassLoader,
String driverClassName) {
Class callerC = null;

try {
callerC = Class.forName(driverClassName, true, callerClassLoader);
}
catch (Exception ex) {
callerC = null; // being very careful
}

return callerC;
}

这其中有两行代码:

1
2
3
4
callerCL = Thread.currentThread().getContextClassLoader();

callerC = Class.forName(driverClassName, true, callerClassLoader);

这儿是取线程上下文中的classloader,也就是AppClassLoader;如果不取此classloader,那么Class.forName(driverClassName)就是使用DriverManager的BootstrapClassLoader加载,那必然是加载不到,这也就是父层类加载器加载不了低层类。

还有个问题,为什么在应用程序中已经加载过Driver,到了getConnection()又要再加载,还得通过Thread.currentThread().getContextClassLoader()?

其实在getConnection()中,只是对比class是否是同一个,像tomcat那样,各个应用都有自己的mysql-driver的jar包,就只能通过classloader来区分,因为class是不是相同需要classname+classloader组合鉴别

spring

对于场景2的问题

如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用线程上下文加载器来加载类,而线程上下文加载器默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean

org.springframework.web.context.ContextLoader类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}

Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();

try {
// Determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);

// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
this.context = createWebApplicationContext(servletContext, parent);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}

if (logger.isDebugEnabled()) {
logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
}

return this.context;
}
catch (RuntimeException ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
catch (Error err) {
logger.error("Context initialization failed", err);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
throw err;
}
}

关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取线程上下文类加载器,默认为WebAppClassLoader
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
// 如果spring的jar包放在每个webapp自己的目录中
// 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
// 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来
// 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出
currentContextPerThread.put(ccl, this.context);
}

这样做的目的在于当通过ConetxtLoader的静态方法获取context的时候,能保证获取的是当前web application的context.实际上就是对于tomcat下面的任何一个线程,我们都能很方便的找出这个线程对应的webapplicationContext.于是在一些不能方便获取servletContext的场合,我们可以通过当前线程获取webapplicationContext.

1
2
3
4
5
6
7
8
9
10
public static WebApplicationContext getCurrentWebApplicationContext() {
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl != null) {
WebApplicationContext ccpt = currentContextPerThread.get(ccl);
if (ccpt != null) {
return ccpt;
}
}
return currentContext;
}

总结

简而言之就是ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作

4、参考资料

以jdbc为例搞清contextClassLoader

java执行过程

先回顾一下要执行java程序,需要经过哪些步骤

执行java程序

  1. 编写java代码
  2. 通过javac把源代码编译成class
  3. 把class载入JVM

1、2两步是需要开发人员参与的,而第3步是JVM的行为,对开发人员透明

JVM类加载

详细看下第三点,class载入JVM过程

从内存空间视角,会分配到各个空间:

内存结构

每个内存空间详情可参考:《GC及JVM参数》

从类生命周期角度,分阶段:

类生命周期

其中类加载的过程包括了加载验证准备解析初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

1.加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口

在加载阶段,虚拟机需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载

加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,
而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据

加载时机

  • 当应用程序启动的时候,所有的类不会被一次性加载,因为如果一次性加载,内存资源有限,可能会影响应用程序的正常运行。例如,A a=new A(),
    一个类真正被加载的时机是在创建对象的时候,才会去执行以上过程,加载类。当我们测试的时候,最先加载拥有main方法的主线程所在类

  • Java虚拟机有预加载功能。类加载器并不需要等到某个类被”首次主动使用”时再加载它,JVM规范规定JVM可以预测加载某一个类,如果这个类出错,但是应用程序没有调用这个类, JVM也不会报错;如果调用这个类的话,JVM才会报错,(LinkAgeError错误)

加载方式

隐式加载
  1. 创建类对象
  2. 使用类的静态域
  3. 创建子类对象
  4. 使用子类的静态域
  5. 在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class
  6. 在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
  7. 在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数所在的类的class文件
显式加载
  1. ClassLoader.loadClass(className),不会进行初始化
  2. Class.forName(String name, boolean initialize,ClassLoader loader); 借助当前调用者的class的ClassLoader完成class的加载,加载class的同时根据initialize是否初始化

2.连接

2.1.验证:确保被加载的类的正确性

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用**-Xverify:none**参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

2.2. 准备:为类的静态变量分配内存,并将其初始化为默认值

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  • 2.2.1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

  • 2.2.2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
    假设一个类变量的定义为:public static int value = 3;
    那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行

  • 2.2.3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

    假设上面的类变量value被定义为: public static final int value = 3;

    编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3

2.3. 解析:虚拟机将常量池中的符号引用替换为直接引用(内存地址)的过程

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

常量池
  1. 字面量:比较接近Java语言层面,如String字符串,声明final的常量等
  2. 符号引用:属于编译原理方面的概念:1、包括类和接口的全限定名 2、字段的名称和描述符3.方法的名称和描述符
常量项结构

常量项结构

这些内容,需要再去分析class文件详细结构,后续再学习了

3.初始化,为类的静态变量赋予正确的初始值

类加载的最后一个阶段,除了加载阶段我们可以通过自定义类加载器参与之外,其余完全又JVM主导。到了初始化阶段,才真正开始执行程序,也就是由java转换成的class

JVM负责对类进行初始化,主要对类变量进行初始化。

在Java中对类变量进行初始值设定有两种方式:

  1. 声明类变量是指定初始值

  2. 使用静态代码块为类变量指定初始值

JVM初始化规则

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化

Java程序对类的使用方式可以分为两种:

  1. 主动使用:会执行加载、连接、初始化静态域
  2. 被动使用:只执行加载、连接,不执行类的初始化静态域
类的主动使用包括以下六种:
  • 创建类的实例,如(1)new (2)反射newInstance (3)序列化生成obj;遇到new、getstatic、putstatic、invokestatic这四条字节码指令
  • 访问某个类或接口的静态变量,或者对该静态变量赋值 (注意static 与static final的区别)
  • 调用类的静态方法
  • 反射(如Class.forName(“Test”))
  • 初始化某个类的子类,则其父类也会被初始化;接口初始化不会导致父接口的初始化(这其实也是static final的原因);对于静态字段,
    • 只有直接定义这个字段的类才会被初始化,因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
  • Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
被动使用,不在主动使用的六种以内都是被动的
  • 1.如通过子类引用父类的静态字段,为子类的被动使用,不会导致子类初始化
  • 2.通过数组定义类引用类,为类的被动使用,不会触发此类的初始化
    • 2.1 原因:其实数组已经不是E类型了,E的数组jvm在运行期,会动态生成一个新的类型,新类型为:
      如果是一维数组,则为:[L+元素的类全名;二维数组,则为[[L+元素的类全名
      如果是基础类型(int/float等),则为[I(int类型)、[F(float类型)等
  • 3.常量在编译阶段会存入调用方法所在的类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 主动 被动使用问题测试
* Created by Jack on 2018/9/28.
*/
public class ClassInitTest3 {
public static void main(String[] args) {
String x = F.s;
}
}

class F {
//因为UUID.randomUUID().toString()这个方法,是运行期确认的,所以,这不是被动使用
static final String s = UUID.randomUUID().toString();

static {
//这儿会被输出
System.out.println("Initialize class F");
}
}

clinit 与 init

在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init

clinit:

clinit指的是类构造器,这个构造器是jvm自动合并生成的,在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行

它合并static变量的赋值操作

  1. 注意是赋值操作,**(仅声明,或者final static)**不会触发,毕竟前面准备阶段已经默认赋过值为0了
  2. static{}语句块生成,且虚拟机保证执行前,父类的已经执行完毕,所以说父类如果定义static块的话,一定比子类先执行
  3. 如果一个类或接口中没有static变量的赋值操作和static{}语句块,那么不会被JVM生成
  4. static变量的赋值操作和static{}语句块合并的顺序是由语句在源文件中出现的顺序所决定的。
init:

在实例创建出来的时候调用,也就是构造函数,包括:

  1. new操作符
  2. 普通代码块
  3. 调用Class或java.lang.reflect.Constructor对象的newInstance()方法;
  4. 调用任何现有对象的clone()方法;
  5. 通过java.io.ObjectInputStream类的getObject()方法反序列化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* <clinit> 与 <init> 区别
*/
public class ClassInitTest2 {
static {
System.out.println("cinit");

i = 3;//可以赋值
//System.out.println(i);//但不能使用,语法错误
}

private static int i = 1;

{
System.out.println("init");//实例化构造器,
}

public static void main(String [] args) {
new ClassInitTest2();
new ClassInitTest2();
String str = "str";
System.out.println(str);
}
}

// 输出
cinit
init
init
str

static 与 static final 对初始化的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* static 与 static final 对初始化的区别
*/
public class ClassInitFinalTest {
public static int age = 20;

static {
//如果age定义为static final,这儿就不会执行
System.out.println("静态初始化!");
}

public static void main(String args[]){
System.out.println(ClassInitFinalTest.age);
}
}

不会执行类初始化的几种情况

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化
  • 类A引用类B的static final常量不会导致类B初始化 (看上面的ClassInitFinalTest)
  • 通过类名获取Class对象,不会触发类的初始化。如
  • System.out.println(Person.class);
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作

结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

类加载测试

看到一段代码,很有意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 测试类加载及初始化顺序问题
* Created by jack01.zhu on 2018/9/28.
*/
public class ClassInit {
private static ClassInit singleton = new ClassInit();
public static int counter1;
public static int counter2 = 0;
private ClassInit() {
counter1++;
counter2++;
}
public static ClassInit getSingleton() {
return singleton;
}
}

/**
* 通过输出结果,推测类加载过程
* Created by jack01.zhu on 2018/9/28.
*/
public class ClassInitTestMain {

public static void main(String []args) {
ClassInit classInitTest = ClassInit.getSingleton();
System.out.println("counter1="+classInitTest.counter1);
System.out.println("counter2="+classInitTest.counter2);
}
}

这段代码输出的结果是什么?

1
2
counter1=1
counter2=0
  1. 入口肯定是ClassInitTestMain.main(),从这儿开始加载,初始化
  2. ClassInit.getSingleton(),首次使用化,所以从加载部分开始执行,执行到准备阶段所有static变量都被设置为初始值。此时
1
2
3
public static int counter1 = 0;
public static int counter2 = 0;
private static ClassInit singleton = null;
  1. ClassInit执行到初始化阶段,生成类构造器,类构造器会合并 static变量的赋值操作和 static语句块。合并后执行
1
2
3
4
5
6
public static int counter1 ; // 由于 counter1没被赋值,所以不会被合并进去

public void clinit() {// 伪代码:<clinit>方法体内容
ClassInit singleton = new ClassInit();//(1)
int counter2 = 0;// (2)
}
  1. 初始化阶段 执行clinit内代码,执行到(1)处,此时counter1和counter2都变为1。
  2. 初始化阶段 执行clinit内代码,执行到(2)处,counter2又被设置为0。
  3. 初始化结束 ,回到Main方法的ClassInit.getSingleton();继续执行main方法,最后输出结束。

以上,就是一个类的生命周期,这篇重点就是加载部分,如上面所说,加载阶段相对别的阶段,对开发人员而言有更强的可控性;下面学习一下类加载器相关知识

类加载器

类加载器

  1. BootstrapClassLoader:加载路径: System.getProperty(“java.class.path”) 或直接通过 -Xbootclasspath 指定

特性: 用C语言写的

手动获取加载路径: sun.misc.Launcher.getBootstrapClassPath().getURLs()

  1. ExtClassLoader:加载路径: System.getProperty(“java.ext.dirs”) 或直接通过 -Djava.ext.dirs 指定

特性: 继承 URLClassLoader

手动获取加载路径:((URLClassLoader)App.class.getClassLoader().getParent()).getURLs()

  1. AppClassLoader:加载路径: System.getProperty(“sun.boot.class.path”) 或直接通过 -cp, -classpath 指定

特性: 继承 URLClassLoader

手动获取加载路径: ((URLClassLoader)App.class.getClassLoader()).getURLs()
通过 ClassLoader.getSystemClassLoader() 就可以获取 AppClassLoader, 自己写的程序中写的 ClassLoader(继承 URLClassLoader), 若不指定 parent, 默认的parent就是 AppClassLoader

同一个class

在JVM中,如何确定一个类型实例:

同一个Class = 相同的 ClassName + PackageName + ClassLoader

在JVM中,类型被定义在一个叫SystemDictionary 的数据结构中,该数据结构接受类加载器和全类名作为参数,返回类型实例。

SystemDictionary 如图所示:

SystemDictionary

加载机制

  1. 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  2. 双亲委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  3. 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

双亲委托模型

双亲委托的工作过程:如果一个类加载器收到了一个类加载请求,它首先不会自己去加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它管理的范围之中没有这个类)时,子加载器才会尝试着自己去加载

  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

  2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

  4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

1
javac –verbose查看运行类是加载了jar文件 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{

// 首先检查,jvm中是否已经加载了对应名称的类,findLoadedClass(String )方法实际上是findLoadedClass0方法的wrapped方法,做了检查类名的工
//作,而findLoadedClass0则是一个native方法,通过底层来查看jvm中的对象。
Class c = findLoadedClass(name);
if (c == null) {//类还未加载
try {
if (parent != null) {
//在类还未加载的情况下,我们首先应该将加载工作交由父classloader来处理。
c = parent.loadClass(name, false);
} else {
//返回一个由bootstrap class loader加载的类,如果不存在就返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found

// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);//这里是我们的入手点,也就是指定我们自己的类加载实现
}
}
if (resolve) {
resolveClass(c);//用来做类链接操作
}
return c;
}

从上面的方法也看出我们在实现自己的加载器的时候,不要覆盖locaClass方法,而是重写findClass(),这样能保证双亲委派模型,同时也实现了自己的方法

为什么要使用双亲委托这种模型呢?

  1. 节约系统资源: 因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次
  2. 保证Java核心库的类型安全: 我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

自定义加载器

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader

定义自已的类加载器分为两步:

  1. 继承java.lang.ClassLoader
  2. 重写父类的findClass方法

示例

很简单的两个类,方法中打印出各自的类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LoaderClass {

public void loader(){
System.out.println("LoaderClass:"+this.getClass().getClassLoader());
LoaderClass1 class1 = new LoaderClass1();
class1.loader();
}
}

public class LoaderClass1 {

public void loader() {
System.out.println(this.getClass().getName() + " loader:"+this.getClass().getClassLoader());

}
}

自定义加载器

  1. 重写findClass方法,从class文件加载
  2. 通过defineClass从bytes构建class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyClassLoader extends ClassLoader {

protected Class<?> findClass(String name) throws ClassNotFoundException {

String root = "d:/";

byte[] bytes = null;
try {
//路径改到根目录下
String file = root + name.substring(name.lastIndexOf(".")+1) + ".class";
InputStream ins = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
bytes = baos.toByteArray();

ins.close();
} catch (Exception e) {
e.printStackTrace();
}
return defineClass(name, bytes, 0, bytes.length);
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ClassLoaderTest {

public static void main(String[]args) throws Exception {
ClassLoaderTest test = new ClassLoaderTest();

System.out.println(test.getClass().getClassLoader());//输出sun.misc.Launcher$AppClassLoader

System.out.println(test.getClass().getClassLoader().getParent());//输出sun.misc.Launcher$ExtClassLoader

System.out.println(test.getClass().getClassLoader().getParent().getParent());//输出null

//=====测试重复加载,类路径中LoaderClass.class存在=================
//======虽然指定了classloader,但依然输出的是LoaderClass:sun.misc.Launcher$AppClassLoader
//==删除类路径下的LoaderClass.class,才会输出LoaderClass:com.jack.classloader.MyClassLoader
//并且loaderclass中创建的对象类加载器也是MyClassLoader
MyClassLoader classLoader = new MyClassLoader();
Class<?> loadClass = Class.forName("com.jack.classloader.LoaderClass", true, classLoader);
Method startMethod = loadClass.getMethod("loader");
startMethod.invoke(loadClass.newInstance());

//===当类加载器不一样时,两个class不相等
MyClassLoader classLoader1 = new MyClassLoader();
Class<?> loadClass1 = Class.forName("com.jack.classloader.LoaderClass", true, classLoader1);
System.out.println(loadClass.equals(loadClass1));//输出false
}
}

参考资料

class加载时机及两种显示加载的区别

JVM类加载机制—类加载的过程

<init>和<clinit>

类加载原理分析&动态加载Jar/Dex

java类的主动使用/被动使用

前言

之前《TreadLocal解析》说过Threadlocal的结构:

ThreadLocal结构

但netty却重新搞了一个fastthreadlocal,从各方面对比一下两者的区别。也不得不说一下netty真不愧是款优秀框架,里面中有很多优秀类和方法值得细品

VS ThreadLocal

1、性能

第一点,从性能开始,为什么要重造轮子,可能就是之前的轮子达不到性能要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class FastThreadLocalTest {


public static void main(String[] args) {
testFast(100);
testSlow(100);
}

private static void testFast(int threadLocalCount) {
final FastThreadLocal<String>[] caches = new FastThreadLocal[threadLocalCount];
final Thread mainThread = Thread.currentThread();
for (int i = 0; i < threadLocalCount; i++) {
caches[i] = new FastThreadLocal();
}
Thread t = new FastThreadLocalThread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < threadLocalCount; i++) {
caches[i].set("float.lu");
}
long start = System.nanoTime();
for (int i = 0; i < threadLocalCount; i++) {
for (int j = 0; j < 1000000; j++) {
caches[i].get();
}
}
long end = System.nanoTime();
System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) +
"]ms");
LockSupport.unpark(mainThread);
}

});
t.start();
LockSupport.park(mainThread);
}

private static void testSlow(int threadLocalCount) {
final ThreadLocal<String>[] caches = new ThreadLocal[threadLocalCount];
final Thread mainThread = Thread.currentThread();
for (int i=0;i<threadLocalCount;i++) {
caches[i] = new ThreadLocal();
}
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<threadLocalCount;i++) {
caches[i].set("float.lu");
}
long start = System.nanoTime();
for (int i=0;i<threadLocalCount;i++) {
for (int j=0;j<1000000;j++) {
caches[i].get();
}
}
long end = System.nanoTime();
System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) +
"]ms");
LockSupport.unpark(mainThread);
}

});
t.start();
LockSupport.park(mainThread);
}
}

//输出
fast[15]ms
slow[302]ms

从输出可见性能提升很大

2、数据结构

两者的数据结构大体相似,都是thread带上map属性,threadlocal实例为key;但在细节算法处理时,不一样

get()

整体思路:通过thread取到map,再从map中取value

ThreadLocal.get()

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

从map中取值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

如果key值相等,直接返回value

如果key不相等,使用循环线性探测,一直找到最后一个元素

FastThreadLocal.get()

1
2
3
4
5
6
7
8
9
10
11
12
13
public final V get(InternalThreadLocalMap threadLocalMap) {
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}

return initialize(threadLocalMap);
}

public Object indexedVariable(int index) {
Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
}

这个明显就快些,有index,直接数组拿值,不需要再去处理循环

set()

主要在于向map中放值

ThreadLocal.set()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
  1. 通过取模,得到index
  2. key相等,直接赋值value
  3. key不相等,那就线性探测存放

FastThreadLocal.set()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
if (setKnownNotUnset(threadLocalMap, value)) {
registerCleaner(threadLocalMap);
}
} else {
remove();
}
}

public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
expandIndexedVariableTableAndSet(index, value);
return true;
}
}

这类似就是放入到数组中

总结

到此可以看出二者的区别

区别 ThreadLocal FastThreadLocal
map ThreadLocalMap InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap
线程 Thread FastThreadLocalThread extends Thread

主要还是在内部map的处理逻辑上,两者都没有使用hashmap,但是自定义了map结构与行为,在《hashmap源码解析》中指出map结构的两种处理方式:拉链法线性探测法;在hasmap中使用的是拉链法,而threadlocal中使用的是线性探测法

线性探查(Linear Probing)方式虽然简单,但是有一些问题,它会导致同类哈希的聚集。在存入的时候存在冲突,在查找的时候冲突依然存在

冲突也就造成了性能损耗,而FastTreadLocal就更简单,直接使用数组

1
2
3
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}

UnpaddedInternalThreadLocalMap

1
2
3
4
5
6
7
8
9
10
11
Object[] indexedVariables;


public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}

整个map就是一个数组结构,在每个thread中,每一个FastThreadLocal在创建时就指定了index,value就是数组元素

RPC

原理

image

什么是Stub?

Stub是一段代码,用来转换RPC过程中传递的参数。处理内容包括不同OS之间的大小端问题。另外,Client端一般叫Stub,Server端一般叫Skeleton。

生产方式:

  1. 手动生成,比较麻烦;
  2. 自动生成,使用IDL(InterfaceDescriptionLanguate),定义C/S的接口

RPC的套路:

自古深情留不住 唯有套路留人心

RPC最本质的就是通过socket把方法信息传输到远程服务器并执行相应method

在java界的rpc框架的实现手法:

  • 服务端:socket + 反射
  • 客户端:动态代理 + socket

之前也解析过motain框架,《motain客服端分析》、《motain服务端分析》

thrift

由于我司框架是通过thrift改造,发现这个框架没有按java套路出牌,可能这是跨语言类RPC的套路,有必要了解一下

thrift最初由facebook开发用做系统内各语言之间的RPC通信 。2007年由facebook贡献到apache基金 ,08年5月进入apache孵化器,支持多种语言之间的RPC方式的通信:php语言client可以构造一个对象,调用相应的服务方法来调用java语言的服务 ,跨越语言的C/S RPC调用   

thrift

示例

IDL文件

1
2
3
4
5
//HelloService.thrfit
namespace java com.jack.thrift
service HelloService{
string helloString(1:string what)
}

生成代码

1
2
运行  thrift -gen HelloService.thrfit

会生成一个HelloService类

实现服务端与客服端

让服务端打印出客户端传入的参数

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ThriftServer {

/**
* 启动thrift服务器
* @param args
*/
public static void main(String[] args) throws Exception {
try {
System.out.println("服务端开启....");
TProcessor tprocessor = new HelloService.Processor<HelloService.Iface>(new HelloServiceImpl());
// 简单的单线程服务模型
TServerSocket serverTransport = new TServerSocket(9898);
TServer.Args tArgs = new TServer.Args(serverTransport);
tArgs.processor(tprocessor);
tArgs.protocolFactory(new TBinaryProtocol.Factory());
TServer server = new TSimpleServer(tArgs);
server.serve();
}catch (Exception e) {
e.printStackTrace();
}
}

}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThriftClient {

public static void main(String[] args) {
System.out.println("客户端启动....");
TTransport transport = null;
try {
transport = new TSocket("localhost", 9898, 30000);
// 协议要和服务端一致
TProtocol protocol = new TBinaryProtocol(transport);
HelloService.Client client = new HelloService.Client(protocol);
transport.open();
String result = client.helloString("哈哈");
System.out.println(result);
} catch (TTransportException e) {
e.printStackTrace();
} catch (TException e) {
e.printStackTrace();
} finally {
if (null != transport) {
transport.close();
}
}
}
}

解析

可以看出server,client代码相对很简单,主要看看生成的HelloService类,这个类就是stub代码

来看一下,这个类是如何封装,把method和args传输到远程的

client

1
2
HelloService.Client client = new HelloService.Client(protocol);
String result = client.helloString("哈哈");

关键点在HelloService.Client.helloString()方法

1
2
3
4
5
public String helloString(String what) throws org.apache.thrift.TException
{
send_helloString(what);
return recv_helloString();
}

发送消息

1
2
3
4
5
6
public void send_helloString(String what) throws org.apache.thrift.TException
{
helloString_args args = new helloString_args();
args.setWhat(what);
sendBase("helloString", args);
}
  1. 把args抽象成了一个类
  2. 属性赋值
  3. 发送

主要看下sendBase()方法

1
2
3
4
5
6
private void sendBase(String methodName, TBase<?,?> args, byte type) throws TException {
oprot_.writeMessageBegin(new TMessage(methodName, type, ++seqid_));
args.write(oprot_);
oprot_.writeMessageEnd();
oprot_.getTransport().flush();
}
  • 1.oprot_.writeMessageBegin 根据Protocol写数据,比如这儿使用的TBinaryProtocol,以二进制写数据
1
2
3
4
5
6
7
8
9
10
11
12
public void writeMessageBegin(TMessage message) throws TException {
if (strictWrite_) {
int version = VERSION_1 | message.type;
writeI32(version);
writeString(message.name);
writeI32(message.seqid);
} else {
writeString(message.name);
writeByte(message.type);
writeI32(message.seqid);
}
}

再深入看看怎么写二进制数据的

int类型

1
2
3
4
5
6
7
public void writeI32(int i32) throws TException {
inoutTemp[0] = (byte)(0xff & (i32 >> 24));
inoutTemp[1] = (byte)(0xff & (i32 >> 16));
inoutTemp[2] = (byte)(0xff & (i32 >> 8));
inoutTemp[3] = (byte)(0xff & (i32));
trans_.write(inoutTemp, 0, 4);
}

string类型,先写长度,再写bytes

1
2
3
4
5
6
7
8
9
public void writeString(String str) throws TException {
try {
byte[] dat = str.getBytes("UTF-8");
writeI32(dat.length);
trans_.write(dat, 0, dat.length);
} catch (UnsupportedEncodingException uex) {
throw new TException("JVM DOES NOT SUPPORT UTF-8");
}
}

这儿写最终还是使用Transport.write,比如这儿使用的TSocket

1
2
3
4
5
6
7
8
9
10
public void write(byte[] buf, int off, int len) throws TTransportException {
if (outputStream_ == null) {
throw new TTransportException(TTransportException.NOT_OPEN, "Cannot write to null outputStream");
}
try {
outputStream_.write(buf, off, len);
} catch (IOException iox) {
throw new TTransportException(TTransportException.UNKNOWN, iox);
}
}

就是写到

1
outputStream_ = new BufferedOutputStream(socket_.getOutputStream(), 1024);
  • 2.args.write(oprot_);
1
2
3
4
5
6
7
8
9
10
11
12
public void write(org.apache.thrift.protocol.TProtocol oprot, helloString_args struct) throws org.apache.thrift.TException {
struct.validate();

oprot.writeStructBegin(STRUCT_DESC);
if (struct.what != null) {
oprot.writeFieldBegin(WHAT_FIELD_DESC);
oprot.writeString(struct.what);
oprot.writeFieldEnd();
}
oprot.writeFieldStop();
oprot.writeStructEnd();
}

这就是写field,也就是向输出流里写参数内容

  • 3.oprot_.writeMessageEnd();
    这表示消息写完成了,各个协议处理不同,比如二进制就是空实现,但如json就需要写个”}”,以完成json格式

  • 4.oprot_.getTransport().flush(); 直接flush

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Flushes the underlying output stream if not null.
*/
public void flush() throws TTransportException {
if (outputStream_ == null) {
throw new TTransportException(TTransportException.NOT_OPEN, "Cannot flush null outputStream");
}
try {
outputStream_.flush();
} catch (IOException iox) {
throw new TTransportException(TTransportException.UNKNOWN, iox);
}
}

client总结

整个发送消息就结束了,虽然没有按套路使用动态代理,而是通过生成的stub代码,把methodName,args给封装好了

server

服务端也没有通过反射的方式

主要逻辑在生成的HelloService$Processor类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static class Processor<I extends Iface> extends org.apache.thrift.TBaseProcessor<I> implements org.apache.thrift.TProcessor {
private static final org.slf4j.Logger _LOGGER = org.slf4j.LoggerFactory.getLogger(Processor.class.getName());
public Processor(I iface) {
super(iface, getProcessMap(new java.util.HashMap<String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>>()));
}

protected Processor(I iface, java.util.Map<String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> processMap) {
super(iface, getProcessMap(processMap));
}

private static <I extends Iface> java.util.Map<String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> getProcessMap(java.util.Map<String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> processMap) {
processMap.put("helloString", new helloString());
return processMap;
}

public static class helloString<I extends Iface> extends org.apache.thrift.ProcessFunction<I, helloString_args> {
public helloString() {
super("helloString");
}

public helloString_args getEmptyArgsInstance() {
return new helloString_args();
}

protected boolean isOneway() {
return false;
}

@Override
protected boolean handleRuntimeExceptions() {
return false;
}

public helloString_result getResult(I iface, helloString_args args) throws org.apache.thrift.TException {
helloString_result result = new helloString_result();
result.success = iface.helloString(args.what);
return result;
}
}

}
  • 1.先看构造函数
1
2
3
4
5
6
7
8
protected Processor(I iface, java.util.Map<String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> processMap) {
super(iface, getProcessMap(processMap));
}

private static <I extends Iface> java.util.Map<String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> getProcessMap(java.util.Map<String, org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> processMap) {
processMap.put("helloString", new helloString());
return processMap;
}

这段把methodName与对应的处理类映射,那后面的事就简单了,当接受到消息,取得methodName,通过map获取对就的处理类回调就可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static class helloString<I extends Iface> extends org.apache.thrift.ProcessFunction<I, helloString_args> {
public helloString() {
super("helloString");
}

public helloString_args getEmptyArgsInstance() {
return new helloString_args();
}

protected boolean isOneway() {
return false;
}

@Override
protected boolean handleRuntimeExceptions() {
return false;
}

public helloString_result getResult(I iface, helloString_args args) throws org.apache.thrift.TException {
helloString_result result = new helloString_result();
result.success = iface.helloString(args.what);
return result;
}
}

处理类,继承ProcessFunction类,实现getResult(),这个方法就是调用了对应service.helloString()

可以再深入看一下,在socket监听消息时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
client = serverTransport_.accept();
if (client != null) {
processor = processorFactory_.getProcessor(client);
inputTransport = inputTransportFactory_.getTransport(client);
outputTransport = outputTransportFactory_.getTransport(client);
inputProtocol = inputProtocolFactory_.getProtocol(inputTransport);
outputProtocol = outputProtocolFactory_.getProtocol(outputTransport);
if (eventHandler_ != null) {
connectionContext = eventHandler_.createContext(inputProtocol, outputProtocol);
}
while (true) {
if (eventHandler_ != null) {
eventHandler_.processContext(connectionContext, inputTransport, outputTransport);
}
if(!processor.process(inputProtocol, outputProtocol)) {
break;
}
}

关键行:processor.process(inputProtocol, outputProtocol)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean process(TProtocol in, TProtocol out) throws TException {
TMessage msg = in.readMessageBegin();
ProcessFunction fn = processMap.get(msg.name);
if (fn == null) {
TProtocolUtil.skip(in, TType.STRUCT);
in.readMessageEnd();
TApplicationException x = new TApplicationException(TApplicationException.UNKNOWN_METHOD, "Invalid method name: '"+msg.name+"'");
out.writeMessageBegin(new TMessage(msg.name, TMessageType.EXCEPTION, msg.seqid));
x.write(out);
out.writeMessageEnd();
out.getTransport().flush();
return true;
}
fn.process(msg.seqid, in, out, iface);
return true;
}

这就很明显了,通过methodName从map中取得ProccessFunction,再执行process方法,调用相应service的方法

总结

虽然thrift没有按以往java套路出牌,但最根本的把method发送到远程执行是一致的。可能对于多语言来讲,便于所以语言一致性,的确需要通过生成的stub代码手法来实现RPC

当然thrift并不简单,还有很多的内容需要深挖学习,但至少这个简单示例可以了解跨语言型的RPC,相关IDL,Stub的知识,有清晰认知,而不局限于概念

软技能-代码之外的生存技能

《软技能-代码之外的生存技能》这本书可以算是《原则》的实践指南,作者对每一个建议都是事无巨细地指导方案,虽然此书对任何职业都有指导意义,但由于作者程序员的身份,让此书对程序员的实际指导更具体明确

之前《软技能笔记之职业与学习》写了职业与学习,这一篇写营销与生产力;作者的做法有些真是自己实践的,莫名喜感

营销

酒香也怕巷子深,尤其追求实干的程序员是得掌握一些自我营销知识

营销

营销就是一场争夺人们注意力的竞赛

凡人听到营销都会皱眉头,名声实在是不怎么样。
但实事上营销追求的是“实现价值在先,要求回报在后”

价值在先

自我营销的正确方式就是为他人提供价值,学习如何控制好自己要传达的信息,塑造好自己的形象,扩展信息送达的人群

营销并不能确保你一定成功,但是它却是你可控的重要元素

基本机制:要想让人们追随你、倾听你、你就要带给他们价值:为他们的问题提供答案,甚至是给他们带去欢乐

正视自我

我不是专家,没什么可营销的 ———— 其实很多人都喜欢向只比自己稍微优秀一点点的人学习,因为这些人才是可望而又可及的

一直谈论自己并试图证明自己价值连城。然而,你会发现,能解决他人的问题,真正能够帮到他人,你更容易获得成功。

营销手段

  1. 创建品牌:品牌即承诺,承诺按照你预期的方式交付你所预期的价值
  2. 打造博客:最大秘诀有且仅有一个————持之以恒,持之以恒地坚持写作,坚持不懈地产生高品质的内容。感觉没什么可写,提前头脑风暴出各种不同的想法,每当有新想法时,添加到主题列表中
  3. 演讲
  4. 著书立说

思维障碍

看起来像个傻瓜
这种想法,我也有,比如我不太敢主动分享自己的文章,感觉写得傻,蠢。好在我没有不行动,还在坚持写

在我的职业生涯中,我一共错失了9000多次投篮,输掉了近300场比赛。我本来有26次绝杀的机会却投球不进。我失败了一次又一次。 这就是我能够成功的原因 ————迈克尔 乔丹

生产力

这是一个快速变化的世界,时间飞逝,人难免焦虑,很多人会去看很多关于时间管理方面的书籍,但时间真的能管理吗?

如何让自己成为一台性能卓越、品质出众的超级高效机器

这一章节,有些地方给了新的认知,有些我自己也在践行,效果不错,值得一试

制定计划

这不用再重复,没有方向的油轮,是永远到达不了目的地的

可以从季度计划、月计划、周计划、日计划,让自己知道在前进,在向目标靠近

番茄工作法

我没有实践过这个方式,但对于程序员来讲,看一段源码,分析一个bug,几个小时的专注其实没什么问题,有时真是废寝忘食。

所以我对此方法也不屑,但作者的看法,带给我启发

番茄工作法只有被当作估算和评估工作的工具时,才能发挥它的真正威力

制订任务列表全凭主观臆断,每天能够专注完成的工作量才是最重要的

其实作者的意思就是要对自己的专注能力进行量化。量化是很重要的,大多数都是感觉,比如感觉自己很专注的工作,但真正专注了多久呢?上班8小时,专注了几个小时?一周专注工作几个小时?平均每个月能专注多久,这些是需要量化的。

无论是制订目标,还是改进目标,都需要量化,数据说话。就好比性能优化,不能凭感觉,而得给出具体数据。

我很认同作者的这个提法,所以现在也在尝试看自己一天能专注几个番茄时间

定额工作法

制订一些固定周期内的工作

比如:

  1. 每周跑3次
  2. 每周一篇博客
  3. 每天学习一个算法

实现定额制后,我发现自己的工作成果比以往多了很多。最大好处在于,长期坚持这么做,我就能随着时间的推移度量并标记自己的进度。可以确切知道自己在给定的一段时间内能够完成的工作量。

承诺是“定额工作法”的核心这其实很明显,太多人是只会制订计划,但从不执行,所以这是对自己的一份承诺

可以帮助克服意志力薄弱的问题,通过预先设定好的必须要遵循的过程,消除需要做出决策的部分

为什么这种工作法有效呢?

以缓慢但稳定的节奏工作,要优于快速但缺乏持久和坚持的工作方式

这个方法,其实我就在使用,比如我自己制定每月至少写一篇读书笔记、一篇技术学习

开始很难做到,但会时时提醒自己,要去读书,对学习的技术,要做整理。

这样坚持一段时间后,现在我算是超额完成,现在每月要写四篇文章,两篇关于读书的,两篇关于技术的

这样也正好呼应了营销部分的打造一个好博客,坚持写作,一个月四篇,一年就有48篇。其实看看一年量化,这数字也太少了,但要完成还真不容易

有了对自己的承诺,会强迫自己去兑现

保持激情

是人就会疲惫,新鲜感会消退,产生倦怠

想起李笑来的一句话,任何事,前期都不要用力过猛,越猛后期就越乏力

赛跑比的是谁耐力更长久,而不是看谁冲刺更有力

所以生产力的真正秘诀在于:长期坚持做一些小事


延伸阅读:

《软技能笔记之职业与学习》

《原则》读书笔记

之前看的《原则》,有没有感觉作者的原则都对,但实际操作又感觉假大空呢?

软技能-代码之外的生存技能

《软技能-代码之外的生存技能》这本书可以算是《原则》的实践指南,作者对每一个建议都是事无巨细地指导方案,虽然此书对任何职业都有指导意义,但由于作者程序员的身份,让此书对程序员的实际指导更具体明确

呐喊

这是此前github上很火的评论,我称之为程序员的呐喊

程序员呐喊

人人都推崇要活到老,学到老。但当真正身陷这个快速变更的行业里,有时难免出现焦躁,疲惫感

如何应对职业生涯,制定好职业规划,是此行业从业者们必须时时反省的话题

软技能

软技能

作者从七个方面阐述他的思想,这一篇记录下职业与学习两方面

职业

职业

作者阐述了在职业中可能遇到的各种问题,面面俱到

心态

不想当将军的士兵不是好士兵

把自己看成老板,不要安逸于一名职员

把自己当做一个软件企业,把雇主当做企业的一个客户,你应当能够提供某种产品或者服务

目标

《原则》中人生五步中的第一步就是确定目标,大多数人没有目标,但更大的问题是不知道如何确定目标

人际交往

talk is cheap ,show me the code

程序员特殊崇尚这句话,喜欢静静的写代码!但实事上每个职业工作都是与人打交道

code is cheap , show me the answer

面试

最快捷的方式让面试官不见其人先闻其声

让面试官对你怀有好感

学习

教育就是当一个人把在学校所学全部忘光之后剩下的东西 –爱因斯坦

教育的首要目标,并不在于“知”而在于“行” –赫伯特 斯宾塞

三要素

为了能够掌握一门技术,我需要了解以下三个要点

  1. 如何开始 ——要想开始使用自己所学的,我需要掌握哪些基本知识?
  2. 学科范围 ——我现在学的东西有多宏大?我应该怎么做?在开始阶段,我不需要了解每个细节,但是如果我能对该学
    科的轮廓有大致的了解,那么将来我就能发现更多细节。
  3. 基础知识 ——不止在开始阶段,要想使用一项特定的技术,我需要了解基本的用户案例和最常见的问题,也需要知道
    自己学的哪20%就能满足80%的日常应用

学习十步法

学习十步法

从第一步到第六步:这些步骤只用作一次!

第一步:了解全局

通常完成这一步我们可以使用网络搜索来完成大量的研究。在这一阶段我们只需要对要学习的东西有一个大概的了解即可。

第二步 :确定范围

现在我们在对我们要学习的东西有一定的了解的情况下,接下来就要集中精力去明确自己到底想要学习什么?在任何项目中,明确项目范围是至关重要的,唯有这样才能了解项目的全局,做好相应的准备工作。

而在这样的一个过程中,我们都很容易犯一个错误就是试图解决太大的问题而把自己搞得不堪重负,因此,我们要明确自己的学习范围。为此我们需要运用在第一步中所获得信息来让自己的关注点落脚在更小也可控制的范围内。

但在此过程中,我们可能会受到诱惑,为了学习该主题下不同的主题,我们可能会扩张自己的学习范围导致自己不够聚焦,所以请务必的抵制这个有诱惑,尽可能的保持专注,一次只能学习一样东西。我们可以稍后再回头学习别的领域的分支。

最后,请一定注意:明确学习范围的时候要考虑的时间因素,你的学习范围务必大小适当,既能够符合你的学习理由,又能符合你的时间限制。

第三步:定义目标

在我们全力以赴之前,明确“成功”的含义极为重要。如果不知道成功是什么样子,很难找准目标,也很难知道自己什么时候已经真正达到目标。所以在当你知道自己的目标是什么的时候,你就可以更轻松的使用倒推方式,明确实现目标所需要的步骤。

这一步的目标是形成一份简明清晰的陈述,勾勒出你勤奋学习后的成功图景。但是一定要确保其中包含的的具体成功标准,从而能让你用来充分评估自己是否已经达成学习目标。

好的目标应该是具体的,无二义性的,不要对自己想要完成的任务进行含糊不清的描述

你想从自己的学习经历中获取什么决定了你的成功标准是什么。请确保你能借此在学习结束后评估自己是否达成目标。好的成功标准也能让你向着既定目标不断前进。

第四步:寻找资源

要尝试收集到多种多样的资源来帮助你学习,而不是只读一本关于这一主题的书。资源是多种多样的,不局限于书籍。现在随着网络的广泛应用,你几乎可以针对自己感兴趣的人和主题找到大量的资源。

在这一不中,你要尽可能多的寻找自己所选择的相关资料,而且此时你无需考虑这些资源的质量。在你寻找过后,你要对你找到的这些资源进行过滤,去伪存真。

如果你不想因为单一来源的信息而产生偏见,那你就尽可能的去获取各种各样的信息吧。

第五步:创建学习计划

好的技术书都遵循着这样的规律:打好基础,做好铺垫,然后逐个展开每一章的论述。对于大多数学科而言,学习是一个自然的过程。从A开始,前进到B,然后到达Z。这个顺序对你掌握随机的碎片化知识价值不大。你需要找出在最短时间内从A到Z的正确路径,并且到达沿途的重要地标。

在这一步,你需要创建自己的学习路径。把它看作自己写作时候打大纲。

打造自己的学习计划,一个好方法就是借鉴吸取他人的方法,我们这时候可以翻看自己在第四步找来的资料,看看他们是如何学习这个主题的,如果很多不同的作者都把内容分解为相同的模块和顺序,你不妨可以去试一试,效仿他们去做一个自己的学习计划。

第六步:筛选资源

现在,我们知道自己要学习什么,按照什么样的方式去学习,那么是时候决定要使用哪些资源来完成自己的学习任务。现在时候对这些资源进行筛选,挑选最有价值记的几项来帮至自己实现目标。

在这一步中,把我们在第四步中收集的全部资料浏览一遍,找出哪些内容能够覆盖自己的学习计划。

一旦完成了这一步,我们就可以准备进到学习计划中的第一个模块了!

但在我们实现自己的目标之前,我们还需要为每个模块重复第7步到第10步。

第七步到第十步:循环往复(学习——实践——掌握——教授)

第七步: 开始学习,浅尝辄止

在这一步中,我们的目标是获得足够多的与所学主题相关的信息,从让能让我们开始学习,并在下一步中动手操作。

这一步的关键在于过犹不及。我们通常会很容易的就失去自控力,开始消化计划学习中列出的所有资源。但是你会发现,如果你能经受住这样的诱惑,你会取得更大的成就。你要专注于掌握自己所需的、能再下一步动手操作的的最小量的知识。

第八步:动手操作,边学边玩

这一步既有趣又可怕。说它有趣是因为你真的是在玩耍,说它可怕是因为这一步完全没有边际。在一部没有任何规则,你可以做任何你想做的事情,如何更好地实施这一步,完全由你来决定。

大多数人会尝试通过读书或者观看视频来掌握某个主题,他们会提前吸收很多信息,然后再付诸实践。这一方法的问题在于,在他们读书或者看视频的时候,他们并不知道哪些内容是重点。他们只是在因循他们设计好的学习路径。

现在,我们无需提前了解全部内容,你要做的首要的一件事情就是亲自操作和亲身体验。采用这种方法,你通过探索和时间学习。在操作的过程中,你的大脑自然地产生各种各样的问题:它是如何工作的?如果我这么做,它会发生什么?我该如何解决这个问题?这些问题引导着你走向真正重要的方向。当你回过头来寻找问题答案的时候,不只是这些问题迎刃而解,而且你记得的东西比你学习的东西要多得多,因为你所学到的都是对你很重要的东西。

在这一步中,不要担心结果,勇敢探索吧。

第九步:全面掌握,学以致用

好奇心是学习特别是自学的重要组成部分。这一步的目标就是让你找会好奇寻驱动的学习。在第八步中,你通过动手操作发现了一些尚未找到的答案的问题。现在时候来回答这些问题了,在这一步中,你要利用先前收集到的所有资料,进行深入学习。

为了有效利用自己选择的材料,为上一步产生的问题寻找答案,阅读文字、观看视频、与他们交流都是必要的手段。这能让你沉浸在学习材料中,尽可能地汲取知识。

不要害怕回头再去操作,付出更多,因为这不仅能让你找到问题的答案,也能让你学到新的东西。给自己足够多的时间去深入理解自己的主题,你可以阅读,可以实验,可以观察,也可以操作。

不过请记住,你依然没有必要把收集到的所有资料全部仔细看看一遍。你只需要阅读或者观看与当前所学有关的部分。

最后请不要忘了,你在第三步中定义的成功标准。试着把自己正在学习的内容与最终目标关联起来。你掌握的每个模块,都应该以某种方式推动你向着终极目标前行。

第十步:乐为人师,融会贯通

如果你真的想深入地掌握一门学问,想对这门学问做到融会贯通,那么你必须要做到”好为人师“。除此之外,别无他法。

在现实中,你只需要超前别人一步,就可以成为他们的老师。有时候,比学生超前太多的专家反而不能得心应手的教,因为他们无法与学生产生共鸣。他们忘记了初学者是什么样子,很容易专注于他们认为简单的细节。

在这一步中,我们要要求自己走出自己的舒适区,将自己学到的知识教给别人。要想确定你确实掌握了某些知识,这是唯一的办法;同时在我们将自己所学到的东西介绍给别人时,这也是查缺补漏的好办法。

你可以通过很多方式将自己所学交给他们。你可以写博客,也可以制作视频。你也可以跟自己的舍友,基友,爱人以及朋友探讨,将自己所学解释给他们。

重点在于,你要花时间将自己学到的东西从大脑提取出来,以别人能理解的方式组织起来。在经历这整个过程之后,你会发现,有很多你以为自己明白了的知识点,其实并没有摸透。于是你将会将那些以前自己没太明白的东西联系起来,并且简化自己大脑中已有的信息,将它们浓缩并经常复习。

后续

这篇只记录了职业与学习两部分,后续还有程序员更需要关注的营销部分

标题有些吸引眼球了,但并不浮夸,甚至还会远远超过百万,现在的平均响应时间在1ms内,0.08ms左右

如此高的QPS,如此低的AVG,为什么会有如此效果,关键点还是在多级缓存上

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流

概述

查询过程

上图基本上就是查询的通用方案,缓存中是否存在,存在就返回,不存在再查询Db,查询到的结果load进缓存

实践

缓存,逃不过三种操作,创建、查询、删除

此实践可能不保证全场景通用,但满足当前系统各项指标,当然没有完美的方案,只有适合的方案。

下面的时序图中,cache lv1是指本地缓存,cache lv2是cache cluster

查询

查询

查询过程:

从一级缓存开始查,如果没有,再向下一级查询,直到db

注意点:

  1. 一直查到db时,需要回源各级cache
  2. 防止击穿,需要在cache中填充value

创建

创建

创建过程:

  1. 创建cacheObject
  2. 放入Db(为了性能,以及db的降级,这儿可以引入异步开关)
  3. 放入cache lv2
  4. 放入cache lv1
  5. publish创建成功消息
  6. 消息监听服务会通知其它服务更新本地缓存

注意点:

  1. 到底是先放入Db,还是先放入cache
  2. db与cache的一致性保障

删除

删除

删除过程:

  1. 通过key查询cacheobject
  2. 清除db
  3. 清除各级cache
  4. publish消除成功消息
  5. 监听服务清除其它服务的本地缓存

注意点:

  1. 先清除db还是cache
  2. Db与cache的一致性保障

缓存操作模式

除了创建,查询,删除,还有更新操作;但我们业务场景没有。

对于我们的实践是不是放之四海而皆准,肯定是不行的。不以业务为基础的设计都是无根之木

先看下业界常见的操作缓存模式

更新缓存的的Design Pattern有四种:Cache aside, Read through, Write through, Write behind caching

Cache aside

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

查询

更新

这是标准的design pattern,包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。为什么不是写完数据库后更新缓存?你可以看一下Quora上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕两个并发的写操作导致脏数据。

那么,是不是Cache Aside这个就不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

但,这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必须在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

Cache Aside,我们的应用代码需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。所以,应用程序比较啰嗦。而Read/Write Through套路是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。

Read Through

Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。

这似乎很像guave的LoadCache

Write Through

Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)

Write Back

在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。

在wikipedia上有一张write back的流程图,基本逻辑如下:

write back

在游戏开发中基本上都是使用这种模式

但他也有缺点:

  1. 数据不是强一致性
  2. 数据可能会丢失
  3. 逻辑比较复杂

争论

  1. 一致性问题
    这儿的一致性是说强一致性,在分布式环境下,保证强一致性促使系统复杂性增加,或者性能有所下降。所以现在一般对非强制性业务场景都使用最终一致性解决。一致性的解读可以看看《zookeeper-paxos》,在我们实践时,在删除操作时,在清理失败时也通过补偿操作去尝试清除。
  2. 到底是update cache,还是delete cache
    其实任务技术手段都是看业务场景的,不能一概而论
    • update cache
      这个在并发写时,A1写db,B1写db,B2写cache,A2写cache;这时就出现db与Cache不一致的问题
      主动更新缓存,如果cacheobject复杂,需要Db与cache的多次交互,虽然减少了一次cache miss,但却增加了系统复杂度,得不偿失
    • delete cache
      这个不会有不一致问题了,但会造成cache miss,会不会造成热key穿透?
  3. 是先操作Db,还是cache
    假设先操作cache,再操作db;A B并发操作,A1 delete cache; B1 get cache –> miss –> select db –> load cache;A2 delete db;
    此种情况就出现此key一直有效状态,如果没有设置超时时间,那会长期在缓存中。这是不是得先操作db呢?
    一个操作先update db,再delte cache时失败了;那会数据库里是新数据,而缓存里是旧数据,业务无法接受。那是不是该先操作缓存呢?

是不是已经晕头了呢?

再有db主从架构中,主从不一致的情况,是不是没法玩了

所以还是开篇讲的没有放之四海而皆准的方案,只能寻找最适合的方案

在各种业务场景下,还是需要去寻找一些最佳实践,比如关注一下缓存过期策略、设置缓存过期时间

参考资料

缓存更新的套路

A beginner’s guide to Cache synchronization strategies

此篇算是对《voliatile,synchronized,cas》理论的一种实践

全局引用场景

单例模式

不用讲,这是首先想到的方式。

饿汉式 static final field

1
2
3
4
5
6
7
8
9
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();

private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}

这是最简单又安全的方式。但也有缺点:

  1. 它不是一种懒加载模式(lazy initialization)
  2. 一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

静态内部类

1
2
3
4
5
6
7
8
9
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本

双重检验锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}

}

这个写法得注意到volatile

主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
    但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

声明为volatile,使用其一个特性:禁止指令重排序优化。

也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

volatile的更多特性,可以看一下上篇文章《voliatile,synchronized,cas》

间接被引用情景

需要创建一次的对象不是直接被全局的引用所引用,而是间接地被引用。经常有这种情况,全局维护一个并发的ConcurrentMap, Map的每个Key对应一个对象,这个对象需要只创建一次

CAS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private final ConcurrentMap<String, InstanceObject> cache
= new ConcurrentHashMap<>();

public InstanceObject get(String key) {
InstanceObject single = cache.get(key);
if (single == null) {
InstanceObject instanceObject = new InstanceObject(key);
single = cache.putIfAbsent(key, instanceObject);
if (single == null) {
single = instanceObject;
}
}
return single;
}

使用这个很可能会产生多个InstanceObject对象,但最终只有一个InstanceObject有用

但并不没有达到仅创建一个的目标

如果创建InstanceObject的成本不高,那也不用太讲究

但一旦是大对象缓存,那么这很可能就是问题了,因为缓存中的对象获取成本一般都比较高,而且通常缓存都会经常失效,那么避免重复创建对象就有价值了

影子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private final ConcurrentMap<String, Future<InstanceObject>> cache1 = new ConcurrentHashMap<>();

public InstanceObject get1(final String key) {
Future<InstanceObject> future = cache1.get(key);
if (future == null) {
Callable<InstanceObject> callable = new Callable() {
@Override
public InstanceObject call() throws Exception {
return new InstanceObject(key);
}
};
FutureTask<InstanceObject> task = new FutureTask<>(callable);

future = cache1.putIfAbsent(key, task);
if (future == null) {
future = task;
task.run();
}
}

try {
return future.get();
} catch (Exception e) {
cache.remove(key);
throw new RuntimeException(e);
}
}

这儿使用Future来代替真实的对象,多次创建Future代价比创建缓存大对象小得多

自旋锁

觉得Future对象还是重了,那就使用更轻的AtomicBoolean,那其实主要使用的还是volatile的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final ConcurrentMap<String, AtomicBoolean> spinCache = new ConcurrentHashMap<>();

public InstanceObject getAtomic(final String key) {
InstanceObject single = cache.get(key);
if (single == null) {
AtomicBoolean newBoolean = new AtomicBoolean(false);
AtomicBoolean oldBoolean = spinCache.putIfAbsent(key, newBoolean);
if (oldBoolean == null) {
cache.put(key, new InstanceObject(key));
newBoolean.set(true);
} else {
//其他线程在自旋状态上自旋,等等被释放
while (!oldBoolean.get()) {}
}
single = cache.get(key);
}
return single;
}

总结

保守写法可以使用synchronized,lock,他们的性能也不低;但为了性能极致,可以使用上面的方式。

完整的测试代码:https://github.com/zhuxingsheng/javastudy/blob/master/src/main/java/com/jack/createonlyone/CreateOnlyOneMain.java

遥远的救世主

这本小说有改编的电视剧《天道》,不论是电视剧还是原著小说都看过多次,已经不记得是何缘起结识了这部书籍。

最近有个朋友又提到这部影剧,说影响了他的人生轨迹;使他看到并大概率得到波动命运琴弦的关键及能力

实在是惭愧,读了那么多次,尽然没有如此感悟,收获。再次翻书阅读

以我的学识、阅历,要读懂可能很难,甚至短时间没有可能性。但想想,那又怎么样呢?

如肖亚文所说

认识这个人就是开了一扇窗户,就能看到不一样的东西,听到不一样的声音,能让你思考,觉悟,这已经够了

能有这样一本,让你时时回味,时时思考,有所悟,或有所得的书籍,这已经很好了

文化属性

此书中的故事 皆有“文化属性”而来,那么吸引人的也大体是这个词了。

透视社会依次有三个层面:技术、制度和文化。小到一个人,大到一个国家一个民族,任何一种命运归根到底都是那种文化属性的产物。强势文化造就强者,弱势文化造就弱者,这是规律,也可以理解为天道,不以人的意志为转移

强势文化就是遵循事物规律的文化,弱势文化就是依赖强者的道德期望破格获取的文化,也是期望救主的文化。强势文化在武学上被称为“秘笈”,而弱势文化由于易学、易懂、易用,成了流行品种。

知道又如何?怎么用呢?

无所用,无所不用;比如文化产业,文学、影视是扒拉灵魂的艺术,如果文学、影视的创作能破解更高思维空间的文化密码,那么它的功效就是启迪人的觉悟,震撼人的灵魂,这就是众生所需,就是功德、市场、名利,精神拯救的暴利与毒品麻醉的暴利完全等值,而且不必像贩毒那样耍花招,没有心理成本和法律风险

中国的传统文化是皇恩浩大的文化,它的实用是以皇天在上为先决条件。中国为什么穷?穷就穷在幼稚的思维,穷在期望救主、期望救恩的文化上,这是渗透到民族骨子里的价值判断体系

神话

芮小丹让丁元英在王庙村写个神话

这个世上原本就没有什么神话,神话不过是常人的思维所不易理解的平常事

无论做什么,市场都不是一块无限大的蛋糕。神话的实质就是强力作用的杀富济贫,这就可能产生两个问题,一是杀富是不是破坏性开采市场资源? 二是让井底的人扒着井沿看了一眼再掉下去是不是让他患上精神绝症?

后面的故事,的确如丁所说,乐圣林雨峰自杀了,三个股东之一的刘冰也自杀了。仁人志士一片讨伐之声。

但这事真的神了吗?

从整个事件里,她没有看到丁元英有任何能让人感到“神”的招式,每一件具体的事都是普通人都能做到的普通事

他的确是在公开,公平的条件下合理合法的竞争,一切都是公开的,没有任何秘密和违法可言,所谓的“神话”竟是这么平淡,简单。实事求是就是神话,说老实话,办老实事的人就是神!

神就是道,道就是规律,规律如来,容不得你思议,按规律办事的人就是神

在小说开篇,其实就有实事求是,客观规律的论述

马克思主义的道理归根到底就是一句话:客观规律不以人的意志为转移。什么是客观规律?归根到底也是一句话:一切以时间,地点和条件为转移

造血功能

整本书貌似都在讲述文化属性,以及各种人潜意识里的文化属性结成了整个故事该有的结局

不是说谁本该成为哪种人是规律,而是说谁本该成为哪种人的条件的可能,因果不虚,因果是规、是律,不可思议

但如何解决造血功能呢?这可能是作者留给读者思考的内容,也就是人人都和各自的条件可能。

但如何才能做到事实求是呢?

本质的道作者已经说了很多很次,就是要事实求是,遵循规律,有道无术,术尚可求也,有术无道,止于术

不可思议一词不是众生道里的对神秘事物的描述,而是如是、本来、就是如此,容不得你思议。也是一种告诫、提示,是告诉你不可以思议,由不得你思议。从数学逻辑上说,一加一等于二,容得了你思议吗?不容,这就是告诉你了,一加一等于二是规律,规律是不以人的意志为转移,你只能认识、遵循,不可思议

如果你生活在一个虚妄的世界里,事实求是就很难,就如三个股东一样

观念,传统观念。一是传统的“事实胜于雄辩”的观念,二是传统的疑罪从有的观念,三是传统的青天大老爷的观念。中国人一直接受简单的文化思维教育,他们相信法律是神圣的,决不冤枉一个好人,也决不放过一个坏人

也如芮小丹说讲

只要不是我觉到,悟到的,你给不了我,给了我也拿不住。只有我自己觉到,悟到的我才有可能做到,我能做到的才是我的。

你的知道是自觉,现在是让你觉他。知道这个道理的人很多,但多是呈道理和知识存在。不是自觉。道理和知识是没用的,只是有用的一个条件。用才有用。让你觉他什么?觉他的无明,觉他的道理和知识的没用

通过否定法,至少要去掉两种思维,才可能性拥有造血功能

传统观念的死结就在一个“靠”字上,在家靠父母,出门靠朋友,靠上帝、靠菩萨、靠皇恩… 总之靠什么都行,就是别靠自己。这是一个沉积了几千年的文化属性问题,非几次新文化运动就能开悟。

咱们翻开历史看看,你从哪一行哪一页能找到救世主救世的记录?没有,从来就没有,从来都是救人的被救了,被救的救了人。如果一定要讲救世主的话,那么符合和代表客观规律的文化就是救世主。扶贫的本质在一个扶字,如果你根本就没打算自己站起来,老天爷来了都没用。

看一段线上的gc日志,这是一段CMS完整步骤的日志,对于GC日志格式,不了解的可以再温习一下《GC及JVM参数》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2017-07-18T21:28:41.422+0800: 11941915.242: [GC (CMS Initial Mark) [1 CMS-initial-mark: 786446K(1048576K)] 789098K(1992320K), 0.2623622 secs] [Times: user=0.00 sys=0.00, real=0.26 secs] 
2017-07-18T21:28:41.684+0800: 11941915.505: Total time for which application threads were stopped: 0.2630097 seconds, Stopping threads took: 0.0000587 seconds
2017-07-18T21:28:41.685+0800: 11941915.505: [CMS-concurrent-mark-start]
2017-07-18T21:29:02.443+0800: 11941936.263: [CMS-concurrent-mark: 20.758/20.758 secs] [Times: user=3.22 sys=1.67, real=20.75 secs]
2017-07-18T21:29:02.443+0800: 11941936.263: [CMS-concurrent-preclean-start]
2017-07-18T21:29:02.502+0800: 11941936.322: [CMS-concurrent-preclean: 0.059/0.059 secs] [Times: user=0.02 sys=0.01, real=0.06 secs]
2017-07-18T21:29:02.502+0800: 11941936.322: [CMS-concurrent-abortable-preclean-start]
CMS: abort preclean due to time 2017-07-18T21:29:07.600+0800: 11941941.420: [CMS-concurrent-abortable-preclean: 0.889/5.098 secs] [Times: user=1.72 sys=0.31, real=5.10 secs]
2017-07-18T21:29:07.602+0800: 11941941.422: Application time: 25.9175914 seconds
2017-07-18T21:29:07.603+0800: 11941941.423: [GC (CMS Final Remark) [YG occupancy: 491182 K (943744 K)]
2017-07-18T21:29:07.603+0800: 11941941.423: [Rescan (parallel) , 0.0654053 secs]
2017-07-18T21:29:07.668+0800: 11941941.488: [weak refs processing, 0.6491578 secs]
2017-07-18T21:29:08.317+0800: 11941942.138: [class unloading, 4.2229435 secs]
2017-07-18T21:29:12.540+0800: 11941946.361: [scrub symbol table, 0.0536739 secs]
2017-07-18T21:29:12.594+0800: 11941946.414: [scrub string table, 0.0009992 secs][1 CMS-remark: 786446K(1048576K)] 1277629K(1992320K), 5.0003976 secs] [Times: user=0.96 sys=0.01, real=5.00 secs]
2017-07-18T21:29:12.603+0800: 11941946.423: Total time for which application threads were stopped: 5.0011973 seconds, Stopping threads took: 0.0000483 seconds

对应着七个步骤:

  1. 初始标记(CMS-initial-mark) ,会导致swt;
  2. 并发标记(CMS-concurrent-mark),与用户线程同时运行;
  3. 预清理(CMS-concurrent-preclean),与用户线程同时运行;
  4. 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
  5. 重新标记(CMS-remark) ,会导致swt;
  6. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
  7. 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;

通过

1
[Times: user=0.96 sys=0.01, real=5.00 secs]

看出STW了5s,对于一个单台1万QPS的系统来讲,那5s就影响了上万次服务,这显示然达不到高可用的要求

通过对user,sys,real的对比,user+sys的时间远远小于real的值,这种情况说明停顿的时间并不是消耗在cpu执行上了,不是cpu那就是io导致的了

此时,可以通过sar命令查看一下

sar(System Activity Reporter系统活动情况报告)是目前 Linux 上最为全面的系统性能分析工具之一,可以从多方面对系统的活动进行报告,包括:文件的读写情况、系统调用的使用情况、磁盘I/O、CPU效率、内存使用状况、进程活动及IPC有关的活动等

sar -B

sar -B 输出说明:

输出项说明:

pgpgin/s:表示每秒从磁盘或SWAP置换到内存的字节数(KB)

pgpgout/s:表示每秒从内存置换到磁盘或SWAP的字节数(KB)

fault/s:每秒钟系统产生的缺页数,即主缺页与次缺页之和(major + minor)

majflt/s:每秒钟产生的主缺页数.

pgfree/s:每秒被放入空闲队列中的页个数

pgscank/s:每秒被kswapd扫描的页个数

pgscand/s:每秒直接被扫描的页个数

pgsteal/s:每秒钟从cache中被清除来满足内存需要的页个数

%vmeff:每秒清除的页(pgsteal)占总扫描页(pgscank+pgscand)的百分比

SWAP

可以看到大量的pgin,这儿就不得不再普及一下linux的swap

Linux divides its physical RAM (random access memory) into chucks of memory called pages. Swapping is the process whereby a page of memory is copied to the preconfigured space on the hard disk, called swap space, to free up that page of memory. The combined sizes of the physical memory and the swap space is the amount of virtual memory available.

Swap space in Linux is used when the amount of physical memory (RAM) is full. If the system needs more memory resources and the RAM is full, inactive pages in memory are moved to the swap space. While swap space can help machines with a small amount of RAM, it should not be considered a replacement for more RAM. Swap space is located on hard drives, which have a slower access time than physical memory.Swap space can be a dedicated swap partition (recommended), a swap file, or a combination of swap partitions and swap files.

Linux内核为了提高读写效率与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。即使你的程序运行结束后,Cache Memory也不会自动释放。这就会导致你在Linux系统中程序频繁读写文件后,你会发现可用物理内存变少。当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap空间中,等到那些程序要运行时,再从Swap分区中恢复保存的数据到内存中。这样,系统总是在物理内存不够时,才进行Swap交换。

swap vs 虚拟内存

windows:虚拟内存

linux:swap分区

windows即使物理内存没有用完也会去用到虚拟内存,而Linux不一样 Linux只有当物理内存用完的时候才会去动用虚拟内存(即swap分区)

swap类似于windows的虚拟内存,不同之处在于,Windows可以设置在windows的任何盘符下面,默认是在C盘,可以和系统文件放在一个分区里。而linux则是独立占用一个分区,方便由于内存需求不够的情况下,把一部分内容放在swap分区里,待内存有空余的情况下再继续执行,也称之为交换分区,交换空间是其中的部分
windows的虚拟内存是电脑自动设置的

为什么会停顿这么长时间呢?

  1. 堆内存分配多大,当gc时,的确需要很长时间
  2. 内存不够用时,使用了swap,在gc时,需要从swap加载到内存,耗时

解决思路

对于上面的原因,可以找出对应的方案:

  1. 分配小点,通过小而快的方式达到快速gc
  2. 定期检测old gen使用情况,当快要到达临界值时候(old gen使用率大于50%)主动执行cms gc

主动Gc可能会影响服务,所以可能需要服务先下线,gc完,再上线

参考资料

CMS垃圾回收器详解

GC Algorithms: Implementations