加密与解密
AES
1 | import java.io.IOException; |
Apache commons codec
1 | <dependency> |
1 | //1、MD5加密 |
1 | import java.io.IOException; |
1 | <dependency> |
1 | //1、MD5加密 |
THE config library 非常好用的API,用过之后不会再想使用其他配置工具
使用gradle
1 | compile 'com.typesafe:config:1.2.1' |
config使用的是HOCON的文件格式,这种文件格式类似于json,很灵活但没有歧义,能引用,能替换,能注释,能兼容老的properties等格式
The following features are desirable, to support human usage:
约束:utf8编码
1 | # 单行注释 |
1 | a : 1 |
1 | { |
1 | { |
1 | logo = """hello |
1 | // one object |
1 | data-center-generic = { cluster-size = 6 } |
1 | { a : { x : 1 } } (first priority) |
使用这样的语法 ${pathexpression} or ${?pathexpression}
1 | animal.favorite = dog |
pathexpression是绝对路径,替换是config对象构建的最后一步。
1 | include "foo" |
自动转换,时间单位(ns、ms、s、m、h etc),文件大小(kB、MB etc)
依次载入合并
1 | # 配置内容 |
使用下面的方法,会把最终所有的配置信息打印出来
1 | logger.debug(myConfig.root().render()) |
输出结果中包含所有载入的配置,如果使用了conf文件,会将对应文件的行信息也打印出来:
1 | { |
Docker动态给容器Container暴露端口
https://blog.csdn.net/lsziri/article/details/69396990
1 | docker pull hub.c.163.com/public/centos:6.7-tools |
1 | # 安装全部 yum install -y java-1.8.0-openjdk* |
install git
1 | yum install gcc gcc-c++ autoconf make automake -y |
安装oh-my-zsh
1 |
|
编辑~/.zshrc文件
找到plugins=(git)这一行,然后再添加autosuggestions,最后为:
1 | plugins=(git zsh-autosuggestions) |
1 | docker container cp target/demo-0.0.1-SNAPSHOT.jar centos-one:/home/deploy/ |
1 | docker commit 95c6d4eed5f0 jdk8-debug |
jhat
vmstat
新docker container,全新的系统,以纯净的系统Centos为例
1 | # 安装java8 |
1 | # 创建并切换到工作目录 |
创建package,用于存放java源文件和编译后的class文件
1 | [deploy@centos ~]$ mkdir -p src/com/lk/optimization/demo target |
// Thread thread = Thread.currentThread();
// System.out.println(thread.getName());
// thread.join();
System.out.println(“join over”);
1 | package com.lk.optimization.demo; |
1 | [deploy@centos ~]$ find src -name "*.java" | xargs javac -d target |
1 | [deploy@centos ~]$ nohup java -cp target -Xcomp -XX:MaxNewSize=1000000 -XX:MaxHeapSize=2000000 com.lk.optimization.demo.DemoTest & |
ps命令选项:
1 | ********* simple selection ********* ********* selection by list ********* |
root用户查看进程信息
1 | [root@centos ~]# ps -ef | grep -v ps |
ps 命令
字段的解释可以在man页的STANDARD FORMAT SPECIFIERS中查找
默认情况下,ps显示与当前用户相同EUID以及调用了同一个终端的进程。
[dd-]hh:mm:ss格式显示的CPU时间样例中的进程信息:
| 进程id | 解释 |
|---|---|
| 0 | 0号进程是根进程 |
| 1 | 通过sshd发起的ssh连接 |
| 421 | ssh连接,用户为root,终端为pts/1,子进程全部这个终端 |
| 423 | bash shell |
| 785 | 切换deploy账户 |
| 786 | deploy账户的bash shell |
| 1129 | java启动进程 |
1 | [root@centos ~]# ps -flL -p 1129 |
线程线程的选项:
通过上面的例子,可以看出
线程id是1129~1154, 共27个线程
实际handler线程和main线程加起来是11个,为什么是27个线程呢
以线程模式查看下进程31951的所有线程情况
1 | top -Hp 31951 |

1 | -H : Threads toggle |
jstack中的线程id为16进制,需要将从top或ps获取的线程id转换为16进制,
1 | printf %x <tid> |
注意:
- jstack必须和运行的JVM进程是同一个用户
1 | Options: |
打印虚拟机所有的参数
1 | java -XX:+PrintFlagsFinal -version | grep : |
1 | jinfo -flags $PID |
JVM statistics
1 | ➜ jstat -options |
GC堆统计
1 | jstat -gc 12196 |
GC统计
1 | jstat -gcutil 12196 |
1 | -gcutil option |
full gc前后dump
1 | java -XX:HeapDumpPath=./heap/ -XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpAfterFullGC |
HeapDumpPath: dump pathHeapDumpOnOutOfMemoryError 内存溢出dumpHeapDumpAfterFullGC、HeapDumpBeforeFullGC :Full GC前后dump
Prints shared object memory maps or heap memory details for a process, core file, or remote debug server. This command is experimental and unsupported.
打印进程、核心文件或远程调试服务器的共享对象内存映射或堆内存详细信息
jmap -heap <pid>
堆的各个分区大小
展示堆满的情况下的各个分区大小
1 | > jmap -heap 16186 |
1 | # 开启远程调试端口 |
例子:
1 | java \ |
修改catalina.sh,
1 | JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=0.0.0.0 |
深入解析String.intern
原文出处: 深入解析String.intern
字符串的常量池
常量池在哪里?
-XX:+UseStringDeduplication 开启String去重
-XX:+PrintStringDeduplicationStatistics 打印详细的去重统计
-XX:StringDeduplicationAgeThreshold=15 达到这个年龄的String对象才会去重
1 | for(int i=0;i<collection.size();i++) // collection.size() 提取成常量 |
1 | for(Map.Entry<String,String> entry:map.entrySet()){ |
System.arrayCopy()1 | Integer i=100; |
1 | Code: |
1 | Integer i1=100; |
1 | Code: |
1 | Integer i1=1000; |
1 | Code: |
java.lang.Integer.IntegerCache.high(默认值是127),则返回缓存1 | public synchronized void f1() { |
1 | public class TestPrimaryType{ |
1 | replace VS replaceAll: 尽量用replace |
1 | log.info(“orderId:”+orgerId); // 不推荐 |
jmap -heap $PID、jcmd或者HeapDumpOnOutOfMemoryError等等手段都可以获取,使用MAT找超级powerful的机器分析;MAT 其实提供了分析脚本可以在不用 IDE 加载整个 HEAP 获取到需要的信息, 也就是通过脚本解析 HEAP 逐步分析, 具体可参考 How to Analyse Large Heap Dumps. PS: 曾经分析过 108G 的堆jmap -histo $PID或jmap -histo:live $PID获得当前堆内对象的个数统计,并采样多次,查看一下里面object的分布,看哪类对象比较多,一般来说200G的堆,做一次jmap -histo可能要几十秒到一分钟,可能会触发full gc,如果使用 CMS 或 G1 的话, 可加入 -XX:+ExplicitGCInvokesConcurrent 使用并发收集器显式;如果不能探明的话, 只能使用 jmap 将整个堆 dump 下来。How to Analyse Large Heap Dumps
下载MAT
到MAT的安装目录下,打开MemoryAnalyzer.ini,调整MAT的启动jvm参数
1 | -Xms6144m |
堆最大大小调整为机器内存大小
dump文件所在文件夹,确保有dump文件两倍的空间
到MAT的安装目录下,使用root账户运行命令
For UNIX:
1 | ./ParseHeapDump.sh /opt/heap_dump/jvm.hprof org.eclipse.mat.api:suspects |
For Windows:
1 | ParseHeapDump.bat D:\heap_dump\jvm.hprof org.eclipse.mat.api:suspects |
使用MAT打开生成的分析结果文件
| Young 年轻代 | Tenured 老生代 | JVM options | 备注 |
|---|---|---|---|
| Serial | Serial | -XX:+UseSerialGC | 单线程回收,全程STW |
| Parallel Scavenge | Serial | -XX:+UseParallelGC -XX:-UseParallelOldGC | 年轻代并行,老年代串行,全程STW |
| Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC | 多线程回收,全程STW |
| Parallel New或Serial | CMS | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC | 年轻代并行或串行,老年代并发,只有某个阶段会STW |
| G1 | G1 | -XX:+UseG1GC | 并发回收, 某个阶段会STW |
垃圾回收器从线程运行情况分类有三种:
串行回收: Serial回收器,单线程回收,全程STW;并行回收: 名称以Parallel开头的回收器,多线程回收,全程STW;并发回收: CMS与G1,多线程分阶段回收,只有某阶段会STW;
分代回收中:
Minor GC清理年轻代(Young GC),除了G1 GC外,都会STW
Major GC清理老年代(Tenured GC)
Full GC清理整个堆
Minor GC触发条件:
Major GC触发条件:
Full GC触发条件:
System.gc时,系统建议执行Full GC,不是必然执行jmap -histo:live <pid> 或者 jmap -dump:live,file=dump_001.bin PID,然后删掉dump_001.bin文件并发,低停顿
特别是拥有大量长期数据(大老年代),多核心,低停顿
启用-XX:+UseConcMarkSweepGC
CMS收集器是分代的。 因此,minor GC和major GC都会发生。 CMS收集器尝试通过使用单独的垃圾收集器线程在执行应用程序线程的同时跟踪可访问对象,来减少由于major GC而导致的暂停时间。 在每个major收集周期中,CMS收集器会在收集开始时暂停所有应用程序线程一小段时间,然后收集中间再暂停一次。 第二次停顿往往是两个停顿中较长的一个。 在两个暂停期间都使用多个线程来执行收集工作。 收集的其余部分(包括大部分活动对象的跟踪和无法访问对象的清除)是通过与应用程序同时运行的一个或多个垃圾收集器线程来完成的。minor GC可以与正在进行的主要周期交错,并在一个 类似于并行收集器的方式(特别是在次要收集期间停止了应用程序线程)。
并发模式失效Concurrent Mode Failure
System.gc())中断if the CMS collector is unable to finish reclaiming the unreachable objects before the tenured generation fills up, or if an allocation cannot be satisfied with the available free space blocks in the tenured generation, then the application is paused and the collection is completed with all the application threads stopped. The inability to complete a collection concurrently is referred to as concurrent mode failure and indicates the need to adjust the CMS collector parameters. If a concurrent collection is interrupted by an explicit garbage collection (System.gc()) or for a garbage collection needed to provide information for diagnostic tools, then a concurrent mode interruption is reported.
Excessive GC Time and OutOfMemoryError
太多的时间花在gc上: 如果总时间的98%花在GC上,并且回收不到2%的堆空间,将抛出OutOfMemoryError
禁用命令行: -XX:-UseGCOverheadLimit
浮动垃圾Floating Garbage
边收集边运行,出现浮动垃圾
CMS只会回收老年代和永久代(1.8开始为元数据区,需要设置CMSClassUnloadingEnabled),不会收集年轻代;
CMS是一种预处理垃圾回收器,它不能等到老年代内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败(并发回收降级);
所以CMS垃圾回收器开始执行回收操作,有一个触发阈值(参数名称),默认是老年代或永久代达到92%;
CMS 处理过程有七个步骤:
| 步骤 | 是否STW | 详情 |
|---|---|---|
| 初始标记(CMS-initial-mark) | 会导致STW | 标记GCRoot和被年轻代引用的老年代对象 |
| 并发标记(CMS-concurrent-mark) | 与用户线程同时运行; | 扫描整个老年代,将引用关系变化的对象置为dirty |
| 预清理(CMS-concurrent-preclean) | 与用户线程同时运行; | |
| 可被终止的预清理(CMS-concurrent-abortable-preclean) | 与用户线程同时运行; | |
| 重新标记(CMS-remark) | 会导致STW | |
| 并发清除(CMS-concurrent-sweep) | 与用户线程同时运行; | |
| 并发重置状态等待下次CMS的触发(CMS-concurrent-reset) | 与用户线程同时运行; |
CMS运行流程图如下所示:

这是CMS中两次stop-the-world事件中的一次。这一步的作用是标记存活的对象,有两部分:

在Java语言里,可作为GC Roots对象的包括如下几种:
ps:为了加快此阶段处理速度,减少停顿时间:
- 开启并行化初始标记:
-XX:+CMSParallelInitialMarkEnabled- 同时调大并行标记的线程数,线程数不要超过cpu的核数:
-XX:ConcGCThreads=4
通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象。
由于应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如:
对于这些对象,需要重新标记以防止被遗漏。为了提高重新标记的效率,本阶段只会把发生变化的对象所在的Card标识为Dirty,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
如下图所示,也就是节点1、2、3,最终找到了节点4和5。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。

这个阶段因为是并发的, 容易导致concurrent mode failure
在并发预清洗阶段,将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象,然后清除Card标识。
前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Direty的Card
如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;

最后将6标记为存活,如下图所示:

本阶段尽可能承担更多的并发预处理工作,从而减轻在Final Remark阶段的stop-the-world。
这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生abort的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。
ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻代的引用,是的下个阶段的重新标记阶段,扫描年轻代指向老年代的引用的时间减少;
在该阶段,主要循环的做两件事:
具体执行多久,取决于许多因素,满足其中一个条件将会中止运行:
预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。
因此,需要有一个stop-the-world的阶段来完成最后的标记工作,这就是重新标记阶段(CMS标记阶段的最后一个阶段)。主要目的是重新扫描之前并发处理阶段的所有残留更新对象。
主要工作:
遍历新生代对象,重新标记;(新生代会被分块,多线程扫描)
根据GC Roots,重新标记;
遍历老年代的Dirty Card,重新标记。这里的Dirty Card,大部分已经在Preclean阶段被处理过了。
这个阶段会导致第二次stop the world,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含young_gen和old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做CMS的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:
当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻代的对象无用的对象,并将对象放入survivor区或晋升到老年代,这样再进行年轻代扫描时,只需要扫描幸存区的对象即可,一般survivor区非常小,这大大减少了扫描时间
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻代的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候。
另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled
并发清理阶段,主要工作是清理所有未被标记的死亡对象,回收被占用的空间。

通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。
这个阶段主要是清除那些没有标记的对象并且回收空间;
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
并发重置阶段,将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备。
这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。
下面就是该参数设置打印出来的gc信息,一些非关键的信息已经去掉,如时间:
1 | //第一步 初始标记 这一步会停顿* |
输出GC详情,需要添加 -verbose:gc 和 -XX:+PrintGCDetails 参数
CMS-initial-mark标示着并发收集周期的开始CMS-concurrent-mark标示着并发标记阶段的结束CMS-concurrent-sweep标志着并发清理阶段的结束CMS-concurrent-preclean标志着预清理阶段,预清理代表着在准备CMS-remark阶段可以并发处理的工作CMS-concurrent-reset是最后阶段,为下一次并发收集做准备
CMS-initial-mark indicates the start of the concurrent collection cycle,
CMS-concurrent-mark indicates the end of the concurrent marking phase,
and CMS-concurrent-sweep marks the end of the concurrent sweeping phase.
Not discussed previously is the precleaning phase indicated by CMS-concurrent-preclean.
Precleaning represents work that can be done concurrently in preparation for the remark phase CMS-remark.
The final phase is indicated by CMS-concurrent-reset and is in preparation for the next concurrent collection.
下面抓取一下gc信息,来进行详细分析,首先将jvm中加入以下运行参数:
先来介绍下下面几个参数的作用:
[0] 打印出启动参数行
[1] 参数指定使用CMS垃圾回收器;
[2]、[3] 参数指定CMS垃圾回收器在老年代达到80%的时候开始工作,如果不指定那么默认的值为92%;
[4] 开启永久代(jdk1.8以下版本)或元数据区(jdk1.8及其以上版本)收集,如果没有设置这个标志,一旦永久代或元数据区间也会尝试进行垃圾回收,但是收集不会是并行的,而再一次进行Full GC;
[5] 使用CMS时默认这个参数就是打开的,不需要配置,CMS只回收老年代,年轻代只能配合Parallel New或Serial回收器;
[6] 减少Remark阶段暂停的时间,启用并行Remark,如果Remark阶段暂停时间长,可以启用这个参数
[7] 如果Remark阶段暂停时间太长,可以启用这个参数,在Remark执行之前,先做一次ygc。因为这个阶段,年轻代也是CMS的gcroot,CMS会扫描年轻代指向老年代对象的引用,如果年轻代有大量引用需要被扫描,会让Remark阶段耗时增加;
[8]、[9]两个参数是针对CMS垃圾回收器碎片做优化的,CMS是不会移动内存的, 运行时间长了,会产生很多内存碎片, 导致没有一段连续区域可以存放大对象,出现”promotion failed”、”concurrent mode failure”, 导致fullgc,启用UseCMSCompactAtFullCollection 在FULL GC的时候, 对年老代的内存进行压缩。-XX:CMSFullGCsBeforeCompaction=0 则是代表多少次FGC后对老年代做压缩操作,默认值为0,代表每次都压缩, 把对象移动到内存的最左边,可能会影响性能,但是可以消除碎片;
1 | 106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs] |
[11] 定义并发CMS过程运行时的线程数。比如value=4意味着CMS周期的所有阶段都以4个线程来执行。尽管更多的线程会加快并发CMS过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加CMS线程数是否真的能够带来性能的提升。如果未设置这个参数,JVM会根据并行收集器中的-XX:ParallelGCThreads参数的值来计算出默认的并行CMS线程数:
1 | ParallelGCThreads = (ncpus <=8 ? ncpus : 8+(ncpus-8)*5/8) ,ncpus为cpu个数, |
这个参数一般不要自己设置,使用默认就好,除非发现默认的参数有调整的必要;
[12]、[13]开启foreground CMS GC,CMS gc 有两种模式,background和foreground,正常的CMS gc使用background模式,就是我们平时说的CMS gc;当并发收集失败或者调用了System.gc()的时候,就会导致一次full gc,这个fullgc是不是CMS回收,而是Serial单线程回收器,加入了参数[12]后,执行full gc的时候,就变成了CMS foreground gc,它是并行full gc,只会执行CMS中stop the world阶段的操作,效率比单线程Serial full GC要高;需要注意的是它只会回收old,因为CMS收集器是老年代收集器;而正常的Serial收集是包含整个堆的,加入了参数[13],代表永久代也会被CMS收集;
[14] 开启初始标记过程中的并行化,进一步提升初始化标记效率;
[15]、[16]、[17]、[18] 、[19]是打印gc日志,其中[16]在jdk1.8之后无需设置
[20]、[21]则是内存溢出时dump堆
有一点需要注意的是:CMS并发GC不是“full GC”。HotSpot VM里对concurrent collection和full collection有明确的区分。所有带有“FullCollection”字样的VM参数都是跟真正的full GC相关,而跟CMS并发GC无关的,CMS收集算法只是清理老年代。
一般CMS的GC耗时 80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数:
-XX:+CMSScavengeBeforeRemark
在执行remark操作之前先做一次ygc,目的在于减少ygen对oldgen的无效引用,降低remark时的开销,如果添加该参数后 ”ygc停顿时间+remark时间<添加该参数之前的remark时间“,说明该参数是有效的;
CMS是基于标记-清除算法的,只会将标记为为存活的对象删除,并不会移动对象整理内存空间,会造成内存碎片,这时候我们需要用到这个参数;
-XX:CMSFullGCsBeforeCompaction=n
这个参数大部分人的使用方式都是错误的,往往会导致设置后问题更大。
CMSFullGCsBeforeCompaction这个参数在HotSpot VM里是这样声明的:
product(bool, UseCMSCompactAtFullCollection, true, \
“Use mark sweep compact at full collections”) \
\
product(uintx, CMSFullGCsBeforeCompaction, 0, \
“Number of CMS full collection done before compaction if > 0”) \
然后这样使用的:
*should_compact =
UseCMSCompactAtFullCollection &&
((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||
GCCause::is_user_requested_gc(gch->gc_cause()) ||
gch->incremental_collection_will_fail(true */* consult_young /));
CMS GC要决定是否在full GC时做压缩,会依赖几个条件。其中,
UseCMSCompactAtFullCollection 与 CMSFullGCsBeforeCompaction 是搭配使用的;前者目前默认就是true了,也就是关键在后者上。
用户调用了System.gc(),而且DisableExplicitGC没有开启。
young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。
上述三种条件的任意一种成立都会让CMS决定这次做full GC时要做压缩。
CMSFullGCsBeforeCompaction 说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 如果把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩(而不是每10次CMS并发GC就做一次压缩,目前VM里没有这样的参数)。这会使full GC更少做压缩,也就更容易使CMS的old gen受碎片化问题的困扰。 本来这个参数就是用来配置降低full GC压缩的频率,以期减少某些full GC的暂停时间。CMS回退到full GC时用的算法是mark-sweep-compact,但compaction是可选的,不做的话碎片化会严重些但这次full GC的暂停时间会短些;这是个取舍。
这个异常发生在CMS正在回收的时候。执行CMS GC的过程中,同时业务线程也在运行,当年轻代空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。
设置CMS触发时机有两个参数:
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC。
-XX:+UseCMSInitiatingOccupancyOnly如果不指定, 只是用设定的回收阈值CMSInitiatingOccupancyFraction,则JVM仅在第一次使用设定值,后续则自动调整会导致上面的那个参数不起作用。
为什么要有这两个参数?
由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
CMS前五个阶段都是标记存活对象的,除了”初始标记”和”重新标记”阶段会stop the word ,其它三个阶段都是与用户线程一起跑的,就会出现这样的情况gc线程正在标记存活对象,用户线程同时向老年代提升新的对象,清理工作还没有开始,old gen已经没有空间容纳更多对象了,这时候就会导致concurrent mode failure, 然后就会使用串行收集器回收老年代的垃圾,导致停顿的时间非常长。
CMSInitiatingOccupancyFraction参数要设置一个合理的值,设置大了,会增加concurrent mode failure发生的频率,设置的小了,又会增加CMS频率,所以要根据应用的运行情况来选取一个合理的值。
如果发现这两个参数设置大了会导致fullgc,设置小了会导致频繁的CMSgc,说明你的老年代空间过小,应该增加老年代空间的大小了;
这个异常发生在年轻代回收的时候;
在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年带有足够的空闲空间,但是由于碎片较多,新生代要转移到老年带的对象比较大,找不到一段连续区域存放这个对象导致的,以下是一段promotion failed的日志:
106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]
(concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]
过早提升与提升失败
在 Minor GC 过程中,Survivor Unused 可能不足以容纳 Eden 和另一个 Survivor 中的存活对象, 那么多余的将被移到老年代, 称为过早提升(Premature Promotion),这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。 再进一步, 如果老年代满了, Minor GC 后会进行 Full GC, 这将导致遍历整个堆, 称为提升失败(Promotion Failure)。
早提升的原因
Survivor空间太小,容纳不下全部的运行时短生命周期的对象,如果是这个原因,可以尝试将Survivor调大,否则端生命周期的对象提升过快,导致老年代很快就被占满,从而引起频繁的full gc;
对象太大,Survivor和Eden没有足够大的空间来存放这些大象;
提升失败原因
当提升的时候,发现老年代也没有足够的连续空间来容纳该对象。
为什么是没有足够的连续空间而不是空闲空间呢?
老年代容纳不下提升的对象有两种情况:
老年代空闲空间不够用了;
老年代虽然空闲空间很多,但是碎片太多,没有连续的空闲空间存放该对象;
解决方法
如果是因为内存碎片导致的大对象提升失败,CMS需要进行空间整理压缩;
如果是因为提升过快导致的,说明Survivor 空闲空间不足,那么可以尝试调大 Survivor;
如果是因为老年代空间不够导致的,尝试将CMS触发的阈值调低;
linux使用了swap,内存换入换出(vmstat),尤其是开启了大内存页的时候,因为swap只支持4k的内存页,大内存页的大小为2M,大内存页在swap的交换的时候需要先将swap中4k内存页合并成一个大内存页再放入内存或将大内存页切分为4k的内存页放入swap,合并和切分的操作会导致操作系统占用cup飙高,用户cpu占用反而很低;
除了swap交换外,网络io(netstat)、磁盘I/O (iostat)在 GC 过程中发生会使 GC 时间变长。
如果是以上原因,就要去查看gc日志中的Times耗时:
[Times: user=0.00 sys=0.00, real=0.00 secs]
user是用户线程占用的时间,sys是系统线程占用的时间,如果是io导致的问题,会有两种情况
[ Times: user=0.51 sys=0.10, real=5.00 secs ]
user+sys的时间远远小于real的值,这种情况说明停顿的时间并不是消耗在cup执行上了,不是cup肯定就是io导致的了,所以这时候要去检查系统的io情况。
sys时间很长,user时间很短,real几乎等于sys的时间,如下:
[ Times: user=0.11 sys=31.10, real=33.12 secs ]
这时候其中一种原因是开启了大内存页,还开启了swap,大内存进行swap交换时会有这种现象;
CMS默认启动的回收线程数目是 (ParallelGCThreads + 3)/4) ,这里的ParallelGCThreads是年轻代的并行收集线程数,感觉有点怪怪的;
年轻代的并行收集线程数默认是(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8),可以通过-XX:ParallelGCThreads= N 来调整;
如果要直接设定CMS回收线程数,可以通过-XX:ParallelCMSThreads=n,注意这个n不能超过cpu线程数,需要注意的是增加gc线程数,就会和应用争抢资源;
参考
https://plumbr.eu/handbook/garbage-collection-algorithms-implementations#concurrent-mark-and-sweep
http://www.infoq.com/cn/presentations/a-long-period-of-atypical-jvm-gc-caused-by-os/
GC Cause
Heap Inspection Initiated GC
因为执行了jmap -histo:live 触发的gc


jmeter是一款纯java的性能测试工具,跨平台运行方便、提供图形化界面设置、简单易用。
在性能测试方法论中,很典型的方法就是二八原则,量化业务需求。
二八原则:指80%的业务量在20%的时间里完成。
如何理解,下面我们来个例子吧
用户登录场景:早高峰时段,8:50—9:10,5000坐席上线登陆。
业务量:5000个
时间:20x60=1200秒
吞吐量=80%x业务量/(20%*时间)=4000/240=16.7/秒
而并非5000/1200=4.1/秒
实际上,登录请求数分布是一个正态分布,最高峰时肯定比4.1/秒更高,高峰段实际上完成了80%的业务量,却只花了20%的时间。
温馨提示:
1.二八原则计算的结果并非在线并发用户数,是系统要达到的处理能力(吞吐量),初学者容易被误导,那这这个数据就去设置并发数,这是错误滴。
2.如果你的系统性能要求更高,也可以选择一九原则或更严格的算法,二八原则比较通用,一般系统性能比较接近这个算法而已,大家应该活用。
3.tps、响应时间、在线并发数三者关系详解:点击打开链接
三者关系图

注:执行机资源消耗必须监控上,保证能提供稳定的并发负载。
注:这里的响应时间是90%响应时间
tps:
每秒事务处理量 - 性能测试的术语介绍
TPS(Transaction Per Second)
每秒钟系统能够处理的交易或事务的数量。它是衡量系统处理能力的重要指标。TPS是LoadRunner中重要的性能参数指标。
1.下载安装
仅仅需要从apache的网站找到下载包,解压到本地文件目录即可。
http://jmeter.apache.org/download_jmeter.cgi
2.启动
解压目录中存在一个bin的目录,里面有很多批处理文件和脚本文件,window系统运行jmeter.bat即可。需要关注的是bin目录中的jmeter.properties文件,这是运行相关的配置文件. 特别是TCP Sampler configuration部分几个配置会和后面内容相关
3.建立一种类型测试
这里只描述简单的tcp测试建立步骤,因为目前支持的测试类型很多,无法一一陈述,功能细节部分可以参考JMeter文档
1)创建测试线程组
1. 启动测试用接口
首先我们写一段 php 代码,通过 PHP 内置的 Server 启动它。
1 | $user_id = $_GET['user_id']; |
以上代码保存为 index.php
命令中执行 php -S 127.0.0.1:8080
在浏览器访问 http://127.0.0.1:8080/index.php?user_id=1 , 输出 1 说明服务接口正常
2. 创建线程组
使用 JMeter 测试应用性能首先要创建一个线程组
右键 “Text Plan”, 在弹出的菜单栏选择 “Add->Threads(Users)->Thread Group”
就创建了一个线程组:

“Number of Threads (users): ” 即并发用户数,相当于 ab 命令的 -c 参数
“Loop Count:” 循环请求次数, 即每个线程请求多少次, 这个数据乘以线程数相当于 ab 命令的 -n 参数
我们设置了 “Number of Threads (users)” 为 5 , “Loop Count” 为 60 , 相当于ab 命令
1 | ab -c 5 -n 300 http://xxx.com |
2. 创建测试请求
右键我们刚刚创建的线程组“Thread Group”, 选择 “Add-> Sampler-> HTTP Request”

这一步相当于通过多个参数拼出要测试的接口地址。
注意Path中, ${__counter(false)} 为 JMeter 内置的函数, 它的返回值为当前请求次数
**这样保证了我们每次向服务器请求的 user_id 的值都不一样 **
此时我们将要进行的测试等同于 ab 测试命令:
1 | ab -c 5 -n 300 http://127.0.0.1/index.php?user_id=1 |
每次调用计数器函数都会产生一个新值,从1开始每次加1。计数器既可以被配置成针对每个虚拟用户是独立的,也可以被配置成所有虚拟用户公用的。如果每个虚拟用户的计数器是独立增长的,那么通常被用于记录测试计划运行了多少遍。全局计数器通常被用于记录发送了多少次请求。
计数器使用一个整数值来记录,允许的最大值为2,147,483,647。
功能:这个函数是一个计数器,用于统计函数的使用次数,它从1开始,每调用这个函数一次它就会自动加1,它有两个参数,第一个参数是布尔型的,只能设置成“TRUE”或者“FALSE”,如果是TRUE,那么每个用户有自己的计数器,可以用于统计每个线程歌执行了多少次。如果是FALSE,那就使用全局计数器,可以统计出这次测试共运行了多少次。第二个参数是“函数名称”
格式:${__counter(FALSE,test)}
**使用:**我们将“_counter”函数生成的参数复制到某个参数下面,如果为TRUE格式,则每个线程各自统计,最大数为循环数,如果为FALSE,则所有线程一起统计,最大数为线程数乘以循环数
参数:
第一个参数:True,如果测试人员希望每个虚拟用户的计数器保持独立,与其他用户的计数器相区别。False,全局计数器
第二个参数:重用计数器函数创建值的引用名。测试人员可以这样引用计数器的值:${test}。这样一来,测试人员就可以创建一个计数器后,在多个地方引用它的值。
以上,摘自网络(不知道怎么用,只好摘抄,记录下来等灵感~~~~(>_<)~~~~ )。
目前,我测试_Counter函数,就是在参数列表加一个参数,值填写为${__counter(FALSE,test)}
)
3.开始测试
右键线程组 “Thread Group”, 选择 “Add-> Listener->Summary Report “, 创建一个结果报表
然后点击, 菜单栏中的绿色按钮, 开始测试:

结果如图:

打开 ‘/tmp/1.log’ 可以看到,每次请求的 user_id的值都是不同的。
1.线程组,或者可以叫用户组,进行性能测试时的用户资源池。
2.是任何一个测试计划执行的开始点。
3.上一篇提到的“控制器”和“HTTP请求”(采集器)必须在线程组内;监听器等其他组件,可以直接放在测试计划下。
https://www.cnblogs.com/linglingyuese/archive/2013/03/06/linglingyuese-three.html
二、Thread Group线程组功能分区
总的来说,一个线程组有三个功能分区,这里分别标注为区域1、区域2、区域3。

1.区域1:在取样器错误后要执行的动作,这个区域的主要作用很明显,在线程内的采样器失败后,接下来做什么。
(1)继续:选择此项,将继续执行接下来的操作。
(2)Start Next Loop:忽略错误,执行下一个循环。
(3)停止线程:退出该线程(不再进行此线程的任何操作)。
(4)停止测试:等待当前执行的采样器结束后,结束整个测试。
(5)Stop Test Now:直接停止整个测试。(注意与4的“停止测试”进行区分)。
2.区域2:线程属性,这里可以设置线程数(模拟的用户数)和循环次数。含义如下图所示:

ramp up:斜坡上升; [动词短语] 加强,加大;
相当于warm up的一个词,包含准备,热身,加速的意思,可用在生产中小批量的试制中, 也可以指人初入公司的锻炼. 在项目初始阶段要做许多准备工作。
3.区域3:调度器配置(全部都在调度器复选框被选中的前提下,下面的选项才会生效。)

最重要的Tcp Sampler:tcp取样器
TCP Sampler提供了3个Sampler的实现,分别是
org.apache.jmeter.protocol.tcp.sampler.TCPClientImpl
org.apache.jmeter.protocol.tcp.sampler.BinaryTCPClientImpl和
org.apache.jmeter.protocol.tcp.sampler.LengthPrefixedBinaryTCPClientImpl。
其中TCPClientImpl实现了以文本编辑器中所编辑的纯文本为内容进行发送,BinaryTCPClientImpl则以文本编辑器中所编辑的16进制字符(hex)内容为基础转换为二进制的字节内容进行发送,LengthPrefixedBinaryTCPClientImpl则会在BinaryTCPClientImpl基础上默认以发送内容的长度以字节前缀进行填充。
我们可以通过配置jmeter.properties文件中tcp.handler属性来设置默认的TCPClient。
被测应用的源码请参见这里. 如果想运行该程序,请点击该链接下载socket_echo-0.0.1-SNAPSHOT.jar,并且在命令行下执行:
https://github.com/XMeterSaaSService/Blog\_sample\_project/tree/master/socket_echo
(javac 和java可以去掉包名后再在命令行执行)
1 | java -cp socket_echo-0.0.1-SNAPSHOT.jar net.xmeter.echo.TextServer这个程序源码: |

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; public class TextServer { public static AtomicInteger sessions = new AtomicInteger(0); public void handleRequest(final Socket socket) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(new Runnable() {
@Override public void run() { try {
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter os = new PrintWriter(socket.getOutputStream()); while(true) {
String line = is.readLine(); if(line == null) {
System.out.println("Probably the client side closed the connection, now close me as well.");
socket.close(); break;
}
System.out.println("Received message: " + line);
os.println("Echo: " + line);
os.flush(); if("bye".equals(line)) { break;
}
}
} catch(Exception ex) {
ex.printStackTrace();
} finally { try {
socket.close(); int num = sessions.decrementAndGet();
System.out.println("Now totally has " + num + " of conn.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
} public static void main(String\[\] args) { try {
ServerSocket server = new ServerSocket(4700); while(true) {
Socket socket = server.accept();
TextServer srv = new TextServer();
srv.handleRequest(socket); int num = sessions.incrementAndGet();
System.out.println("Received new conn, now totally has " + num + " of conn.");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1 | (这个程序测试: |
1 | 注意几图:hello后面有个换行, ENDof line Byte value 填写的是10.LF (NL line feed, new line) 换行键 ,ascill是10.os.println("Echo: " + line); 用的是println,服务端返回的最后是一个换行符。如果不填写EOF byte value,那么客户端将会一直阻塞没有返回。 |
我们发现EOL原来是与读数据相关的,就是设定来自于服务器数据流的一个结束标识字节。没有设置EOL将会一直读到输入流结束为止。
这里值得注意的是,这是个十进制的值(千万不要写成hex),比如你可以查询ASCII表,来确认一个表示结束字符的十进制值,我们以$作为案例,改造一下Mock TCP Server,输出结尾为$,如下面代码:
)
1 |
(请确保您的机器上已经安装了Java)。 该程序会在4700端口建立一个ServerSocket,等待来自客户端的请求,客户端如果发送了一个字符串,服务器端返回“Echo: “ + 客户端发送的字符串。如下图所示,如果我们使用telnet连接到服务器端的套接字应用,双方就可以直接进行通信了。
我们使用TCPClientImpl对Mock TCP Server进行测试,配置参考下图:

点击运行测试,你会发现测试发生了阻塞,原因是服务器使用了readLine获取客户端的发送数据,需要根据发送数据中的CRLF(\r或\n)判断一行的结束。而我们制作的发送内容并不包括CRLF标识内容,因此,服务器阻塞在了读数据,测试客户端得不到服务器响应,同样也阻塞在了读数据,正确的配置需要添加一个“回车”(不能是”\r”或”\n”,因为TCPClientImpl会自动将其转换为对应的两个字符而不是CRLF标识)参考下图
TCP 取样器通过TCP/IP来连接特定服务器,连上服务器之后发送消息,然后等待服务器回复。
如果“Re-use connection”(重复使用连接) 复选框被选中了,在同一个线程中Samplers(取样器)共享连接,包含相同主机名和端口,不同主机/端口合并将会使用不同线程。如果“Re-use connection” 和 “Close connection”(关闭连接)同时被选中,这个套接字在运行完当前Samplers将会关闭。再下一个Sampler将会另外创建一个新套接字。你可能想要在每次线程循环结束之后关闭套接字。
如果一个错误被检测到或者“Re-use connection” 没有被选中,这个套接字将会关闭,另外套接字将会在接下Samplers被再一次打开。
详细看这篇文章:
还有这篇文章:https://www.jianshu.com/p/63e08071075e
jmeter报告结果中会出现三个时间
Elapsed time 经过的时间(= Sample time = Load time = Response time )
这个时间是我们测试常用的时间,也是整个请求的消耗时间,从发送到接收完成全程消耗的时间
Latency time 延迟时间
不常用,表示请求发送到刚开始接收响应时,这个时间<Elapsed time
3. Connection time 建立连接时间 (2.13新增参数)
不常用,请求连接建立的时间,这个时间 < Latency time < Elapsed time
另一个整理http://alanhou.org/java-optimization/
1 | javap <options> <classes> |
即可保存字节码文件
会有三个部分组成
操作数栈
LineNumberTable
LocalVariableTable
i++和++i 的执行效果完全相同 多了一个压入栈顶操作
for(int i=0;i<10;i++) {}
for(int i=0;i<10;++i) {} 执行效果一样
2:
public static void f1() {
String src = “”;
for(int i=0;i<10;i++) {
//每一次循环都会new一个StringBuilder 然后在src.append(“A”);
src = src + “A”;
}
System.out.println(src);
}
public static void f2() {
//只要一个StringBuilder
StringBuilder src = new StringBuilder();
for(int i=0;i<10;i++) {
src.append(“A”);
}
System.out.println(src);
}
3:
public static String f1() {
String str = “hello”;
try{
return str;
}
finally{
str = “imooc”;
}
} 返回 hello 但会执行finally 中的代码
4:字符串拼接都会在编译阶段转换成stringbuilder
5:字符串去重
字符串在任何应用中都占用了大量的内存。尤其数包含独立UTF-16字符的char[]数组对JVM内存的消耗贡献最多——因为每个字符占用2位。
内存的30%被字符串消耗其实是很常见的,不仅是因为字符串是与我们互动的最好的格式,而且是由于流行的HTTP API使用了大量的字符串。使用Java 8 Update 20,我们现在可以接触到一个新特性,叫做字符串去重,该特性需要G1垃圾回收器,该垃圾回收器默认是被关闭的。
字符串去重利用了字符串内部实际是char数组,并且是final的特性,所以JVM可以任意的操纵他们。
对于字符串去重,开发者考虑了大量的策略,但最终的实现采用了下面的方式:
无论何时垃圾回收器访问了String对象,它会对char数组进行一个标记。它获取char数组的hash value并把它和一个对数组的弱引用存在一起。只要垃圾回收器发现另一个字符串,而这个字符串和char数组具有相同的hash code,那么就会对两者进行一个字符一个字符的比对。
如果他们恰好匹配,那么一个字符串就会被修改,指向第二个字符串的char数组。第一个char数组就不再被引用,也就可以被回收了。
这整个过程当然带来了一些开销,但是被很紧实的上限控制了。例如,如果一个字符未发现有重复,那么一段时间之内,它会不再被检查。
那么该特性实际上是怎么工作的呢?首先,你需要刚刚发布的Java 8 Update 20,然后按照这个配置: -Xmx256m -XX:+UseG1GC 去运行下列的代码:
1 | public class LotsOfStrings { |
这段代码会执行30个迭代之后报OutOfMemoryError。
现在,开启字符串去重,使用如下配置去跑上述代码:
-Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics
此时它已经可以运行更长的时间,而且在50个迭代之后才终止。
6:
ArrayLIst 底层是数组 扩容会拷贝
hashmap 底层也是数组+ 链表 扩容 重新计算key 负载因子是 0.75
linklist底层是双向链表
1. 尽量重用对象,不要循环创建对象,比如:for 循环字符串拼接(不在 for中使用+拼接,先new 一个StringBuilder再在 for 里 append)
2. 容器类初始化的地时候指定长度
List
Map<String, String> map = new HashMap<String, String>(32);
3. ArrayList(底层数组)随机遍历快,LinkedList(底层双向链表)添加删除快
4. 集合遍历尽量减少重复计算
5. 使用 Entry 遍历 Map可以同时取出key和value
6. 大数组复制使用System.arraycopy 底层是native实现的
7. 尽量使用基本类型而不是包装类型
public class Test03 {
public static void main(String[] args) {
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
System.out.println(f1 == f2);
System.out.println(f3 == f4);
}
}
如果不明就里很容易认为两个输出要么都是true要么都是false。首先需要注意的是f1、f2、f3、f4四个变量都是Integer对象引用,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,如果看看valueOf的源代码就知道发生了什么。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1==f2的结果是true,而f3==f4的结果是false。
8. 不要手动调用 System.gc()
9. 及时消除过期对象的引用,防止内存泄漏
public string pop()
{
string currentValue=object[size];
//object[size]=null;如果不添加这句话就会造成内存泄漏
size–;
return currentValue;
}
10. 尽量使用局部变量,减小变量的作用域 方便出了作用域尽快垃圾回收
11. 尽量使用非同步的容器ArraryList vs. Vector
12. 尽量减小同步作用范围, synchronized 方法 vs. 代码块
public class SynchronizedTest {
public static void main(String[] args) {
}
public synchronized void f1() {//在this對象上加鎖
System.out.println(“f1”);
}
public void f2() {//在this對象上加鎖
synchronized(this) {
System.out.println(“f2”);
}
}
public static synchronized void f3() {//在类上加鎖
System.out.println(“f3”);
}
public static void f4() {//在类上加鎖
synchronized(SynchronizedTest.class) {
System.out.println(“f4”);
}
}
}
13. 用ThreadLocal 缓存线程不安全的对象,SimpleDateFormat 缓存重量的对象避免重新构造
@SuppressWarnings(“rawtypes”)
private static ThreadLocal threadLocal = new ThreadLocal() {
protected synchronized Object initialValue() {
return new SimpleDateFormat(DATE_FORMAT);
}
};
14. 尽量使用延迟加载
15. 尽量减少使用反射,必须用加缓存,反射比较影响性能
16. 尽量使用连接池、线程池、对象池、缓存
17. 及时释放资源, I/O 流、Socket、数据库连接
18. 慎用异常,不要用抛异常来表示正常的业务逻辑,异常也是比较重的对象要记录堆栈信息
19. String 操作尽量少用正则表达式 比如replaceAll是用正则 比较耗费性能 replace就不是用正则
20. 日志输出注意使用不同的级别
21. 日志中参数拼接使用占位符
log.info(“orderId:” + orderId); 不推荐 会用字符串拼接
log.info(“orderId:{}”, orderId); 推荐 用占位符 不会进行字符串拼接
7:JVM的参数类型
标准参数(各版本中保持稳定)
-help
-server -client
-version -showversion
-cp -classpath
X 参数(非标准化参数)
-Xint:解释执行
-Xcomp:第一次使用就编译成本地代码
-Xmixed:混合模式,JVM 自己决定是否编译成本地代码
示例:
java -version(默认是混合模式)
Java HotSpot(TM) 64-Bit Server VM (build 25.40-b25, mixed mode)
java -Xint -version
Java HotSpot(TM) 64-Bit Server VM (build 25.40-b25, interpreted mode)
XX 参数(非标准化参数)
主要用于 JVM调优和 debug
格式:-XX:[+-]
如:-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
格式:-XX:
如:-XX:MaxGCPauseMillis=500
-xx:GCTimeRatio=19
-Xmx -Xms属于 XX 参数
-Xms 等价于-XX:InitialHeapSize
-Xmx 等价于-XX:MaxHeapSize
-xss 等价于-XX:ThreadStackSize
-XX:+PrintFlagsInitial 查看jvm初始值
-XX:+PrintFlagsFinal 查看jvm最终值
-XX:+UnlockExperimentalVMOptions 解锁实验参数
-XX:+UnlockDiagnosticVMOptions 解锁诊断参数
-XX:+PrintCommandLineFlags 打印命令行参数
输出结果中=表示默认值,:=表示被用户或 JVM 修改后的值
示例:java -XX:+PrintFlagsFinal -version
补充:测试中需要用到 Tomcat,CentOS 7安装示例如下
sudo yum -y ``install java-1.8.0-openjdk*
wget http://mirror.bit.edu.cn/apache/tomcat/tomcat-8/v8.5.32/bin/apache-tomcat-8.5.32.tar.gz
tar -zxvf apache-tomcat-8.5.32.tar.gz
mv apache-tomcat-8.5.32 tomcat
cd tomcat/bin/sh startup.sh
pid 可通过类似 ps -ef|grep tomcat或 jps来进行查看
查看java进程 -l 可以知道完全类名
jinfo -flag MaxHeapSize
jinfo -flags
可以查看jvm的统计信息 如类加载。垃圾回收信息,jit编译信息
详情参考 jstat 官方文档

类加载
jstat -class
垃圾收集
-gc, -gcutil, -gccause, -gcnew, -gcold
jstat -gc
以下大小的单位均为 KB

S0C, S1C, S0U, S1U: S0和 S1的总量和使用量
EC, EU: Eden区总量与使用量
OC, OU: Old区总量与使用量
MC, MU: Metacspace区(jdk1.8前为 PermGen)总量与使用量
CCSC, CCSU: 压缩类区总量与使用量
YGC, YGCT: YoungGC 的次数与时间
FGC, FGCT: FullGC 的次数与时间
GCT: 总的 GC 时间
JIT 编译
-compiler, -printcompilation
一个对象默认分配在堆上面 但是有个指针指向class默认是64位长指针,可以设置为用32位存储在压缩类空间
非堆区 即对应于虚拟机规范中的方法区 是操作系统本地内存 独立于jvm堆区之外 jdk8后面叫metaspace jdk8前面叫performancespace
codecache 存储的是jit即时编译的代码 以及native代码
详情参考jmap 官方文档
内存溢出演示:
https://start.spring.io/生成初始代码
最终代码:monitor_tuning
为快速产生内存溢出,右击 Run As>Run Configurations, Arguments 标签VM arguments 中填入
-Xmx32M -Xms32M

Exception in thread “http-nio-8080-exec-2” Exception in thread “http-nio-8080-exec-1” java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: Java heap space
-XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M(同时在 pom.xml 中加入 asm 的依赖)

访问 http://localhost:8080/nonheap
Exception in thread “main” java.lang.OutOfMemoryError: Metaspace
Exception in thread “ContainerBackgroundProcessor[StandardEngine[Tomcat]]“ java.lang.OutOfMemoryError: Metaspace
内存溢出自动导出
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./
右击 Run As>Run Configurations, Arguments 标签VM arguments 中填入
-Xmx32M -Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
可以看到自动在当前目录中生成了一个java_pid660.hprof文件
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to ./java_pid660.hprof …
另一种导出溢出也更推荐的方式是jmap
option: -heap, -clstats, -dump:
jmap -dump:format=b,file=heap.hprof

MAT下载地址:http://www.eclipse.org/mat/
找开上述导出的内存溢出文件即可进行分析,如下图的溢出源头分析:

Histogram
[](http://static.oschina.net/uploads/space/2014/0702/120039_qSi5_1767531.png)
Class Name : 类名称,java类名
Objects : 类的对象的数量,这个对象被创建了多少个
Shallow Heap :一个对象内存的消耗大小,不包含对其他对象的引用
Retained Heap :是shallow Heap的总和,也就是该对象被GC之后所能回收到内存的总和
Dominator Tree
我们可以看到ibatis占了较多内存
快速找出某个实例没被释放的原因,可以右健 Path to GC Roots–>exclue all phantom/weak/soft etc. reference :

得到的结果是:

从表中可以看出 PreferenceManager -> … ->HomePage这条线路就引用着这个 HomePage实例。用这个方法可以快速找到某个对象的 GC Root,一个存在 GC Root的对象是不会被 GC回收掉的.
详情参考 jstack 官方文档
jstack
jstack 15672 >15673.txt 导出当前进程文件
可查看其中包含java.lang.Thread.State: WAITING (parking),JAVA 线程包含的状态有:
NEW:线程尚未启动
RUNNABLE:线程正在 JVM 中执行
BLOCKED:线程在等待监控锁(monitor lock)
WAITING:线程在等待另一个线程进行特定操作(时间不确定)
TIMED_WAITING:线程等待另一个线程进行限时操作
TERMINATED:线程已退出
此时会生成一个monitor_tuning-0.0.1-SNAPSHOT.jar的 jar包,为避免本地的 CPU 消耗过多导致死机,建议上传上传到虚拟机进行测试
nohup java -jar monitor_tuning-0.0.1-SNAPSHOT.jar &
访问 http://xx.xx.xx.xx:12345/loop(端口12345在application.properties文件中定义)
top 是查询所有进程的cpu 占用率
top还可以用来显示一个进程中各个线程CPU的占用率:top -p
top命令如下

top -p

使用 jstack
“http-nio-12345-exec-3” #18 daemon prio=5 os_prio=0 tid=0x00007f10003fb000 nid=0x668 runnable [0x00007f0fcf8f9000]
java.lang.Thread.State: RUNNABLE
at org.alanhou.monitor_tuning.chapter2.CpuController.getPartneridsFromJson(CpuController.java:77)
…
访问http://xx.xx.xx.xx:12345/deadlock(如上jstack

“Thread-5”:
at org.alanhou.monitor_tuning.chapter2.CpuController.lambda$deadlock$1(CpuController.java:41)
- waiting to lock <0x00000000edcf3470> (a java.lang.Object)
- locked <0x00000000edcf3480> (a java.lang.Object)
at org.alanhou.monitor_tuning.chapter2.CpuController$$Lambda$337/547045985.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
“Thread-4”:
at org.alanhou.monitor_tuning.chapter2.CpuController.lambda$deadlock$0(CpuController.java:33)
- waiting to lock <0x00000000edcf3480> (a java.lang.Object)
- locked <0x00000000edcf3470> (a java.lang.Object)
at org.alanhou.monitor_tuning.chapter2.CpuController$$Lambda$336/1704575158.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
查看后台日志,都是使用tail -f catalina.out命令来查看
jvisualvm 图形化工具
插件安装Tools>Plugins>Settings根据自身版本(java -version)更新插件中心地址,各版本查询地址:
http://visualvm.github.io/pluginscenters.html
建议安装:Visual GC, BTrace Workbench
概述 监控可以堆dump 线程可以线程dump 抽样器可以对cpu和内存进行抽样调查
以上是本地的JAVA进程监控,还可以进行远程的监控,在上图左侧导航的 Applications 下的 Remote 处右击Add Remote Host…,输入主机 IP 即可添加,在 IP 上右击会发现有两种连接 JAVA 进程进行监控的方式:JMX, jstatd
bin/catalina.sh(以192.168.0.5为例)
JAVA_OPTS=”$JAVA_OPTS -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9004 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.net.preferIPv4Stack=true -Djava.rmi.server.hostname=192.168.0.5”
启动tomcat,
启动tomcat服务
方式一:直接启动 ./startup.sh
方式二:作为服务启动 nohup ./startup.sh &
查看tomcat运行日志
tail -f catalina.out
tomcat设置jvm参数
修改文件 apache-tomcat-9.0.10/bin下catalina.bat文件
以 JMX 为例,在 IP 上右击点击Add JMX Connection…,输入 IP:PORT

以上为 Tomcat,其它 JAVA 进程也是类似的,如:
nohup java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9005 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.net.preferIPv4Stack=true -Djava.rmi.server.hostname=192.168.0.5 -jar monitor_tuning-0.0.1-SNAPSHOT.jar &
BTrace 可以动态地向目标应用程序的字节码注入追踪代码,使用的技术有 JavaCompilerApi, JVMTI, Agent, Instrumentation+ASM
使用方法:JVisualVM中添加 BTrace 插件
方法二:btrace
btrace只能调试本地进程
btrace修改后的字节码不能被还原
pom.xml 中添加 btrace-agent, btrace-boot, btrace-client的依赖


拦截构造方法

拦截同名方法

拦截返回值

拦截行号

拦截异常信息

拦截复杂类型

拦截正则表达式

拦截环境参数信息
常用参数:
-Xms -Xmx
-XX:NewSize -XX:MaxNewSize
-XX:NewRatio -XX:SurvivorRatio
-XX:MetaspaceSize -XX:MaxMetaspaceSize 以下几个参数通常这样只设置这个值即可
-XX:+UseCompressedClassPointers
-XX:CompressedClassSpaceSize
-XX:InitialCodeCacheSize
-XX:ReservedCodeCacheSize
Tomcat 远程 Debug
JDWP
bin/startup.sh 修改最后一行(添加 jpda)
exec “$PRGDIR”/“$EXECUTABLE” jpda start “$@”
bin/catalina.sh 为便于远程调试进行如下修改
JPDA_ADDRESS=”localhost:8000”
JPDA_ADDRESS=”54321”
若发现54321端口启动存在问题可尝试bin/catalina.sh jpda start
使用 Eclipse 远程调试,右击 Debug As > Debug Configurations… > Remote Java Application > 右击 New 新建
普通java进程可以这样配置
java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=10001 access-10000.jar
tomcat-manager 监控
1.conf/tomcat-users.xml添加用户
2.conf/Catalina/localhost/manager.xml配置允许的远程连接
远程连接将allow=”127\.0\.0\.1”修改为allow=”^.*$”,浏览器中输入http://127.0.0.1:8080/manage或对应的 IP,用户名密码为tomcat-users.xml中所设置的
3.重启 Tomcat 服务

psi-probe 监控
下载地址:https://github.com/psi-probe/psi-probe,
下载后进入psi-probe-master目录,执行:
mvn clean package -Dmaven.test.skip
将 web/target/probe.war放到 Tomcat 的 webapps 目录下,同样需要conf/tomcat-users.xml和conf/Catalina/localhost/manager.xml中的配置(可保持不变),启动 Tomcat 服务
浏览器中输入http://127.0.0.1:8080/probe或对应的 IP,用户名密码为tomcat-users.xml中所设置的

Tomcat 调优
线程优化(webapps/docs/config/http.html):
maxConnections
acceptCount
maxThreads
minSpareThreads
配置优化(webapps/docs/config/host.html):
autoDeploy
enableLookups(http.html)
reloadable(context.html)
protocol=”org.apache.coyote.http11.Http11AprProtocol”
Session 优化:
如果是 JSP, 可以禁用 Session
Nginx 安装
添加 yum 源(/etc/yum.repos.d/nginx.repo)
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basesearch/
gpgcheck=0
enabled=1
安装及常用命令
yum install -y nginx
systemctl status|start|stop|reload|restart nginx
nginx -s stop|reload|quit|reopen nginx 启动nginx
cat default.conf | grep -v “#’ > default2.conf 移除配置文件中的注释 并生成新的配置文件
nginx -V
nginx -t
配置反向代理 setenforce 0
ngx_http_stub_status 监控连接信息
location = /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
可通过curl http://127.0.0.1/nginx_status 进行查看或注释掉 allow 和 deny 两行使用 IP 进行访问
ngxtop监控请求信息
查看官方使用方法:https://github.com/lebinh/ngxtop
yum install epel-release
yum install python-pip
pip install ngxtop
使用示例
指定配置文件:ngxtop -c /etc/nginx/nginx.conf
查询状态是200:ngxtop -c /etc/nginx/nginx.conf -i ‘status == 200’
查询访问最多 ip:ngxtop -c /etc/nginx/nginx.conf -g remote_addr

Nginx 优化
增加工作线程数和并发连接数
worker_processes 4; # 一般CPU 是几核就设置为几
events {
worker_connections 1024; # 每个进程打开的最大连接数,包含了 Nginx 与客户端和 Nginx 与 upstream 之间的连接
multi_accept on; # 可以一次建立多个连接
use epoll;
}
启用长连接
upstream server_pool{
server localhost:8080 weight=1 max_fails=2 fail_timeout=30s;
server localhost:8081 weight=1 max_fails=2 fail_timeout=30s;
keepalive 300; # 300个长连接
}
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;
proxy_pass http://server\_pool;
}
启用缓存压缩
gzip on;
gzip_http_version 1.1;
gzip_disable “MSIE [1-6]\.(?!.*SV1)”;
gzip_proxied any;
gzip_types text/plain text/css application/javascript application/x-javascript application/json application/xml application/vnd.ms-fontobject application/x-font-ttf application/svg+xml application/x-icon;
gzip_vary on;
gzip_static on;
操作系统优化
sysctl -w net.ipv4.tcp_syncookies=1 # 防止一个套接字在有过多试图连接到时引起过载
sysctl -w net.core.somaxconn=1024 # 默认128,连接队列
sysctl -w net.ipv4.tcp_fin_timeout=10 # timewait 的超时时间
sysctl -w net.ipv4.tcp_tw_reuse=1 # os 直接使用 timewait的连接
sysctl -w net.ipv4.tcp_tw_recycle=0 # 回收禁用
hard nofile 204800
soft nofile 204800
soft core unlimited
soft stack 204800
其它优化
sendfile on; # 减少文件在应用和内核之间拷贝
tcp_nopush on; # 当数据包达到一定大小再发送
tcp_nodelay off; # 有数据随时发送
1 | case class Event(id: Int) { |
1 | 13:00:43,342 INFO org.apache.flink.api.java.typeutils.TypeExtractor - class org.myorg.quickstart.Event does not contain a setter for field id |
提示信息:找不到setter,对于POJO类型必须所有的字段必须要有setter和getter
命名是case class啊
再看生产环境的例子: 折腾了一下午
1 | case class Event(uin: String, |
使用的是flink 1.6版本的,case class识别出来了,但是equities没有传递到下一个算子中,始终没有值
老老实实的修改成普通类
1 | class Event(_uin: String, |
可以了
初步估计,序列化除了问题
flink 类型和序列化机制
flink 支持的数据类型
Java Tuples 跟 Scala Case 类
Java POJOs
基础类型(Primitive Types : int/long/string/char/short/boolean 等)
普通的类(非POJO)
Values
Hadoop Writable
特殊类型(Scala : Either, Option, Try; Java : List, Map)
flink 支持的序列化
Tuple
Row
Pojo
Avro
Protobuf (via Kryo)
Thrift (via Kryo)
Kryo

flink 序列化性能
flink serialization performance results
可以看到 flink 内置的 Tuple 跟 Row 性能最好, POJO 次之,
一般 Tuple 跟 POJO使用的频率最高, 但是只有POJO 跟 Avro 支持 Schema 升级
POJO 一不小心就可能回退到 Kryo
POJO 第一个要求是符合 Java Bean 规范, 但是目前(1.12.2) 还不支持特殊的类型(List, Map等)
为了避免 POJO序列化回退, 开发过程中可以开启
env.getConfig().disableGenericTypes();
当POJO 中包含List/Map 处理方式
1 | public class Pojo1 { |
因为POJO包含了不支持的List该序列化, names 字段序列化会回退到 Kryo序列化
解决方案
1: 把 List 换成 Array
1 | public static class Pojo1 { |
2: 指定 POJO 字段 TypeInformation
1 | public static void main(String[] args) { |
引用
https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/types_serialization.html
https://flink.apache.org/news/2020/04/15/flink-serialization-tuning-vol-1.html
Flink的序列化
Flink实现了自己的序列化框架,并结合自身的内存模型,实现了对象的密集存储也高效操作。
Flink序列化框架

可以看出这种序列化方式存储密度是相当紧凑的。其中 int 占4字节,double 占8字节,POJO多个一个字节的header,PojoSerializer只负责将header序列化进去,并委托每个字段对应的serializer对字段进行序列化。
memory pool 内存池 memorySegment的数据结构,由两部分组成,一部分是存储key+pointer(完整二进制数据的指针以及定长的序列化后的key),第二部分是对象的二进制数据
如下图:

使用内存池管理内存和使用二进制存储数据的的好处:
避免oom,所有的运行时数据结构和算法只能通过内存池申请内存,保证了其使用的内存大小是固定的,不会因为运行时数据结构和算法而发生OOM。在内存吃紧的情况下,算法(sort/join等)会高效地将一大批内存块写到磁盘,之后再读回来。因此,OutOfMemoryErrors可以有效地被避免。
节省内存空间,Java 对象在存储上有很多额外的消耗,使用二进制可以避免。
高效的二进制操作 & 缓存友好的计算,第一,交换定长块(key+pointer)更高效,不用交换真实的数据也不用移动其他key和pointer。第二,这样做是缓存友好的,因为key都是连续存储在内存中的,可以大大减少 cache miss(cpu读取L1,L2,L3高速缓存速度高于读取主内存速度几个数量级,使用key+pointer极大提高缓存L1,L2,L3命中率)
注意:Flink 中,排序会先用 key 比大小,这样就可以直接用二进制的key比较而不需要反序列化出整个对象。因为key是定长的,如果key相同(或者没有提供二进制key),那就必须将真实的二进制数据反序列化出来,然后再做比较。之后,只需要交换key+pointer就可以达到排序的效果,真实的数据不用移动。
1)该类型系统与SQL的兼容性不好。
2)无法控制Decimal类型的精度。
3)无法区分char和varchar类型。
4)物理类型和逻辑类型紧耦合。
5)物理类型是类型描述,而不是类型的序列化/反序列化器。
Flink SQL引入了新的LogicalTypes类型系统
TypeInformation类型系统是为DataStream/DataSet API设计的
DataType有两个职责:
1)声明逻辑类型LogicalType。
2)运行时逻辑转换类,允许为空。


类型推断:
Java的Flink应用使用反射机制获取Function的输入和输出类型。Scala使用Scala Macro类提取类型。
类型提取:

泛型的类型推断:
Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息
使用TypeHint的匿名类来获取泛型的类型信息

Lambda函数的类型提取
Eclipse的JDT编译器会把Lambda函数的泛型签名等信息写入编译后的字节码中,而对于javac等常见的其他编译器,则不会这样做,因而Flink就无法获取具体类型信息了
(1)Java类型擦除的原因
1)避免JVM的重构。如果JVM将泛型类型延续到运行期,那么到运行期时JVM就需要进行大量的重构工作,提高了运行期的效率。
2)版本兼容。在编译期擦除可以更好地支持原生类型(Raw Type)。
(2)Java泛型类型擦除规则
1)如果是继承基类而来的泛型,就用getGenericSuperclass(), 转型为ParameterizedType来获得实际类型。
2)如果是实现接口而来的泛型,就用getGenericInterfaces(), 针对其中的元素转型为ParameterizedType来获得实际类型。
3)Java泛型在字节码中会被擦除,并不总是擦除为Object类型,而是擦除到上限类型。
显示类型:
Flink提供了等价的Types类
(org.apache.flink.api.common.typeinfo.Types),Types作为类型声明的统一入口,基本涵盖了常用类型。
类型擦除带来的问题
org.apache.flink.api.java.typeutils.runtime.kryo.JavaSerializer,而非com.esot-ericsoftware.kryo.serializers.JavaSerializer,以防止与Flink不兼容Flink SQL中则使用DataType中的LogicalType类型系统来描述类型信息
LogicalType类型系统与SQL标准基本保持一致,同时增加了一些额外的信息,如是否可以为null等,目的是提高scala expression(标量表达式)的处理效率。
Flink SQL执行时,最终转换为了FlinkDataStream/DataSet应用,此时就需要TypeInfomation类型信息来实现序列化/反序列化,所以SQL逻辑类型LogicalType需要转换为TypeInfomation

1)org.apache.flink.types.Row:在Flink Planner中使用,是1.9版本之前FlinkSQL使用的Row结构,在SQL相关的算子、UDF函数、代码生成中都是使用该套Row结构。
2)org.apache.flink.table.dataformat.BaseRow及其子类:是在Blink Runtime和Blink Planner中使用的新的Row类型数据结构,在Blink算子、UDF函数和代码生成中使用此结构。
Blink Row总览

ColumnarRow
ColumnarRow是一种内存列式存储结构,每一列的抽象结构为ColumnVector。在当前的实现中,只支持堆上ColumnVector,堆外的ColumnVector尚不被支持。堆上ColumnVector本质上是使用Java原始类型数据保存一列的数据。Orc类型的列式存储使用了ColumnarRow。对于查询类的请求,使用列式存储能够提高CPU缓存命中率。CPU的数据预读取策略总是尝试将相邻的数据预读取到缓存中,因为列式存储形式中一列数据总是紧邻的,与行式数据相比,访问同一个字段的时候,CPU缓存命中率更高,因此CPU就无须浪费宝贵的事件周期去等待数据从内存加载,从而提高计算效率,如图5-8所示。

MapFunction使用了匿名内部类的方式实现,默认内部类会持有一个外部对象的引用this$0,如果外部对象不实现序列化接口,内部类的序列化会失败,所在Flink中使用ASM操作字节码将匿名内部类中的this$0设置为null。在FlinkDataStreamp的map、filter、keyBy等接口中都使用了ClosureCleaner#clean方法来设置this$0。
如果开发者在编写Flink应用过程中使用了自定义类型,并且又没有提供类型的注册和序列化/反序列化方法,Flink就无法对该类型进行该自定义序列化/反序列化。此时为了Flink的正常运行,对于这一类的数据类型,无法识别的类型就会交给Kryo进行序列化。Kryo可以对任意类型的Java对象进行序列化,是一种Java中的通用序列化方式,缺点是序列化/反序列化效率相对较低。
奇怪,没有依赖HBase-Client,而是依赖了HBase-server
1 | <dependency> |
1 | <dependency> |
1 | // Get the classloader actually used by HBaseConfiguration |