-
Notifications
You must be signed in to change notification settings - Fork 1
/
201803.txt
547 lines (478 loc) · 44 KB
/
201803.txt
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
ThreadPoolExecutor: This class provides protected overridable beforeExecute(Thread, Runnable) and afterExecute(Runnable, Throwable) methods that are called before and after execution of each task. These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals, gathering statistics, or adding log entries. Additionally, method terminated can be overridden to perform any special processing that needs to be done once the Executor has fully terminated.
If hook or callback methods throw exceptions, internal worker threads may in turn fail and abruptly terminate.
public class ThreadPoolExecutor extends AbstractExecutorService {
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
//Packing and unpacking ctl
private static int runSateOf(int c) {return c & ~CAPACITY;}
private static int workerCountOf(int c) {return c & CAPACITY;}
private static int ctlOf(int rs, int wc) {return rs | wc;}
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/* Proceed in 3 steps
1. If fewer than corePoolSize threads are running, try to start a new thread with the given
command as its first task. The call to addWorker atomically checks runState and workerCount,
and so prevents false alarms that would add threads when it shouldn't, by returning false.
2. If a task can be successfully queued, then we will need to double-check whether we should
have added a thread(because existing ones died since last checking) or that the pool shut down
since entry into this method. So we recheck state and if necessary roll back the enqueuing if
stopped, or start a new thread if there are none.
3. If we cannot queue task, then we try to add a new thread. If it fails, we know we are shut down
or saturated and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
} else if (!addWorker(command, false))
reject(command);
}
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maxmumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); //Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry innner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//Recheck while holding lock.
//Back out on ThreadFactory failure or if shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) //precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (!workerStarted)
addWorkerFailed(w);
}
return workedStarted;
}
private final class Worker extends AbstractQueuedSynchronizer
implements Runnable {
private static final long serialVersionUID = 6138294804551838833L;
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (comareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
public class FutureTask<V> implements RunnableFuture<V> {
...
}
架构图分类:一般比较流行的是4+1视图,分别为场景视图、逻辑视图、物理视图、处理流程视图和开发视图。
1. 场景视图:场景视图用于描述系统的参与者与功能用例间的关系,反映系统的最终需求和交互设计,通常由用例图表示。
2. 逻辑视图:逻辑视图用于描述系统软件功能拆解后的组件关系、组件约束和边界,反映系统组成与系统如何构建的过程,通常由UML的组件图和类图来表示。
3. 物理视图:物理视图用于描述系统软件到物理硬件的映射关系,反映出系统的组件是如何部署到一组可计算机器节点上,用于指导软件系统的部署实施过程。
4. 处理流程视图:处理流程视图用于描述系统软件组件之间的通信时序,数据的输入输出,反映系统的功能流程与数据流程,通常由时序图和流程图表示。
5. 开发视图:开发视图用于描述系统的模块划分和组成,以及细化到内部包的组成设计,服务于开发人员,反映系统开发实施过程。
C4模型使用容器(应用程序、数据存储、微服务等)、组件和代码来描述一个软件系统的静态结构。
C4模型: 代表上下文(Context), 容器(Container), 组件(Component)和代码(Code)。
Redis分片的实现:
1. 客户端分片(Client side partitioning)
2. 代理分片(Proxy assisted partitioning):Twemproxy
3. 查询路由(Query routing): 发送到任意实例,实例负责转发到正确的节点
Redis分片的缺点:
1. 涉及多个键的操作不再原子支持。如不能对两个分片上的键执行交集、并集、差集等
2. 跨多个分片的事务不支持;
3. 扩容/缩容需要认值考虑。
4. 如果一台出现宕机,那么整个分片不可用,需要考虑高可用性(考虑Redis Sentinel/Redis Cluster)。
Redis分片方式:
1. 范围分区
2. 一致性哈希
3. 哈希
Redis集群没有使用一致性哈希,而是另外一种分片形式。每个键概念上为哈希槽(hash slot)的一部分。Redis Cluster有16384个哈希槽,使用键的CRC16编码对16384取模来计算一个指定键的哈希槽。每个Redis集群中的节点都承担一个哈希槽的子集。因为从一个节点向另外一个节点移动哈希槽并不需要停止操作,所以添加或移除节点,或者改变节点持有的哈希槽百分比,都不需要任何停机时间(downtime)。
Redis集群支持使用cluster failover命令来手动故障转移,必须在想进行故障转移的主服务的一个从服务器上执行。手动故障转移比因主服务器失效而产生的故障转移更安全,因为采取了避免过程中数据丢失的方式,仅当系统确认新的主服务器处理完了旧的主服务器的复制流时,客户端才从原主服务器切换到新主服务器。
一般来说,基于Redis客户端分片通常使用hash(一致性hash)、取模等算法。很多时候常用的都采用一致性哈希算法,一致性哈希算法的好处是当Redis节点进行增减只会影响新增或删除节点前后的小部分数据,相对于取模等算法来说,对数据的影响范围较小。如果将Redis作为缓存,并且不考虑数据丢失导致的缓存穿透造成的影响,在Redis节点增减时可以不用考虑部分数据无法命中的问题。如果将Redis作为一个NOSQL的数据库或者缓存穿透影响很大,则需要进行数据的迁移,或者使用预分配的方式来延迟或者规避扩容。
ShardedJedis提供了一致性哈希和MD5两种哈希算法,默认使用一致性哈希算法。并且为了使请求能均匀的落在不同的节点上,ShardedJedis会使用节点的名称虚拟化出160个虚拟节点。也可以根据不同节点的weight,虚拟化出160*weight个节点。
基于客户端的Redis分片的缺点:
1. 分片等操作在客户端,客户端规模比较大时,会增加运维的难度;
2. 后续Redis扩容时需要修改客户端的配置,甚至重启客户端;
3. 每台客户端服务与每台Redis节点都单独建立连接,当客户端规模比较大时,仍不能共享连接资源,造成资源浪费,而且不易优化。
基于Proxy服务的分片能很好解决上面的问题,但Proxy服务也有很大的不足。由于中间多了一层代理转发,会造成一定程度上的性能下降,并且需要使用KeepAlive等保证Proxy服务的高可用性。常用的Proxy代理有Twemproxy, Codis等。
基于redis服务器的分片,又可称之为“查询路由”,就是客户端随机连接redis集群中的一个节点,向其发送读写请求,如果这个请求不能够被当前节点处理,则这个节点会将请求转发给正确的节点来处理(这一点很像zookeeper中的主从写请求机制),有的实现并不会由当前节点转发给其他节点,而是当前节点响应给客户端一个正确节点的信息,由客户端再次向正确的节点发出请求。
总的来说,ShardedJedis分片的过程如下:
1. 根据redis节点名称虚拟出160*weight个虚拟节点。
2. 在对key进行存取时,先通过keytag(key或key的一部分)计算出hash值,找到离他最近的比它大的虚拟节点,然后把请求发送到这个节点上。
通过redis.clients.jedis.ShardedJedis的源码,发现Redis很多涉及多个key操作的命令ShardedJedis都没有支持,这是因为在分片时,这些key可能被分片到不同的Redis节点上,这就会造成:
1. 如果是读取操作,则响应时间等于响应时间最慢的Redis节点的时间,并且多个key的读取操作,在ShardedJedis中需要进行归并取交集等操作。
2. 如果是写操作,有可能同时写多个Redis节点,会造成一部分成功一部分失败的情况,无法保证数据一致性的问题。
package redis.clients.jedis.util;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Sharded<R, S extends ShardInfo<R>> {
public static final int DEFAULT_WEIGHT = 1;
private TreeMap<Long, S> nodes;
private final Hashing algo;
private final Map<ShardInfo<R>, R> resources = new LinkedHashMap<ShardInfo<R>, R>();
private Pattern tagParttern = null;
public static final Pattern DEFAULT_KEY_TAG_PATTERN = Pattern.compile("\\{(.+?)\\}");
public Sharded(List<S> shards) {
this(shards, Hashing.MURMUR_HASH);
}
public Sharded(List<S> shards, Hashing algo) {
this.algo = algo;
initialize(shards);
}
public Sharded(List<S> shards, Pattern tagPattern) {
this(shards, Hashing.MURMUR_HASH, tagPattern);
}
public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {
this.algo = algo;
this.tagPattern = tagPattern;
initialize(shards);
}
private void initialize(List<S> shards) {
nodes = new TreeMap<Long, S>();
for (int i = 0;i != shards.size(); ++i) {
final S shardInfo = shards.get(i);
if (shardInfo.getName() == null) {
for (int n = 0;n < 160 * shardInfo.getWeight(); n ++) {
nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
}
} else {
nodes.put(this.algo.hash(shardInfo.getName() + "*" + n), shardInfo);
}
resource.put(shardInfo, shardInfo.createResource());
}
}
public R getShard(byte[] key) {
return resources.get(getShardInfo(key));
}
public R getShard(String key) {
return resource.get(getShardInfo(key));
}
public S getShardInfo(byte[] key) {
SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
if (tail.isEmpty()) {
return nodes.get(nodes.firstKey());
}
return tail.get(tail.firstKey());
}
public S getShardInfo(String key) {
return getShardInfo(SafeEncoder.encod(getKeyTag(key)));
}
public String getKeyTag(String key) {
if (tagPattern != null) {
Matcher m = tagPattern.matcher(key);
if (m.find())
return m.group(1);
}
return key;
}
public Collection<S> getAllShardInfo() {
return Collections.unmodifiableCollection(nodes.values());
}
public Collection<R> getAllShards() {
return Collections.unmodifiableCollection(resources.values());
}
}
DualPivotQuicksort:双轴快速排序,是Arrays给基本类型的数据排序使用的具体实现,它针对每种基本类型做了实现,实现的方式稍微有些差异,但思路是相同的。整个实现的思路是首先检查数组的长度,比一个阈值(286)小的时候直接使用双轴快速排序。其他情况,先检查数组中的数据的顺序连续性。把数组中连续升序或连续降序的信息记录下来,顺便把连续降序的部分倒置。这样数据就被分割成一段连续升序的数列。如果顺序连续性好,直接使用TimSort算法。TimSort算法的核心在于利用数列中的原始顺序,所以可以提高很多效率。顺序连续性不好的数组直接使用了 双轴快排 + 成对插入排序。成对插入排序是插入排序的改进版,它采用了同时插入两个元素的方式调高效率。双轴快排是从传统的单轴快排到3-way快排演化过来的。
CMS决定是否在FullGC时做压缩,满足下面任意条件之一就会在FullGC时做压缩:
1. UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction都是true,前者默认是true;
2. 用户调用了System.gc(),并且DisableExplicitGC为关闭;
3. YGC报告接下来要做增量收集会失败。即YGC预计Old区没有足够空间容纳下次YGC晋升的对象。
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次ygc,目的在于减少old gen对ygc gen的引用,降低remark时的开销--一般CMS的GC耗时 80%都在remark阶段
用-XX:+UseCMSInitiatingOccupancyOnly标志来命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期。而是,当该标志被开启时,JVM通过CMSInitiatingOccupancyFraction的值进行每一次CMS收集,而不仅仅是第一次。然而,大多数情况下,JVM比我们自己能作出更好的垃圾收集决策。因此,只有当理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。
java.lang.OutOfMemoryError: unable to create new native thread
这个异常问题本质原因是创建了太多的线程,而能创建的线程数是有限的,导致了异常的发生。能创建的线程数的具体计算公式如下:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / ThreadStackSize = Number of threads
MaxProcessMemory:一个进程的最大内存
JVMMemory: JVM内存
ReservedOsMemory: 保留的操作系统内存
ThreadStackSize: 线程栈的大小
由公式可得:JVM的内存越多,可创建的线程数越少。越容易发生OutOfMemoryError:unable to create new native thread.
JVM最大创建线程数量由JVM堆内存大小、线程的Stack内存大小、系统最大可创建线程数(Java线程的实现是基于底层系统的线程机制来实现的,Windows下_beginthreadex,Linux下pthread_create)三个方面影响
CMS:是一款并发的、使用标记-清除算法的GC,如果老年代使用CMS,需要添加参数: -XX:+UseConcMarkSweepGC
CMS的周期性Old GC,执行的逻辑叫Backgroud Collect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2秒)是否需要触发。
周期性Old GC的触发条件:
1. 如果没有设置UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(线上环境建议加上这个参数,不然会加大排查难度);
2. 老年代使用率达到阈值CMSInitiatingOccupancyFraction,默认92%
3. 永久代的使用率达到阈值CMSInitiatingPermOccupancyFraction,默认92%,前提是开启了CMSClassUnloadingEnable
4. 新生代的晋升担保失败。
晋升担保失败:老年代是否有足够的空间来容纳全部的新生代对象或历史平均晋升到老年代的对象,如果不够,就提早进行一次老年代的回收,防止下次进行YGC的时候发生晋升失败。
CMS的过程:
1. InitialMarking:初始标记,整个过程STW。
该阶段单线程执行,主要分为两步:1. 标记GC Roots可达的老年代对象;
2. 遍历新生代对象,标记可达的老年代对象;
2. Marking:并发标记
该阶段GC线程和应用线程并发执行,遍历InitialMarking阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。
因为该阶段并发执行,在运行期间可能发生新生代的对象晋升到老年代、或者直接在老年代分配对象、或者更新老年代对象的引用关系等等。对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。
为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描张哥老年代。
3.1 Precleaning 预清理
通过参数CMSPrecleaningEnabled可关闭该阶段,默认启用,主要做两件事情:
1. 处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B对象之前没有被标记),在这个阶段就会标记B为活跃对象。
2. 在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty(其实这里并非使用CardTable, 而是一个类似的数据结构ModUnionTable),通过扫描这些Table,重新标记那些在并发标记阶段引用被更新的对象(晋升到老年代的对象、原本就在老年代的对象)
3.2 AbortablePreclean:可中断的预清理
该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold,默认2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。
为什么需要这个阶段,存在的价值是什么?
因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽量去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。
在该阶段,主要循环做两件事:
1. 处理From和To区的对象,标记可达的老年代对象;
2. 和上一阶段一样,扫描处理Dirty Card中的对象;
这个阶段不会一直循环下去,打断这个循环的条件有三个:
1. 可以设置最多循环次数CMSMaxAbortablePrecleanLoops,默认0,表示没有循环次数限制;
2. 如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认5S,会退出循环
3. 如果新生代Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环(这个条件能够成立的前提是,在进行Precleaning时,Eden区的使用率小于十分之一)
如果在循环退出之前,发生了一次YGC,对于后面的Remark阶段来说,大大减轻了年轻代的负担,但是发生YGC并非人为控制,所以只能期望这5S可以发生一次YGC。
4. FinalMarking:并发重新标记,STW过程
该阶段并发执行,在之前的并行阶段,可能产生新的引用关系如下:
1. 老年代的新对象被GC Roots引用
2. 老年代的未标记对象被新生代对象引用
3. 老年代已标记对象增加新引用指向老年代其他对象
4. 新生代对象指向老年代引用被删除
5. 其他情况。
上述对象中可能有一些已经在Precleaning阶段或AbortablePreclean阶段被处理过,但总存在没来得及处理的,所以还需要进行如下处理:
1. 遍历新生代对象,重新标记;
2. 根据GC Roots, 重新标记;
3. 遍历老年代的Dirty Card,重新标记,这里的Dirty Card大部分已经在clean阶段处理过。
在第一步骤中,需要遍历新生代的全部对象,如果新生代的使用率很高,需要遍历处理的对象也很多,这对于这个阶段的总耗时来说,是个灾难(因为可能大量的对象暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数增加了很多),如果在AbortablePreclean阶段中能够恰好发生一次YGC,这样就可以避免扫描无效的对象。
如果在AbortablePreclean阶段没来得及执行一次YGC,怎么办?
CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收也就更彻底些。
不过这个参数有利有弊,利是降低了Remark阶段的停顿时间,弊端是新生代对象很少的情况下也多触发了一次YGC,最极端的情况是AbortablePreclean阶段已经发生了一次YGC,又白白触发了一次YGC。
5. Sweeping:并发清理
6. Resizing:调整堆大小
7. Resetting:重置
CMS的主动Old GC,触发条件如下:
1. YGC过程发生了Promotion Failed, 进而对老年代进行回收;
2. System.gc(),前提是-XX:+ExplicitGCInvokesConcurrent参数
如果触发了主动Old GC,这时周期Old GC正在执行,那么会剥夺周期性Old GC的执行权(同一个时刻只能有一种Old GC进行),并记录concurrent mode failure或者concurrent mode interrupted。
主动GC开始时,需要判断本次GC是否要对老年代的空间进行Compact(因为长时间的周期性GC会造成大量的碎片空间)
在三种情况下,会进行压缩:
1. 参数UseCMSCompactAtFullCollection(默认true)和CMSFullGCsBeforeCompaction(默认0),所以默认每次的主动GC都会对老年代的内存空间进行压缩,就是把对象移动到内存的最左边;
2. System.gc();
3. 如果新生代的晋升担保失败。
Redis集群方案:官方的Redis Cluster,社区的Twitter团队Twemproxy,豌豆荚的Codis.
三者的对比如下:
Codis Twemproxy Redis-Cluster
resharding without restarting cluster: YES NO YES
pipeline : YES YES NO
hash tag for multi-key operations : YES YES NO
multi-key operations while resharding: YES / NO
Redis clients supporting : Any clients Any clients clients have to support cluster pool
全局ID生成的需求:
1. 全局唯一性
2. 趋势递增
3. 单调递增
4. 信息安全
其中3和4需求是互斥的,无法同时满足。
全局ID的非功能需求:
1. 高可用
2. 高QPS
3. 平均延迟和TP999(保证99.9%的请求都能成功的最低延迟)延迟都要尽可能低
全局ID的方案:
1. UUID
2. 雪花算法:64位的二进制数。1位固定0 + 41位时间戳(精确到毫秒) + 10位机房/机器编码 + 12位递增序列。可使用zookeeper来生成唯一的workerId和dataCenterId,保证这10位的唯一性。
3. 数据库ID: 可以使用多个数据库保证高性能和高可用性,每个的auto-increment=2, offset=2,保证一个是奇数一个是偶数,或者多个数据库的情况。(经典的Flicker:用多组Mysql提供全局ID。利用了Mysql的自增ID和replace into语法)
4. Redis生成
5. MongoDB的ObjectId:有12个字节组成:4个字节的时间(秒为单位) + 3个字节的机器标识 + 2个字节的进程ID + 3个字节的计数器
6. 美团的Leaf: 从Snowflake进行演进。提供两种:Leaf-segment数据库方案和Leaf-snowflake方案。
7. 百度的UidGenerator: 从雪花算法Snowflake的基础上做了优化。对每个位数进行了重新规划,其中worker占28位,workerId由启动时数据库分配。由DefaultUidGenerator和CachedUidGenerator两种实现。高性能采用CacehedUidGenerator,使用RingBuffer缓存生成的ID。CachedUidGenerator采用双RingBuffer,Uid-RingBuffer用于存储Uid, Flag-RingBuffer用于存储Uid状态(是否可填充,是否可消费)。由于数组元素在内存中是连续分配的,可最大程度利用CPU cache以提升性能。但同时会带来"伪共享"FalseSharing问题,为此在Tail, Cursor指针,Flag-RingBuffer中采用了CacheLine补齐方式。
UUID的缺点: 不易存储,占36位的字符串;信息不安全,基于MAC地址生成的UUID,可能会造成MAC泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。对MySQL索引不利,如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能,可参考B+树的索引原理。
雪花算法的缺点:依赖机器时钟,如果服务器时钟回拨,会导致重复ID生成(虽然代码可以保证时间必须递增,但会一直阻塞到时间大于上次时间);在分布式环境中,每个服务器时钟不可能完全同步,有时会出现不是全局递增的情况。时钟回退后会造成时间被追回之前的这段时间服务不可用。
数据ID的缺点:依赖数据库单机的读写性能,如数据库繁忙,会导致生成阻塞/延迟高;依赖数据库,数据库异常时,完全不能工作。系统水平扩容困难;数据库压力大。
MongoDB的ObjectId的缺点:机器标识和进程ID需保证唯一性,不然大概率会有重复ObjectId
Redis生成ID的缺点:需要引入Redis,增加系统复杂度;
数据库ID:发生主从切换时的不一致可能导致重复ID。
Spring Boot堆外内存泄露,注意观察Swap区域是否使用异常。
-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=128m -XX:InitialCodeCacheSize=128m -Xss512k -Xmx4g -Xms4g -XX:+UseG1GC -XX:G1HeapRegionSize=4M
Java进程启动的时候,可以为JVM指定合适的内存大小,但是这些内存操作系统并没有真正的分配给JVM,而是等JVM访问这些内存的时候,才真正分配,这样会造成以下问题:
1. GC的时候,新生代的对象要晋升到老年代,需要内存,这个时候操作系统才真正分配内存,这样就会加大YGC的停顿时间;
2. 可能存在内存碎片的问题
可以在JVM启动的时候,配置-XX:+AlwaysPreTouch参数,这样JVM就会先访问所有分配给它的内存,让操作系统把内存真正的分配给JVM,后续JVM就可以顺畅访问内存了。
当然,启动了-XX:+AlwaysPreTouch,会导致启动时间加长。在JDK8中使用单线程进行处理很慢。到JDK9使用了并行PreTouch,时间大大缩短。
使用top快速查看java进程占用的内存大小
使用-XX:NativeMemoryTracking=detail启动(会造成5-10%的性能损耗)JVM,然后使用命令jcmd <pid> VM.native_memory detail 来查看JVM的内存分布。
因为jcmd命令显示的内存包括堆内内存,Code区域,通过unsafe.allocataMemory和DirectBuffer申请的内存,但是不包括Native Code(C代码)申请的堆外内存。
可以通过pmap -x <pid> | sort -k 3 -n -r查看进程的内存分布。
大概猜测到某个类/方法有问题后,可以使用Btrace/Arthas进行追踪代码。
jinfo -flag ReservedCodeCacheSize查看CodeCache的大小
promotion failed:在进行YGC时,Surivor Space放不下,对象只能进入老年代,而此时老年代也放不下(promotation failed时老年代CMS还没有机会进行回收,又放不下转移到老年代的对象,因此会出现下一个问题concurrent mode failure,需要stop-the-world降级为GC-Serial Old)
concurrent mode failed: 该问题是在执行CMS GC的过程中,同时应用线程将对象放入老年代,而此时老年代空间不足。或者在做YGC时,新生代Surivor空间放不下,需要放入老年代,而老年代放不下而产生的。
promotion failed – concurrent mode failure:
Minor GC后, Survivor空间容纳不了剩余对象,将要放入老年代,老年代有碎片或者不能容纳这些对象,就产生了concurrent mode failure, 然后进行stop-the-world的Serial Old收集器。
解决办法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 或者调大新生代或者Survivor空间
concurrent mode failure:
CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年代直接分配,例如大对象,但是老年代没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。
解决办法:+XX:CMSInitiatingOccupancyFraction,调大老年带的空间,+XX:CMSMaxAbortablePrecleanTime
总结一句话:使用标记整理清除碎片和提早进行CMS操作。
TCP粘包/拆包的原因:
1. 应用程序写入的数据大于socket缓冲区大小,这将会发生拆包;
2. 应用程序写入的数据小于socket缓存区大小,网卡将应用多次写入的数据发送到网络,这将会发生粘包;
3. 进行mss(Maximum Segment Size)大小的TCP分段,当TCP报文长度-TCP头部长度>mss的时候将发生拆包;
4. 接收方不及时读取socket缓冲区数据,这将发生粘包。
解决方法:TCP是无界的数据流,且协议本身无法避免粘包、拆包的发生,只能在应用层协议上进行控制。
1. 使用带消息头的协议,消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候,解析出消息长度,然后读取该长度的内容。
2. 设置定长消息,服务端每次读取定长的内容作为一条完整消息。
3. 设置消息边界,服务端从网络流中按消息编辑分离出消息内容。
使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑:
topic创建主要分为两个部分:命令行部分+后台(controller)逻辑部分,主要思想就是后台逻辑会监听zookeeper下对应的目录节点,一旦发起topic创建命令,该命令会创建新的数据节点从而触发后台的创建逻辑。
简单来说命令行主要做两件事情:1. 确定分区副本的分配方案(每个分区的副本都分配到哪些broker上);2. 创建zookeeper节点,把这个方案写入/brokers/topics/<topic>节点下。
kafka controller部分主要做下面事情:1. 创建分区;2. 创建副本;3. 为每个分区选举leader, ISR; 4. 更新各种缓存。
所谓的后台逻辑其实是由Kafka的controller负责提供的。Kafka的controller内部保存了很多信息,其中有一个分区状态机,用于记录topic各个分区的状态。这个状态机内部注册了一些zookeeper监听器。Controller在启动的时候会创建这些监听器。其中一个监听器(TopicChangeListener)就是用于监听zookeeper的/brokers/topics目录的子节点变化的。一旦该目录子节点数发生变化就会调用这个监听器的处理方法。对于上面的例子来说,由于命令行已将分配方案持久化到/brokers/topics/test下,所以会触发该监听器的处理方法。
TopicChangeListener监听器一方面会更新controller的缓存信息(比如更新集群当前所有的topic列表以及更新新增topic的分区副本分配方案缓存等),另一方面就是创建对应的分区及其副本对象并为每个分区确定leader副本及ISR。
至此,整个topic的创建就完成了!
Netty的零拷贝实现在如下三个方面:
1. Netty的接收和发送ByteBuffer采用Direct buffers, 使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(Heap buffers)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
2. Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
3. Netty的文件传输采用了transferTo方法,可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。
Monitor基本元素:
Monitor机制需要几个元素来配合,分别是:
1. 临界区
2. Monitor对象及锁
3. 条件变量以及定义在Monitor对象上的wait,signal操作
使用Monitor机制的目的主要是为了互斥进入临界区,为了做到能够阻塞无法进入临界区的进程/线程,还需要一个Monitor Object来协助,这个Monitor Object内部会有相应的数据结构,例如列表,来保存被阻塞的线程/进程。同时由于Monitor机制本质上基于Mutux这种基本原语的,所以MonitorObject还必须维护一个基于Mutux的锁。
此外,为了在适当的时候能够阻塞和唤醒进程/线程,还需要引入一个条件变量,这个条件变量用来决定什么时候是“适当的时候”,这个条件可以来自程序代码的逻辑,也可以是在MonitorObject的内部,总而言之,程序员对条件变量有很大的自主性。不过,由于MonitorObject内部采用了数据结构来保存被阻塞的队列,因此它也必须对外提供两个API来让线程进入阻塞状态以及之后的被唤醒,分别是wait和notify。
锁信息就存在前4个字节的MarkWord中,JVM对synchronized的加锁过程优化为:
1. 检测MarkWork里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁;
2. 如果不是,则使用CAS将当前线程的ID替换为MarkWord,如果成功则表示当前线程获得偏向锁,设置偏向标志位1;
3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁;
4. 当前线程使用CAS将对象头的MarkWord替换为锁记录指针,如果成功,当前线程获得锁;
5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;
6. 如果自旋成功,则依然处于轻量级状态;
7. 如果自旋失败,则升级为重量级锁。
Java源代码被编译成class字节码,最终需要加载到JVM中才能运行。整个生命周期包括:加载->验证->准备->解析->初始化->使用->卸载7个阶段。
1. 加载阶段:用合适的ClassLoader将class文件加载到JVM中;
2. 验证阶段:确保Class文件符合JVM要求,主要包括格式验证、元数据验证、字节码验证和符号引用验证;
3. 准备阶段:为类变量(static修饰)在方法区中分配内存并设置初始化。(比如:private static int var = 100,准备阶段完成后var为0,在初始化阶段才赋值100。但private static final int VAL=100, 在编译阶段会为VAL生成ConstantValue属性,在准备阶段会根据ConstantValue属性将VAL赋值为100)
4. 解析阶段:解析阶段是将常量池中的符号引用替换为直接引用的过程,符号引用和直接引用有什么不同?符号引用使用一组符号来描述所引用的目标,可以是任何形式的字面常量,定义在Class文件格式中。直接引用可以是直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄。
5. 初始化阶段:初始化阶段是执行类构造器<clinit>方法的过程,<clinit>方法由类变量的赋值动作和静态语句块按照在源文件出现的顺序合并而成,该合并操作由编译器完成。
1、<clinit>方法对于类或接口不是必须的,如果一个类中没有静态代码块,也没有静态变量的赋值操作,那么编译器不会生成<clinit>;
2、<clinit>方法与实例构造器不同,不需要显式的调用父类的<clinit>方法,虚拟机会保证父类的<clinit>优先执行;
3、为了防止多次执行<clinit>,虚拟机会确保<clinit>方法在多线程环境下被正确的加锁同步执行,如果有多个线程同时初始化一个类,那么只有一个线程能够执行<clinit>方法,其它线程进行阻塞等待,直到<clinit>执行完成。
4、注意:执行接口的<clinit>方法不需要先执行父接口的<clinit>,只有使用父接口中定义的变量时,才会执行。
虚拟机中严格规定有且只有5种情况必须对类进行初始化:
1. 执行new,getstatic, putstatic和invokestatic指令;
2. 使用reflect对类进行反射调用;
3. 初始化一个类的时候,父类还没有初始化,会事先初始化父类
4. 启动虚拟机时,需要初始化包含main方法的类;
5. 在JDK1.7中,如果java.lang.invoke.MethodHandler实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化;
以下几种情况,不会触发类的初始化:
1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2. 定义对象数组,不会触发该类的初始化。
3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
4. 通过类名获取Class对象,不会触发类的初始化。比如 Class claz = HashMap.class;但Class.forName("HashMap");会触发。
5. 通过Class.forName加载指定类时,如果指定参数initialize为false,也不会触发类初始化,其实这个参数告诉JVM,是否对类进行初始化。
6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
双亲委派的好处:比如位于rt.jar包中的类java.lang.Object,无论哪个加载器加载这个类,最终都委托给顶层的Bootstrap ClassLoader进行加载,确保Object类在各个ClassLoader中都是同一个类。相反如果没有双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了java.lang.Object,并放在了ClassPath中,那么系统中将会出现多个不同的Object类,整个java提现中的最基础的行为也就无法保证。
有没有什么场景是打破双亲委派模型:
1. JDK1.2之前还没有引入双亲委派模型,为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK1.2之后已经不再提倡用户再去覆盖loadClass()方法,而是应当把自己的类加载逻辑写到findClass()方法完成加载,这样就可以保证新写出来的类加载器是符号双亲委派规则的。
2. JNDI服务的代码由启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现并部署在应用程序的ClassPathc下的JNDI接口提供者的代码,但启动类加载器不可能认识这些代码。为了解决这个问题,引入了一个不太优雅的设计:线程上下文加载器(Thread Context ClassLoader).这个类加载器可以通过Thread类的setContextClassLoader()进行设置。如果创建线程时还没设置,它将从父线程继承,如果在全局范围内都没有设置的话,那这个类加载器就是这个应用程序类的加载器。JNDI服务使用这个ContextClassLoader去加载所需要的SPI代码,这就是父类加载器请求子类加载器去完成类加载动作,这个行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上违背了双亲委派,但这也是无可奈何。Java中所有涉及SPI的加载动作基本都是采用这种方式,比如JNDI,JDBC,JCE,JAXB和JBI等。
3. 业界事实上的Java模块化标准的OSGi,它实现模块化热部署的关键就是它自定义的类加载机制的实现。在OSGi环境下,类加载不再是双亲委派的树状结构,而是更加复杂的网状结构。
Spring中循环依赖注入分为三种情况:
1. 构造器循环依赖;
2. Setter方法注入-单例模式(scope=singleton)
3. Setter方法注入-非单例模式
1. 通过构造器注入导致的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。
Spring容器将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,因此如果在创建Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlyInCreationException异常表示循环依赖;而对于创建完毕的Bean将从“当前创建Bean池”中清除掉。
2. 对于Setter注入导致的循环依赖是通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(如Setter注入)的Bean来完成的,而且只能解决单例作用域的Bean循环依赖。
3. 对于Setter注入非单例Setter循环依赖,对于"prototype"作用域Bean,Spring容器无法完成依赖注入,因为prototype作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean。
Spring通过三级缓存解决了循环依赖,三级缓存如下:
Map<String, Object> singletonObjects;
Map<String, ObjectFactory<?>> singletonFactories;
Map<String, Object> earlySingletonObjects;
对Kafka的Log Compaction的理解:Kafka中的Log Compaction是指在默认的日志删除(Log Deletion)规则之外提供的一种清理过时数据的方式。如下图所示,Log Compaction对于有相同key的的不同value值,只保留最后一个版本。如果应用只关心key对应的最新value值,可以开启Kafka的日志清理功能,Kafka会定期将相同key的消息进行合并,只保留最新的value值。
试想一下,如果一个系统使用Kafka来保存状态,每次有状态变更都会将其写入Kafka中。在某一时刻此系统异常崩溃,进而在恢复时通过读取Kafka中的消息来恢复其应有的状态,那么此系统关心的是它原本的最新状态而不是历史时刻中的每一个状态。如果Kafka的日志保存策略是日志删除(Log Deletion),那么系统势必要一股脑的读取Kafka中的所有数据来恢复,而如果日志保存策略是Log Compaction,那么可以减少数据的加载量进而加快系统的恢复速度。Log Compaction在某些应用场景下可以简化技术栈,提高系统整体的质量。
Kafka中用于保存消费者消费位移的主题“__consumer_offsets”使用的就是Log Compaction策略。
当topic的cleanup.policy(默认delete)设置为compact时,kafka的后台线程会定时把topic遍历两次,第一次把每个key的哈希值最后一次出现的offset都保存下来,第二次检查每个offset对应的key是否在更后面的日志中出现过,如果出现了就删除对应日志。Log Compaction的大部分功能由CleanerThread完成,核心逻辑在Cleaner的clean方法。