-
Notifications
You must be signed in to change notification settings - Fork 10
/
jvm.txt
676 lines (390 loc) · 33.6 KB
/
jvm.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
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
翻译过程 // .class --> 机器码
解释执行 逐条翻译 边翻译,边执行 逐条将字节码翻译成机器码,并执行 优势在于 -> 无需等待编译
即时编译 提前翻译 先翻译,再执行 将一个方法中包含的所有字节码编译成机器码后,再执行 优势在于 -> 实际运行速度更快
运行时内存
方法区、堆、PC寄存器、Java方法栈、本地方法栈
类加载机制(双亲委派模型)
每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
Java虚拟机将 字节流 转化为 Java类 的过程
加载, 是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。
链接, 是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。链接还分验证、准备和解析三个阶段。其中,解析阶段为非必须的。
初始化,则是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。
主流垃圾回收算法
引用计数法(reference counting) // 淘汰 - 古老的辨别方法
为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。
缺:需要额外的空间来存储计数器,以及繁琐的更新操作, 重大漏洞,无法处理循环引用对象。 // a 与 b 相互引用,除此之外没有其他引用指向 a 或者 b.
可达性分析算法 // 主流
垃圾回收
- 核心工作就是回收垃圾
那关键点来了,什么是垃圾?这个垃圾需要分类嘛?怎么定位垃圾?怎么回收垃圾?回收垃圾的方法都有哪些?他们都有什么优缺点?另外,就是我们为什么要学习垃圾回收?
站在JVM的视角来看:
垃圾 - 就是无用对象所占用的堆内存空间
垃圾分类 - 貌似不需要垃圾分类,识别垃圾并回收就行
定位垃圾 - 是垃圾回收的关键点,无用的对象占用的堆空间即是垃圾,那就需要先定位无用的对象,这里的无用是不再使用的意思,咋判断呢?文中介绍了两种方法,计数法和标记法(祥看原文)核心在于能定位出无用的对象,后出现的方法往往比早出现的更好一点,这里也一样,标记法能解决计数法,解决不了的循环引用不能回收的问题,但是也存在其他的问题,误报和漏报的问题,误报浪费点垃圾回收的机会浪费点空间,漏报在多线程并发工作时可能会死JVM的,所以,比较严重,所以,JVM采用了简单粗暴的stop-the-world的方式来对待,所以,老年代的回收有卡顿的现象
怎么回收垃圾 - 定位出垃圾,回收就是一个简单的事情了,当然也非常关键,把要回收的堆内存空间标记为可继续使用就行,下次有新对象能在此空间创建就行
回收垃圾的方法 - 文中介绍了三种,清除、压缩、复制
清除法 - 简单,但易产生碎片,可能总空间够但分配不了的问题
压缩法 - 能解决清除法的问题,但是复杂且耗性能
复制法 - 折衷一些,但是空间利用率低,总之,各有千秋
为什么要学 - 这个最容易,因为面试需要、装逼需要、升职加薪需要、人类天生好奇、还有免于被鄙视及可以鄙视其他人
小结:
二八法则 - 适用于许多的领域,对象 存活于 JVM内存空间的生命周期 也同样符合
为了更好的JVM性能 以及充分利用对象生命周期的二八法则,JVM作者 将 JVM的内存空间 进行了分代的处理
堆内存空间 = 年轻代 + 老年代
年轻代 = Eden + Survivor(from + to)
年轻代用于分配新生的对象
Eden - 通常用于存储新创建的对象,对内存空间是共享的,所以,直接在这里面划分空间需要进行同步(加锁)
from - 当Eden区的空间耗尽时,JVM便会出发一次Minor GC 来收集新生代的垃圾,会把存活下来的对象放入Survivor区,也就是from区
注意,from和to的指针会每次对调
to - 指向的Survivor区是空的,用于当发生Minor GC 时,Eden和from区中的存活对象 会被复制到 to 指向的 Survivor 区中,然后再交换 from 和 to 指针,以保证下一次 Minor GC 时,to 指向的 Survivor 区还是空的。
老年代 - 用于存储 存活时间更久 的对象,比如:15次Minor GC 还存活的对象,就放入老年代中.
堆内存分代后,会根据他们的不同特点来区别对待,进行垃圾回收的时候会使用不同的垃圾回收方式
针对 新生代 的垃圾回收器有三个:Serial、Parallel Scavenge、Parallel New,他们采用的都是 标记 - 复制 的垃圾回收算法。
针对 老年代 的垃圾回收器有三个:Serial Old 、Parallel Old 、CMS,
Serial Old 、Parallel Old 使用的都是 标记 - 压缩 的垃圾回收算法。
CMS 采用的是 标记 - 清除 算法,并且是并发的
TLAB(Thread Local Allocation Buffer)- 这个技术是用于解决多线程竞争堆内存分配问题的,核心原理是对分配一些连续的内存空间
卡表 - 这个技术是用于解决减少老年代的全堆空间扫描
synchronized 的实现: 重量级锁 -> 轻量级锁 -> 偏向锁
重量级锁:激烈竞争 --> 同一时间,同时 抢夺锁
针对的是 多个线程 同时竞争 同一把锁的情况
重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。
Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况。
轻量级锁:低竞争 --> 不同时间 申请锁
针对的是 多个线程 在不同时间段 申请同一把锁的情况
轻量级锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。
它针对的是多个线程在不同时间段申请同一把锁的情况。
偏向锁:无竞争 --> 只有一个线程 申请锁
针对的是 锁仅会被 同一线程持有 的情况
偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。
在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
方法内联
方法内联 - 是编译器的一种代码优化手段。
会根据不同代码调用方式有不同的优化方式,目的都是为了提高JVM的效率。
根本方式,我认为就是采用取巧的方式,提前判断出来可以少做一些事情,然后先提前做一些准备,整体的时间和空间成本会降下来。
新建的对象一定会进入新生代吗?
gc算法有哪些?(有哪些垃圾回收机制)
class文件只会加载一次吗?如果实现class加载多次
tomcat实现热部署的原理
Object a = new Object()内存中是怎样分配的?
int a = 1 内存是如何分配的?
java内存模型8大原子操作
怎么判断对象是否被回收?
java中的引用类型
jvm中的垃圾回收算法有哪些?
新生代垃圾回收器和老生代垃圾回收器有什么区别?
分代垃圾回收器的工作机制?
垃圾回收算法:
标记清除:1阶段标记GcRoot不可达对象,二阶段清除淘汰数据,会存在内存不连续。
复制算法:将现有空间分为两块,每次使用其中一块,垃圾回收时,将存活对象复制到未使用的内存块中
标记整理:在标记清除的基础中优化碎片空间,整理内存不连续空间。
垃圾回收器原理
cms
触发条件
a.周期性触发,根据是否达到阈值进行判断是否要进行Gc
2.主动触发,老年代的空间不够年轻代对象
处理过程
1.初始化标记(stop the world)
a.标记GcRoot可达的老年代对象
b.遍历新生代标记可达的老年代对象
2.并发标记(老年代对象变化还是较少的)
a.遍历初始化标记的存活对象,继续递归标记这些对象可达的对象
b.对于此阶段在新生代晋升、老年代分配的对象、更新引用等对象记录下来
3.重新标记(stop the world)
为何要有重新标记
a.老年代的对象重新被GcRoots引用
b.老年代的未标记对象被新生代对象引用
c.引用被删除等一系列情况
可优化方式,重新标记前执行一次YoungGC,可减少新生代对象的重新标记
(缺点:如果新生代对象本来就很少,需要多一次YoungGc)
重新标记对象
a.遍历新生代对象,重新标记
b.根据GcRoot,重新标记
c.遍历老年代的dirtyCrard重新标记
压缩
1.使用参数,设置多少次Gc后进行压缩
2.主动进行了System.gc()
3.如果新生代的晋升担保失败
concurrent mode failure
并发模式失败,jvm会自动转变为Serial Old进行垃圾回收,会增大停顿时间
可能原因
1.新生代提升速度过快,老年代收集速度赶不上新生代
2.老年代碎片化严重,无法容纳新生代提升上来的大对象
解决办法
(1)如果频率太快的话,说明空间不足,首先可以尝试调大新生代空间和晋升阈值。
(2)如果内存有限,可以设置 CMS 垃圾收集在老年代占比达到多少时启动来减少问题发生频率(默认68 XX:CMSInitiatingOccupancyFraction )
3)如果频率太快或者 Full GC 后空间释放不多的话,说明空间不足,首先可以尝试调大老年代空间
4)如果内存不足,可以设置进行 n 次 CMS 后进行一次压缩式(UseCMSCompactAtFullCollection,CMSFullGCBeforeCompaction)
去永久代采用元数据?
(1)字符串存在永久代中,容易出现性能问题和内存溢出。
(2)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
(3)元数据采用本地内存可避免溢出问题
如果判断对象可gc回收
1.引用计数法,无法解决环的问题,造成内存泄漏
2.rootGc可达性(对象作为起始节点,利用图论,可达对象便是存活对象,而不可达对象则是需要回收的垃圾内存)
root节点:a.全局性引用(静态变量或常量),b.局部变量表中的引用对象
内存溢出
a.堆内存溢出(年轻代,老年代溢出 -Xms -Xmx -Xmn -NewRatio -SurvivorRatio)
b.栈溢出(栈太深 -Xss)
c.元数据溢出(原永久代)
d.直接内存溢出(nio操作直接内存时可能会溢出)
GC正常执行标准
MinorGC 执行时间不到50ms;
Minor GC 执行不频繁,约10秒一次;
Full GC 执行时间不到1s;
Full GC 执行频率不算频繁,不低于10分钟1次。
------------------------------------------------------------------------------------------------------------------------
面向切面 -> 思想
实现:
1、静态代理 -> 在编译期织入代码 增加了编译的时间(启动时间变长) -> 运行期 性能无损
2、动态代理 -> 不会修改生成的 Class字节码 而是在 运行期 生成一个代理对象 -> 这个代理对象 对源对象 做了字节码增强
在运行期 生成代理对象 -> 性能比静态代理要差
应用:
校验、限流、全局日志
静态代理 -> AspectJ // 静态代理 -> 尽量减少对于原先接口性能的影响
Spring AOP
动态代理 -> JDK动态代理 / javassist(基于接口) 、 Cglib(基于类继承)
------------------------------------------------------------------------------------------------------------------------
字节码 修改工具
ASM:Java 字节码框架
生成新的 class 文件,修改已有的 class 文件.
辅助类:ASMifier
应用:
通过字 节码注入 实现的程序行为 监控工具.
Java 8 中 Lambda 表达式的适配器类,也是借助 ASM 来 动态生成 的.
// java -cp .../rt.jar xx.xx.ASMifier Demo.class | tee DemoDump.java
实践: // ASM --> cglib --> Spring AOP 动态代理
cglib 就是 基于 ASM 封装的 字节码工具类库
Spring 就是使用 cglib 代理库
代理的三个阶段:
编译器(javac) -> 字节码加载前 -> 字节码加载后
静态代理:
AspectJ 作用在 编译期(javac)
动态代理: 作用在 运行期
1. JDK 动态代理: // 核心:反射 ==> 基于接口(匿名实现类)代理 -> 支持final
通过反射 实现被代理类的接口
JDK动态代理只需要 JDK环境 就 可以进行代理,流程为:
1、实现InvocationHandler
2、使用Proxy.newProxyInstance产生代理对象
3、被代理的对象必须实现接口
实现原理:
target --> 接口 --> 接口代理类 实例
通过 被代理对象实例target ---> 拿到 target的 (所有) 接口 ---> 通过反射 生成target的接口 的 代理对象proxy
通过实现InvocationHandler,重写其invoke函数,来进行对 target类的方法增强,并将 该InvocationHandler 注册到 proxy
调用 proxy的方法时,就会调用 InvocationHandler中重写过的invoke函数
2. CGLIB 动态代理: // 核心:.class字节码修改 ==> 基于类(继承)代理 -> 不支持final
通过 修改被代理类的字节码
cglib 基于 ASM字节码工具 封装,Spring AOP 对类(无接口)的代理 就是使用 cglib 代理库
实现:
ASM 字节码工具 --> cglib 动态代理 --> Spring AOP 动态代理
通过 ASM 字节码工具 提供的 .class 字节码修改能力,进行进一步的封装 ---> cglib 得到 ASM的 .class字节码编辑能力
cglib 通过 ASM工具,达到 运行时 动态的修改被代理类的 .class字节码,从而进行 代理增强。
target --> 生成 target的 子类 代理实例proxy --> ASM 修改 proxy的method 的 .class字节码 --> 方法覆盖增强
两种代理方式的本质:
1、JDK 动态代理 是针对实现了接口的类生成代理,不是针对类
2、CGLIB 使用的是 为被代理类 生成一个 子类,通过 继承 的方式,覆盖并增强 其方法,
但是因为是继承 -> 所以 被代理类 和 增强方法 不能为 final ==> 无法被继承、重写 -> 就无法实现CGLIB代理
Spring 的 AOP 实现:
根据 目标类target 有没有实现interface,来选择 动态代理方式:
1、有接口 --> JDK 动态代理
2、无接口 --> CGLIB 动态代理
比较:
JDK 动态代理 ==> 支持final -> 接口类 的 匿名实现类(非target类 / 非target方法) ==> 可 '重写' final(类/方法)
CGLIB 动态代理 ==> 不支持 final -> 子类 无法继承/重写 final
----总结
Java字节码(基础篇)
1、 .java代码 由 Java的语言语法 组成,由开发人员来编写。
2、 .class 代码 由 Java编译器 来编译,Java编译器 也是由 对应的开发人员 来编写的。
.class 代码 由 字节码指令 来组成,如果 人理解 Java字节码指令集 比较容易的话,也可以直接编写 .class代码。
3、 .java 对应的 机器码 由 JVM 编译出来,原料是 .class代码,如果 人类理解 机器码 比较容易,那么可能变成就 直接在 机器硬件上 编写机器码 了。
4、 高级语言的出现,是为了提高人编写代码的效率。
我们学习 .class字节码指令集、JVM机器码 等的知识,是为了使我们 编写的高级语言代码,能更高效的 在机器硬件上执行。
从 高级语言的代码 到 能在机器上运行的机器码,中间经过了好几层的转换,
所以,了解了 每一层的转换,就能更快的定位出 高级语言代码的 性能瓶颈,
感觉是为了在 人的编码效率 和 机器的执行效率 之间找平衡点。
5、 JVM基于栈的计算模型的原因,推测可能是为了更简单的实现和更高的性能,但是是怎么做到的呢?
基于栈的计算模型,确实是为了实现起来容易一些,但它并不高效,因为没有使用 底层体系架构的寄存器。
在JVM中,只有解释器完整地模拟出该计算模型。
即时编译器 在解析字节码时,会使用一个虚拟的栈计算模型,但是在接下来的 编译优化,以及 生成的机器码 就不用了。
6、 .java --> .class(字节码) --> 机器码(0、1)
.java 代码: // 开发人员 用人类方便理解的 Java高级语言语法 编写而成
public static int bar(int i) {
return ((i + 1) - 2) * 3 / 4;
}
.class 字节码: // Java编译器(javac) 编译 人类写的 .java代码 --> 人类勉强读的懂得 .class代码
javac xx.java --> xx.class
// 对应的字节码如下:
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: iconst_1
2: iadd
3: iconst_2
4: isub
5: iconst_3
6: imul
7: iconst_4
8: idiv
9: ireturn
机器码: // JVM 编译 .class字节码文件 --> 硬件能理解的 机器码(0、1)
.class --> 机器码(0、1)
010101010011
intrinsic:
1.intrinsic - 可认为也是一种hotspot虚拟机,为提高JVM性能的优化机制或技巧
2.使用注解的方式来和Java代码结合 // @HotSpotIntrinsicCandidate
3.本质上适配出对应系统体系架构,然后直接使用和系统体系架构强关联的高效指令,来执行对应的功能
本质上可以理解为,根据不同的平台架构,识别,走提前定制版本的高效代码
4.针对不同的类,具体的高效指令亦不同
疑问❓
1、intrinsic 是只有hotspot虚拟机支持吗?
2、系统的体系架构适配是唯一的吗?主要是x86_64?按照这个思路是不是可以有多个类似的注视,针对多种的系统体系架构来优化呢?毕竟计算机系统的体系架构是有限的
逃逸分析:
逃逸分析的 主要优化点:同步消除、栈上分配、标量替换。其中同步消除比较少,栈上分配在HotSpot中暂未实现,主要是 标量替换。
逃逸分析的 缺点:分析过程比较耗费性能,或者分析完毕后发现非逃逸的对象很少。
逃逸程度:不逃逸,方法逃逸,线程逃逸;其中栈上分配不支持线程逃逸,标量替换不支持方法逃逸。
部分逃逸分析:if-then 条件分支 判断新建对象真正逃逸的分支,并且支持将新建操作推延至逃逸分支。
循环优化:
站在编译器的角度来作出的优化动作,这里介绍了几种方式。
我感觉万变不离其宗,优化的核心关键点还是少做一些事情。当然,事情少做了,作用不能减!
1. 循环无关码外提 —— 将循环内的某些无关代码外移,减少某些程序的反复执行
2. 循环展开 —— 减少循环条件的判断,针对循环次数少的循环
3. 循环判断外提 —— 减少每次循环的都进行判断次数
4. 循环剥离 —— 将不通用的、处理起来稍微费劲一些的动作,放在循环外处理
总之,要做减法!!!
性能优化的核心点:
1、让做的快的做
2、如果不能实现,则让做的快的多做一点,做的慢的少做一些
3、取巧,事情少做了,但是目的依旧能够达到
在程序语义不改变的情况下,编译器会尽可能地减少生成代码的工作量。
向量化优化:
1. 向量化优化 - 本质是一次性多干一些活,免得来回折腾费时费力,通过减少来回折腾的工作量来提高性能。
他是怎么实现的呢?
他是借助CPU的 SIMD指令,通过单条指令控制多组数据的运算,实现了CPU指令级别的并行。
2. 这么好为什么不大批量的使用哪?他有几种方式呢?
使用向量化优化是有一些前提条件的,目前HotSpot 虚拟机运用向量化优化的方式有两种。
第一种,使用HotSpot intrinsic,在调用特定的方法的时候替换为使用了SIMD指令的高效实现。
第二种,是依赖即时编译器进行的自动向量化,自动向量化也有苛刻的使用前提条件。
注解处理器:
1. 注解处理器 - 本质也是代码,以插件的形式存在,以插件的形式接入Java编译器,这些插件有什么用呢?
2. 注解处理器 主要有三个作用:
1、定义编译规则,并检查被编译的源文件。 // 可以为Java编译器添加一些编译规则,这也就是传说中的自定义注解。它可以定义一些编译规则,这些编译规则会以插件的形式提供给Java编译器。
2、修改已有源代码 // 可以修改已有的JAVA源文件(不推荐,为什么呢?因为本质上注解处理器不能修改已有的JAVA源代码,但是它可以修改有java源代码生成的抽象语法树,从而使生成的字节码发生变化,不过对抽象语法树的修改修改设计了java编译器的内部API,这部分很可能随着版本的变更而失效,所以,才不推荐使用的,存在埋深坑的隐患。)
3、生成新的源代码 // 可以生成一些新的.java源文件 e.g.:jcstress工具、JMH工具生成测试代码
3. 元注解 - 给注解使用的注解就是元注解,这些注解是JDK的开发人员提前定义了的,也同样是以插件的形式接入Java编译器的。
注意:所有的注解处理器都必须实现Processor接口,这个接口中有四个方法,每个方法都有其特殊的作用在,详情需要回头细看。
另外,JDK提供了一个实现Processor接口的抽象类AbstractProcessor,这个抽象类实现了Processor接口的其中三个方法。
4. 自定义的注解被编译为.class文件后,便可以将其注册为Java编译器的插件了,注册方法有两种,祥看专栏内容吧!
5. Java源代码的编译过程 分为三个步骤:
1、解析源文件生成抽象语法树
2、调用已注册的注解处理器(注解处理器有两种注册到Java编译器的方式)
3、生成字节码
4、如果第2步中,注解处理器生成了新的源代码,那么Java编译器将重复第1、2步,直到不再生成新的源代码。
6. 注解相当于给某些代码贴了个标签。
我们既可以通过 注解处理器 在编译时 解析这些标签,也可以在 运行时 通过反射解析这些标签。
解析后都会有一系列动作,这些动作就是对标签语义的诠释。
7. 编译时生成 与 运行时使用cglib等类库生成 的字节码,在性能和使用场景上有什么区别吗?
Cglib等字节码工具会影响启动性能,峰值性能上没啥区别。
如果对字节码不熟的话,用注解处理器比较容易些。另一方面,字节码处理工具更强大些,能做很多源代码不能做的。
JVM基准测试框架 JMH(OpenJDK 子项目):
JNI 的运行机制:
1. Java 中的 native 方法的链接方式主要有两种:
1、自动链接
按照 JNI 的默认规范命名所要链接的 C 函数,并依赖于 Java 虚拟机自动链接。
2、主动链接
另一种则是在 C 代码中主动链接。
2. JNI 提供了一系列 API 来允许 C 代码使用 Java 语言特性。
1、映射了 Java ⇋ C 的 基本数据类型 和 引用数据类型 int -> jint ...
2、异常处理
3. 防止 C代码 中 引用到的 Java对象被 JVM GC
在C代码中,可以访问传入的引用类型参数,也可以 通过 JNI API 创建新的 Java 对象。
Java 虚拟机需要一种机制,来告知垃圾回收算法,不要回收这些 C 代码中可能引用到的 Java 对象。
- JNI 的 局部引用(Local Reference)和 全局引用(Global Reference)
这两者都可以 阻止垃圾回收器 回收 被引用的 Java对象。
不同的是,局部引用 在 native 方法调用返回之后便会失效。传入参数 以及大部分 JNI API 函数的返回值 都属于 局部引用。
局部引用 ⇋ 全局引用 // 通过 JNI函数 互转 NewGlobalRef / DeleteGlobalRef
方法返回时,局部引用失效。可转化为全局引用,保持引用。
4. JNI 的额外性能开销
1、进入 C 函数时,对引用类型参数的句柄化,和调整参数位置(C 调用和 Java 调用传参的方式不一样)
2、从 C 函数返回时,清理线程私有句柄块
Java Agent与字节码注入:
1. Java agent 是啥玩意?
我的理解是Java语言的一个特性,这个特性 能够实现 Java字节码的注入。
2. Java字节码的注入 有什么用处?
在平时编程几乎没有使用到这方面的功能,应该是在一些框架的设计的时候才使用吧!比如:面相切面编程。
3. Java agent 本质上是通过 c agent 来实现的,那 c agent 本质上是怎么实现的?
C agent是一个 事件驱动 的工具实现接口,通常我们会在 C agent 加载后的入口方案 Agent_OnLoad处 注册各个事件的钩子方法。当Java虚拟机触发了这些事件时,便会调用对应的钩子方法。
4. 写代码实现某些功能,我的理解有三个时间段:
1、源码阶段, 最常用的,也是编程的主要活动时间
2、字节码阶段, 有些功能可能会在加载字节码时修改或者添加某些字节码,某些框架做的事情
3、运行阶段, 某些工具,在程序运行时修改代码,实现运行时功能分支的控制
5. 大概说一下我自己的理解:
1. Agent就是一个调用 JVMTI函数 的一个程序。
2. JVMTI提供的函数 能够获得JVM的运行信息,还可以修改JVM的运行态。
3. JVMTI能够修改JVM运行态,是因为JVM已经在运行流程中埋下了 钩子函数,JVMTI中的函数 可以传递具体逻辑 给 钩子函数。
4. JVMTI函数是C语言实现的JNI方法。
5. 通过Instrumentation,我们可以用Java语言调用大部分JVMTI函数。
6. JVM在启动时会加载 Agent 入口函数Agent_OnLoad,我们可以在此函数中注册Agent。
7. JVM在运行中可以通过 Agent_OnAttach函数 来加载Agent,我们可以 在此函数中 注册Agent。
8. B虚拟机 调用attach方法 attach到A虚拟机后,可以将 Agent程序作为参数 调用 A虚拟机的Agent_OnAttach函数。
9. premain 方法中的程序逻辑 会被注册到 Agent_OnLoad函数中。
10. agentmain 方法中的程序逻辑 会被注册到 Agent_OnAttach函数中。
11. 在 premain 或 agentmain 方法中的拿到的Instrumentation引用,可以理解成 拿到了JVMTI的引用(大部分函数)。
Graal编译器: // Graal:用Java编译Java
1. Graal是一个编译器,是使用 java语言编写 的 即时编译器。
既然是编译器就拥有编译器的各种特点(主要负责接收Java字节码,并且生成可以直接运行的二进制码) // .class --> 机器码
当然,后来者通常比先来的会多一些特点,否则也没有必要来啦!
Graal性能相对来说更好一点,更具模块化、更易维护(相对C2而言)。
Graal编译器是一个即时编译器,从JDK9就被集成到JDK中了。
当然,可能还不成熟时作为一个实验性质的编译器集成到JDK中的,可以有选择性的启动或者关闭。
2. Graal编译器 是 GraalVM 的基石,编译器是VM的一部分,相对来说比较独立
它和JVM的交互主要有如下三部分:
1、响应变异请求
2、获取编译所需的元数据(如:类、方法、字段)和反应程序执行状态的profile
3、将生成的二进制码部署至代码缓存里
3. Graal和JVM 通过 JVMCI 来实现 解耦,本质是通过 java语言层面的接口 来实现 解耦的。
虚拟机 的 功能模块:
解释执行器 组件 实现词法分析、语法分析、针对语法分析所生成的抽象语法树...
即时编译器 组件
垃圾回收器 组件
...
jvm(编译)优化:
方法编译优化:
方法内联
intrinsic HotSpot,根据平台架构不同,走定制化的 method 高效实现( = 方法重写)
逃逸分析 锁消除、栈上分配、标量替换
即时编译器 的 代码编译优化:
字段访问优化 缓存 // 字段缓存、存储优化
死代码消除 死存储消除、不可达分支
循环优化 循环无关代码外提、循环展开(空间换时间)、循环判断外提、循环剥离
向量化优化
-------
jvm
内存管理 + 内存(垃圾)回收 + 编译优化
别动不动一说jvm,就只知道背一堆 垃圾回收 的狗屁概念
------
JDK 中用于 监控及诊断 的 命令行工具:
jps 将打印所有正在运行的 Java 进程。
jstat 允许用户查看目标 Java 进程的类加载、即时编译以及垃圾回收相关的信息。它常用于检测垃圾回收问题以及内存泄漏问题。
jmap 允许用户统计目标 Java 进程的堆中存放的 Java 对象,并将它们导出成二进制文件。
jinfo 将打印目标 Java 进程的配置参数,并能够改动其中 manageabe 的参数。
jstack 将打印目标 Java 进程中各个线程的栈轨迹、线程状态、锁状况等信息。它还将自动检测死锁。
jcmd 则是一把瑞士军刀,可以用来实现前面除了jstat之外所有命令的功能。
这些命令的底层实现: // 对应的信息都是怎么获取到的?都是从哪里获取到的?
很多是通过 MXBeans 的
然后JVM有个专门存放 perf data 的,JVM组件会将东西存在那,而jstat会从那里读取。
实现起来不复杂的,可以参考一下工具的源代码
https://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/jdk.jcmd/share/classes/sun/tools
两个 GUI 工具:eclipse MAT 以及 JMC
eclipse MAT 可用于分析由jmap命令导出的 Java 堆快照。
它包括两个相对比较重要的视图,分别为直方图和支配树。直方图展示了各个类的实例数目以及这些实例的 Shallow heap 或 Retained heap 的总和。
支配树则展示了快照中每个对象所直接支配的对象。
Java Mission Control 是 Java 虚拟机平台上的性能监控工具。
Java Flight Recorder 是 JMC 的其中一个组件,能够以极低的性能开销收集 Java 虚拟机的性能数据。
JFR 的启用方式有三种,分别为在命令行中使用-XX:StartFlightRecording=参数,使用jcmd的JFR.*子命令,以及 JMC 的 JFR 插件。JMC 能够加载 JFR 的输出结果,并且生成各种信息丰富的图表。
----------------------------------------------------
CPU 100%
1、top -c + P(大写) // 打印 进程 cpu排行榜
2、top -Hp pid + P(大写) // 打印 进程内 线程 cpu排行榜
3、printf "%x\n" pid // 将 线程PID 转化为 16进制 2a34 -> 0x2a34
4、jstack 0x2a34 // 打印 线程堆栈
jstack pid(进程ID) | grep tid(线程ID 16进制) -A 30