liuhao163.github.io

杂七杂八


  • Home

  • Categories

  • Tags

  • Archives

  • Sitemap

Spring源码-带着问题学习

Posted on 2019-12-01 | Edited on 2022-09-21 | In java , spring , 源码

spring可以说是java有史以来最为庞大,应用最为广泛的框架,在看源码之前应该先立足于某一点在逐步发散到面。本文主要是整理观看spring源码前/中遇到的问题,以便于读代码更有针对性

spring的IOC和AOP

问题:

  1. Spring容器中的Bean的生命周期是什么?
  2. 对应的PostProcessor什么时候执行?有什么作用
  3. BeanDefinition的架构以及spring的设计思想
  4. BeanFactory和ApplicationContext的关系是什么?他们的作用是什么?
  5. ApplicationContext的构造函数都做了什么?
    1. scan/register做了什么事?
    2. refresh做了什么事?
    3. refresh的详细的流程,对照着bean的生命周期看
  6. AnnotationConfigUtils.registerAnnotationConfigProcssors做什么用的?
  7. Spring的自动装配策略是什么(byType–>Primary–>Propity–>ByName)具体见临时链接
  8. ClassPathBeanDefinitionScanner、AnnotatedBeanDefinitionReader的作用 Spring源码-类扫描器-ClassPathBeanDefinitionScanner
  9. 为什么@Component、@Repository、@Controller、@Service以及JavaEE6的@ManagedBean和@Named注解这些注解能注册到容器中Spring源码-实现自定义注解
  10. Spring如何实现自定义注解,它是怎么实现的(CommponentScan,inculudFilter)
  11. Resource到BeanDefiniton的过程相关的问题,从resource–>SimpleMetadataReader–>BeanDefinition(since 2020-01-28)
    1. SimpleMetadataReader的作用是什么?
    2. 需要用到的spring-io中的ResourceLoader需要明确是做什么用的
    3. SimpleMetadataReader构造方法中用到了spring-asm是做什么用的
    4. Lookup注解干什么用的

RoketMq源码学习-八-延迟队列

Posted on 2019-11-30 | Edited on 2022-09-21 | In 消息队列 , rocketmq , 源码学习

在业务中我们发送定时消息等会用到,延迟队列,rocketmq内置了该功能,帮我实现这种延迟队列。实现很简单

实现方式、原理

  1. 生成者发送消息时候为消息设置setDelayTimeLevel,mq不能自己指定延时时间而只能采用系统设置好的level
  2. broker在putmessage时候会将message的real-topic备份,然后将message放到SCHEDULE_TOPIC_XXXX这个topic中,根据delayLevel制定不同的queueId。
  3. ScheduleMessageService会启动timer定期扫描各个delayLevel的Queue,已经到执行时间的message在将SCHEDULE_TOPIC_XXXX的message移到real-topic中。

缺点:由于是一个timer在扫描SCHEDULE_TOPIC_XXXX下所有queue的消息,如果消息积压过多于可能造成消息的发送延迟。可以考虑每个level用一个线程取扫描(不过也要考虑线程切换的成本导致频繁切换反而降低性能)

关键代码

producer

producer发送消息,设置level

1
2
3
public void setDelayTimeLevel(int level) {
this.putProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL, String.valueOf(level));
}

broker

broker处理putMessage

  1. CommitLog.putMessage保存信息会将真实的topic的备份放到指定队列
  2. DefaultMessageStore的ReputMessage在写CounmeQueue的时候会计算DeliverTime并且写入到tagCode中

见代码

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
public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
......
//消息不是事物类型
final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
|| tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
// Delay Delivery Message
if (msg.getDelayTimeLevel() > 0) {
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}

//延时消息--将消息投递到
topic = ScheduleMessageService.SCHEDULE_TOPIC;
queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());

// Backup real topic, queueId 将消息设置成topic
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));

msg.setTopic(topic);
msg.setQueueId(queueId);
}
}
......

return true;
}

//在ConsumeQueue存储消息信息时候会将tagCode设置为发送的日期
private void doReput() {
......
// 生成重放消息重放调度请求,-1-失败,0-到文件尾,1-正常
DispatchRequest dispatchRequest =
DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
......
}

public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, final boolean checkCRC,
final boolean readBody) {
......
// Timing message processing 延时消息将tagsCode设置为时间戳,
{
String t = propertiesMap.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL);
if (ScheduleMessageService.SCHEDULE_TOPIC.equals(topic) && t != null) {
int delayLevel = Integer.parseInt(t);

if (delayLevel > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
delayLevel = this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel();
}

//计算DeliverTimestamp
if (delayLevel > 0) {
tagsCode = this.defaultMessageStore.getScheduleMessageService().computeDeliverTimestamp(delayLevel,
storeTimestamp);
}
}
}
......
}

ScheduleMessageService

level的个数的设置由MessageStoreConfig的messageDelayLevel决定。rocketmq只能按照这个设置来决定消息的延迟时间。

1
messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
  1. 初始化:BrokerController在initialize()时候初始化DefaultMessageStore,DefaultMessageStore会初始化ScheduleMessageService,ScheduleMessageService负责定时任务的check和调度。
  2. load加载配置:BrokerController在initialize()会调用DefaultMessageStore的load,在该方法中会调用ScheduleMessageService的load,关键代码如下
  3. 启动:在BrokerController.start后,随着DefaultMessageStore的start而启动。

    加载配置的过程

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

//DefaultMessageStore的关键代码
public boolean load() {
......
if (null != scheduleMessageService) {
result = result && this.scheduleMessageService.load();
}
......
}

//ScheduleMessageService的代码
public boolean load() {
boolean result = super.load();
//初始化配置的DelayLevel
result = result && this.parseDelayLevel();
return result;
}

public boolean parseDelayLevel() {
HashMap<String, Long> timeUnitTable = new HashMap<String, Long>();
timeUnitTable.put("s", 1000L);
timeUnitTable.put("m", 1000L * 60);
timeUnitTable.put("h", 1000L * 60 * 60);
timeUnitTable.put("d", 1000L * 60 * 60 * 24);

String levelString = this.defaultMessageStore.getMessageStoreConfig().getMessageDelayLevel();
try {
String[] levelArray = levelString.split(" ");
for (int i = 0; i < levelArray.length; i++) {
String value = levelArray[i];
//1h-->ch=h,tu=1000L * 60 * 60
String ch = value.substring(value.length() - 1);
Long tu = timeUnitTable.get(ch);

//初始化maxDelayLevel
int level = i + 1;
if (level > this.maxDelayLevel) {
this.maxDelayLevel = level;
}
//1h-->1
long num = Long.parseLong(value.substring(0, value.length() - 1));
long delayTimeMillis = tu * num;
//put levelIndex,1*1000L * 60 * 60
this.delayLevelTable.put(level, delayTimeMillis);
}
} catch (Exception e) {
log.error("parseDelayLevel exception", e);
log.info("levelString String = {}", levelString);
return false;
}

return true;
}

start

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
    public void start() {
//遍历delayLevelTable,找到每个level的offset,然后异步的启动DeliverDelayedMessageTimerTask去检查
for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
Integer level = entry.getKey();
Long timeDelay = entry.getValue();
Long offset = this.offsetTable.get(level);
if (null == offset) {
offset = 0L;
}

if (timeDelay != null) {
//关键代码,每个level和offset会创建一个DeliverDelayedMessageTimerTask,第一次FIRST_DELAY_TIME(1s后执行)
this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
}
}

this.timer.scheduleAtFixedRate(new TimerTask() {

@Override
public void run() {
try {
ScheduleMessageService.this.persist();
} catch (Throwable e) {
log.error("scheduleAtFixedRate flush exception", e);
}
}
}, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
}
  • DeliverDelayedMessageTimerTask没有采用for/while循环这种来保证扫描delayQueue的实时性,而是每次根据处理Messagge的结果在启动一
  • DeliverDelayedMessageTimerTask来控制频次、和保证实时性。在DeliverDelayedMessageTimerTask的run方法中调用executeOnTimeup这个是主要逻辑
  • 由于延期消息是队列,所以相同粒度的延期信息一定是按照顺序写入到队列中的。所以如果当前消息没到发布时间,后面所有消息就都没到发布时间。具体见下面代码
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
public void executeOnTimeup() {
//根据delayLevel找到制定的consumerQueue
ConsumeQueue cq =
ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(SCHEDULE_TOPIC,
delayLevel2QueueId(delayLevel));

long failScheduleOffset = offset;
if (cq != null) {
//步骤1,在consumerQueue中根据偏移量找到这条消息的索引信息
SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
if (bufferCQ != null) {
try {
long nextOffset = offset;
int i = 0;
//获取cqExtUnit
ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
long offsetPy = bufferCQ.getByteBuffer().getLong();
int sizePy = bufferCQ.getByteBuffer().getInt();
//tagsCode-在conumeQueue持久化时候已经变味了DeliverTimestamp
long tagsCode = bufferCQ.getByteBuffer().getLong();

if (cq.isExtAddr(tagsCode)) {
if (cq.getExt(tagsCode, cqExtUnit)) {
tagsCode = cqExtUnit.getTagsCode();
} else {
//can't find ext content.So re compute tags code.
log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}",
tagsCode, offsetPy, sizePy);
long msgStoreTime = defaultMessageStore.getCommitLog().pickupStoreTimestamp(offsetPy, sizePy);
tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
}
}

//步骤2,计算过期时间
long now = System.currentTimeMillis();
//修正deliverTimestamp,如果deliverTimestamp>now+delayMills说明过期了
long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);

nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);

long countdown = deliverTimestamp - now;
if (countdown <= 0) {
//到期了开始投递消息
MessageExt msgExt =
ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
offsetPy, sizePy);

if (msgExt != null) {
try {
//修改realTopic,发送消息
MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
PutMessageResult putMessageResult =
ScheduleMessageService.this.defaultMessageStore
.putMessage(msgInner);

//发送成功后,继续处理下一条消息
if (putMessageResult != null
&& putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
continue;
} else {
//发送失败,10sec后在开始下一轮扫描
// XXX: warn and notify me
log.error(
"ScheduleMessageService, a message time up, but reput it failed, topic: {} msgId {}",
msgExt.getTopic(), msgExt.getMsgId());
ScheduleMessageService.this.timer.schedule(
new DeliverDelayedMessageTimerTask(this.delayLevel,
nextOffset), DELAY_FOR_A_PERIOD);
ScheduleMessageService.this.updateOffset(this.delayLevel,
nextOffset);
return;
}
} catch (Exception e) {
/*
* XXX: warn and notify me
*/
log.error(
"ScheduleMessageService, messageTimeup execute error, drop it. msgExt="
+ msgExt + ", nextOffset=" + nextOffset + ",offsetPy="
+ offsetPy + ",sizePy=" + sizePy, e);
}
}
} else {
//消息没到期,创建Task扫描一次该消息,延期countdown防止无谓的计算
ScheduleMessageService.this.timer.schedule(
new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
countdown);
ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
return;
}
} // end of for

//队列所有消息都扫描结束后,100L后开启下一轮扫描
//由于延期消息是队列,所以相同粒度的延期信息一定是按照顺序写入到队列中的。所以如果当前消息没到发布时间,后面所有消息就都没到发布时间。具体见下面代码
nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
this.delayLevel, nextOffset), DELAY_FOR_A_WHILE);
ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
return;
} finally {

bufferCQ.release();
}
} // end of if (bufferCQ != null)
else {

long cqMinOffset = cq.getMinOffsetInQueue();
if (offset < cqMinOffset) {
failScheduleOffset = cqMinOffset;
log.error("schedule CQ offset invalid. offset=" + offset + ", cqMinOffset="
+ cqMinOffset + ", queueId=" + cq.getQueueId());
}
}
} // end of if (cq != null)

ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
failScheduleOffset), DELAY_FOR_A_WHILE);
}

JAVA内存模型与线程

Posted on 2019-11-18 | Edited on 2022-09-21 | In java , jvm

是对之前java并发的一个补充。:P

java的操作都是将数据从主内存加载到工作内存去做操作,为了保证线程的安全归定了几个原子性操作。

java的几个原子性操作

  • read:主内存,将数据从主内存传输到工作内存,以便load使用
  • load:工作内存,将read的变量副本放入到工作内存中
  • assign:工作内存,赋值操作。将执行引擎(jvm)中的值赋值给变量。
  • use:工作内存,把工作内存中的值传递给执行引擎。
  • store:工作内存,将数据传输到主内存中,以便write操作使用
  • write:主内存,将数据写入到主内存中。
  • lock:主内存,将变量标识为线程独占
  • unlock:主内存,释放锁标记,标识变量变为共享状态。

几个规定

  • read和load,store和write成对出现,并且先后顺序不能改变
  • assign必须在load之后,且assign之后必须写会主内存,且加载到内存的变量必须assign
  • use、store之前必须assign,load
  • lock操作后工作内存会被清空
  • unlock必须在lock之后,且unlock必须等待变量别写会主内存。

保证线程安全的3个要点

  • 原子性:8个原子操作
  • 可见性:voltile、final、synchrozied可以保证
  • 顺序性:voltile、synchrozied

线程实现

1:1模型:使用内核线程,实现简单,但是无法支持高并发因为内核态和用户态消耗大,并且由于是1:1无法支持高并发。
1:N:使用用户线程,可支持高并发,但是调度等实现难度大。
N:M:用户线程用于并发,内核线程用于调度。综合上面俩种优缺点。

线程状态

new:new一个线程
run:执行start
block:synchronized
wait:没有参数的wait
time_waiting:有参数的wait和sleep

jvm的锁优化

由于锁需要从用户态转到内核态作为控制,所以采用如下几种优化方案。

  • 锁消除:通过逃逸分析发现没有变量逃逸的情况,会去掉锁。
  • 锁粗化:如果多一个对象反复加锁、解锁会考虑合并
  • 偏向锁:如果当前线程获取锁,复制markword,markword修改锁标记位,以及获取锁线程的线程ID;
  • 轻量级锁:如果遇到竞争,复制markword,markword修改锁标记位,以及锁的lockrecord地址。

JVM的编译优化-后期

Posted on 2019-11-17 | Edited on 2022-09-21 | In java , jvm

JIT(java in time complier),即为了提高热点代码效率将代码编译成本地代码。

解释器与编译器

HotSpot采用的是解释器与编译器共存。

解释器可以发挥编译优势,省去编译成本地代码的时间,直接运行;编译器把反复执行的代码编译成本地代码提高执行效率。同时如果编译器的优化比较激进发现编译后的结果不成立可以通过解释器退回到之前状态。

解释器和编译器这种搭配方式称为混合模式。可以通过-Xint来控制

俩个编译器ClientComplier和ServerComplier

ClientComplier注重编译速度,ServerComplier注重编译的质量。HotSpot在JDK1.7时代默认采用分层编译

  • C0:解释执行
  • C1:将字节码编译成本地代码,简单的优化。
  • C2:会启用一些耗时优化。

出发条件

HotSpot采用基于计数器的方式。

  • 方法的重复调用
    • 条件:重复一定次数(方法调用计数器控制)
    • 方式:调用次数+1.触发阈值后发送编译请求。编译完成后替换方法地址
    • 半衰周期:当一段时间没到阈值会,方法调用计数器会衰减一半
    • 默认值:client:1500,server:10000
  • 循环体的调用:OSR(on stack replacement)栈上替换,发生在运行时的方法栈
    • 条件:重复一定次数(回边计数器)
    • 方式:以方法为单位如果,每一次循环回边计数器都+1,如果回边计数器和方法调用计数器超过一定数值后发送编译请求。(这时候重新调整计数器以便继续循环)编译完成后替换方法地址

编译过程

后台编译

client-compile

具体的HIR LIR我也没太看明白以后看编译原理在不上:P

编译优化的技术

编译代码比解释代码优化,一方面是没有虚拟接解释代码的消耗,一方面是所有的代码优化措施都集中在JIT上。

优化技术概览

jit优化技术
jit优化技术

公共子表达式优化

比如程序有俩个bc和cb,javac不会进行优化,java则会优化成E=b*c,然后代码使用E

异常消除

NullPointer,ArrayIndexOutOfBounds等异常信息,在运行时如果不优化每次判断都会带来开销,所以jit会对齐进行优化,消除这些隐式的判断。而且会根据Profile收集到的信息进行“智能”优化。比如一个对象经常为空的情况就不回采用try catch的优化。

方法内联

由于JAVA是面向对象的,很对方法是虚方法,为了优化就采用CHA,如果是虚方法会去查询有几个版本如果只有一个版本会进行内联,如果继承关系发生改变,比如动态代码,这时候要退回解释执行。如果发现多个版本还会采用Inline Cache来尝试内联

逃逸分析

栈上分配、同步消除、标量替换

JVM的编译优化-前期

Posted on 2019-11-15 | Edited on 2022-09-21 | In java , jvm

JAVA是将类文件编译后生成操作码交给解释器执行,所以是解释性语言。

面向栈的指令集和面向寄存器的指令集

java是通过操作码将数据压入/弹出到操作栈中进行操作,所以是面向栈的,而还有一种流派是直接生成本地代码调用寄存器的指令,
俩者的区别。

  • 面向栈:便于移植,代码简单,由于不依赖寄存器,支持寄存器不支持的功能。由于执行指令多所以性能差
  • 面向寄存器:不便于移植,功能依赖于CPU等硬件。性能好

javac编译的过程

编译过程

源码如下

编译过程

解析文件和添加符号表

  • 词法、语法分析:通过词法分析器和语法分析器生成抽象语法树
  • 输入到符号表:将语法树种的在javac源码就是enterTree过程,符号表是一个kv的数据结构,用于收集符号以及变量,在语义校验阶段用于校验语法和产生中间代码,目标代码生成阶段是分配符号内存的依据

注解处理器处理注解

JDK1.5之后支持了注解,实际上是一个个的语法插件,会对语法树进行读取、修改,如果对语法树进行修改,需要重新解析文件和添加符号表,称为Round。也就是图上的回环

语义分析与字节码生成

  • 标注检查:检查变量是否已经声明,类型与赋值类型是否匹配。
  • 数据及控制流分析:进一步对语法和语义做检查。注意局部变量设置为final的语义检查是在编译器的,因为局部变量没有该final的常量标志位,所以在编译后是否声明称final没有区别。
  • 解语法糖:泛型、自动装箱、拆箱等都需要在编译阶段还原。
  • 字节码生成:
    • 实例构造器init方法和类构造器clinit方法添加到语法树,将调用父类的实例构造器等方法收敛到这俩个方法中
    • 优化操作:将String的添加等操作改为StringBuffer.append

JAVA语法糖详解

泛型和类型擦除

JVM虚拟机执行引擎

Posted on 2019-11-12 | Edited on 2022-09-21 | In java , jvm

java的执行引擎是虚拟机自己提供的,所以更加灵活可以支持应将不支持的指令集。(对硬件指令集的封装 :P)

栈帧

jvm执行方法调用的数据结构,一个方法从开始执行到结束执行是一次入栈出栈的过程。
在便宜Class文件时候局部变量表占用内存的大小和栈帧深度都已经决定好了。
一个方法执行调用链会很长,只有当前的线程中调用栈栈顶的栈帧才能被执行,该栈帧被称为当前栈帧(Current Frame)对应的方法是当前方法(Current Method),栈帧结构如图:

栈帧结构]

局部变量表

总共有byte、short、int、short、boolean、float、refrence、returnAddress 8种。

在编译期就在方法的Code属性max_locals就决定了大小。局部变量表的大小单位是Slot,虚拟机规定者byte、short、int、boolean、refrence、returnAddress 数据类型可以用32位或者更小的空间存储,即4byte。如果是64位系统采用8byte但是需要用数据补齐等方式让其看起来是32位的。
long、double则采用64位空间存储即8byte,注意这里虽然是用俩个32位空间存储但是因为是局部变量,但是是建立在线程的栈上没有共享问题是线程安全的。
refrence要遵循俩个协议

  1. 能找到堆中对象的数据起始位置
  2. 能找到方法去中Class类的位置

    参数值到参数变量列表的转换,如果不是static方法,局部变量表第0位给隐藏变量“this”,参数分别占第1~n位。剩下的会分配给方法内生命的局部变量。局部变量的slot可以重用。

    导致的一个“坑”是假如当前的变量后面不需要了,如果后面没有对变量的声明,修改局部变量,在方法执行期间gc是不会回收该局部变量,所以有一些比较耗内存的变量在用完了后,会“a=null”,加速gc的回收

    局部变量不赋值是不能使的,因为没有向static变量有个准备阶段赋0值

操作栈

后入先出,保存栈帧运行时候的数据,比如运算操作,方法调用等,实际上方法的执行就是操作栈不同的入栈/出栈。最大值在方法的属性Code规定的max_stack中在编译时候操作栈的深度就规定好了。栈元素的数据类型必须与自己吗指令序列严格匹配。

动态链接

栈帧都包含一个指向运行时常量池中,该栈帧所对应的方法的引用,用于在运行时将一些符号应用初始化成直接引用的操作。

接口返回地址

  1. Nomal Method Invocation Completion。即正常的方法
  2. Abrupt Method Invocation Completion。即出现异常的返回

无论何种返回都会退回到上层调用者的位置,正常返回有可能将返回值给调用者,调用者会将返回值压入自己的操作栈中。异常返回无返回值。

方法的推出等同于当前的栈帧出栈,同时把返回值压入调用者的操纵栈,修改PC指向方法调用的后面一条指令

方法调用

方法调用有5个指令

invokestatic:静态方法
invokespecial:构造方法,私有方法,父类方法
invokevirtual:调用所有的虚方法
invokeinterface:调用接口方法,会在运行时制定一个接口的实现类
invokedynmatic:运行时动态解析所应用的方法–这是用户设计的应到方法决定的

解析调用

只在编译期就能决定,且不会在运行时修改的方法,这里指invokestatic、invokespecial指令调用的方法,还有final的方法。

分派

由于java面向对象特性中的多态,导致编译器无法决定使用哪个版本需在运行期指定方法版本的方法。

类A a=new B()。B是A的子类或者接口实现类,那么A是静态类型,B是真实类型。即父类、接口是静态类型,实现类、子类是真实类型。

静态分派

如果方法的版本取决于类的静态类型,我们称为静态分派。主要场景用于重载见下面的方法。发生在编译阶段,因为在编译时候只能确认静态版本,但是在实际调用中我们只能通过真是的类型去推断选择一个合理版本

动态分派

如果确认方法的版本去介于类的动态类型,我们称为动态分派。主要场景用于重写。原理是当我们在方法中声明一个对象时候,会把对象的实际数据(引用)压入操作栈,当需要调用方法时候,取出来的是实际数据调用的就是实际数据的方法版本。发生在运行期。

单分派和多分派

当一个方法有俩个宗树的静态、动态分派时候就是多分派。

jvm实现动态分派的方法

由于动态分派需要在class元数据中分析查找方法,为了提高性能,jvm在方法区简历了vtable(虚方法表),当调用指令invokeinterface时候会直接通过vtable的索引查找需要的方法版本

vtable–virtual interface

虚方法表,由于动态分派非常频繁,jvm为了提高性能,在方法区为类创建了虚方法表,虚方法表保存的是类方法的真实地址,如果遇到没重写父类的方法地址就是父类的地址。一般在类加载的链接阶段,当类初始化0值完成方法表也初始化完成

动态类型语言支持

动态类型语言主要是通过java.lang.invoke包支持。他和反射的区别是,发射时模拟类的创建和调用过程,而invoke模拟的是操作码

基于栈的字节码解释执行引擎

java的执行引擎是基于操作栈的,解释执行。与其相对的还有编译执行如图:

编译原理.png

java的解释器和解释执行在jvm中,抽象语法树之前的步骤都独立于jvm虚拟机,所以java是半独立解释性语言。

基于栈或者基于寄存器

  • 基于寄存器的优缺点:优点:直接操作寄存器,不直接操作内存,CPU执行性能高性能高。缺点:由于直接在寄存器操作依赖硬件移植性差
  • 基于栈的优缺点:优点:移植性好,通过操作栈不直接操作寄存器,代码更为紧凑,简单。缺点:效率比基于寄存器低

解释执行过程

将数据先push到操作栈中在pop到局部变量表,在从局部变量表load数据计算然后push到操作栈中。

java类加载机制

Posted on 2019-11-10 | Edited on 2022-09-21 | In java , jvm

JAVA类是在运行时进行转载的,这种动态机制虽然降低了些许性能,但是使用起来更加灵活。相比编译时候需要连接的语言C++等来说。
类加载过程分为:加载-验证-准备-解析-初始化-使用-卸载。其中加载、验证、初始化的开始顺序是固定,但是解析有可能放在初始化后面。并且他们也有可能是交叉进行的。

初始化阶段

jvm对加载没有强制的规定,但是对类的初始化有了强制的规定。即对一个类的主动引用,主动因为指的是一下5中情况。

  1. new对象、调用对象的static属性(final除外因为final直接进入了运行时常量池)、调用对象的static方法。
  2. 初始化一个类时候发现父类没有初始化要对父类进行初始化
  3. main方法所以在的类
  4. 反射时候如果发现类没有初始化
  5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。(实际上是因为第一条的规定)

    注意:接口初始化和类很类似也需要初始化父类接口的class对象,不过不需要初始化所有父类接口,只有使用父类时候才会初始化(使用父类接口的常量)。

加载过程

加载

这里只的是类加载的“加载过程”。记载分为3个过程

  1. 根据类全限定名找到对应外部二进制文件
  2. 将二进制文件加载到内存中
  3. 在方法区创建对应的class类做外部访问数据的接口

    几种外部的二级制文件加载方式zip包、网络、运行时、数据库等等
    注意:数组的加载方式,由虚拟机直接产生。去掉数组的一维,如果是引用类型的话即组件类,该组件类的Classloader加载该类,并且数组类会在组件类的Classloader的的类空间进行标示。如果是int等非引用类型,我们会调用引导类的类加载器。访问方式取决于组件类的访问方式。

验证

java虚拟机校验类文件的合法性,防止恶意篡改、类错误导致虚拟机或者程序崩溃。主要有以下四个阶段

  1. 文件格式校验:判断class的魔数、版本等,通过校验后会将二进制流读入到方法区,后续的校验会针对方法区的二进制流进行校验(检查文件)
  2. 元数据校验:对字节码元数据进行分析,查看是否有父类,父类是否合法。(检查类)
  3. 字节码校验:保证方法体字节码的合法性,比如是否有非法的类型转换、赋值能。jdk1.7后增加了StackMapTable用来保存方法体中初始的状态,校验时候直接通过读取StackMapTable即可(检查方法)
  4. 符合引用校验:字段的引用是否合法,是否访问了不存在的字段、方法、以及超过了访问权限等

准备

java虚拟机为类的变量分配内存的过程,这里指的是static修饰的变量因为实例变量会在实例创建时候赋值。对于一般情况变量分配完内存后要赋0值,真实的值保存在<clinit>()方法中,在初始化时候才会赋值。final修饰的变量会在该阶段赋值真实值

解析

java将class文件中的符号引用改为直接引用的过程

字段的解析

  • 普通引用比如对于字段N类型C来说,将N交给类的ClassLoader加载C的类型
  • 数组加载数组的ComponentType的类,然后由虚拟机生成数组的直接应用
  • 校验范围权限

    类方法的解析

  • 检查类的class_index如果是类方法,class_index是接口方法直接抛出java.lang.IncompatibleClassChangeError异常。

  • 类中有方法的简单名称和描述符返回,否则去父类查找如果有返回,否则去接口查找如果接口有抛异常java.lang.AccessMethodError,如果没有抛出java.lang.NoSuchMethodError
  • 检查访问权限

    接口方法解析:步骤同上只是不用检查权限

初始化

在该阶段开始执行java代码(字节码),生成方法<clinit>()。

  • clinit方法是由静态字段和静态代码块共同合并生成,且顺序自上到下
  • 如果有父类,且父类有有静态类和静态字段,父类的clinit方法会先调用
  • 接口因为也可能有静态阶段,接口也可能生成clinit,如果有父类接口,只有用到父类接口的变量才会生成父类接口的clinit方法

注意clinit出于线程安全考虑会加锁这就是为什么static代码块只执行一次。如果static里阻塞会阻塞类的创建

类加载器

唯一性:ClassLoader+类=唯一,每个ClassLoader都有自己的类命名空间,这个会影响到instanceOf等结果。

java的classloader分为3种

  1. 启动类加载器:Bootstrap ClassLoader,由C++编写,java无法直接调用,如果将类委托给该加载器调用改classloader回报异常。用于加载JAVA_HOME/lib以及 -Xbootclasspath对应地址的类并且要符合命名规则,比如rt.jar
  2. 扩展类加载器:Extension ClassLoader,由java编写,java可直接调用。JAVA_HOME/ext/lib或者java.ext.dirs指定的类
  3. 应用类加载器:Application ClassLoader,由java编写,java可直接调用。classpath中的类,如果不指定自己的classloader他是默认的classloader

类加载的双亲委派模型:即类加载器会优先调用父类的加载器,父类加载器会调用直到启动类加载器,之后在调用自己的类加载器。好处在于对于一些系统的类全局只有一个唯一的Class类(受唯一性约束)。顺序是:启动类加载器–>扩展类加载器–>应用类加载器–>自定义类加载器。

这是java建议的使用方法

双亲委派模型的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected synchronized Class<?>loadClass(String name,boolean resolve)throws ClassNotFoundException {
//首先,检查请求的类是否已经被加载过了
Class c=findLoadedClass(name)
if(c==null){
try{
if(parent!=null){
c=parent.loadClass(name,false);
} else {
c=findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
//如果父类加载器抛出ClassNotFoundException
//说明父类加载器无法完成加载请求
} if(c==null){
//在父类加载器无法加载的时候
//再调用本身的findClass方法来进行类加载
c=findClass(name);
}

JAVA类文件结构

Posted on 2019-11-05 | Edited on 2022-09-21 | In java , jvm

class类的结构

如图,单位是字节u1代表1个字节。

avator

魔术和版本号

文件前4个字节代表这是个java文件固定CAFEBABE
第5、6字节代表此版本号,7、8字节代表主版本号,版本号有向下兼容性

常量池

紧接着这就是常量池了,常量池的前俩个字节偏移量是0x00000008,代表常量的个数,和java其他的数据表示方法不一样,常量池下标是从1开始,(比如数字是22,常量21个索引范围是1~21个)常量池有14种数据类型,每一个常量的第一位tag(长度u1)就是它类型,见下图:
avator

具体数据个数如下:
avator

可以通过javap命令查看类的常量池 javap-verbose TestClass

访问标志

是一个标志位,标志类是否是public的是interface还是class是否声明为final等,具体如下:

avator

计算方式采用“|”

类索引、父类索引和接口索引集合

类索引、父类索引长度都是8,指向常量表的索引,接口索引是一组指向常量表的索引。
除了Object类,父类索引都不是0。

字段表、方法表集合

包括了

  • 访问标志(access_flags):public、private等,长度8个字节
  • 名称索引(name_index):指向常量表
  • 修饰符所以(descripor_index):用简短方法表述字符比如java.lang.String[],描述为[Ljava/lang/String
  • 属性集合表count(attribute_count):长度8个字节,代表属性长度
  • 属性信息(attribute_info):长度8个字节,数量attribute_count,代表属性信息。方法有固定的属性Code

    附:
    字段表结构
    avator
    字段access_flags
    avator
    方法表访问标志位
    avator
    demo
    avator

属性表

avator
avator

java的自己码指令

jvm的指令码是面试操作栈的,它的指令由1个字节的代码特殊含义的操作码以及后面0到多个操作参数组成。由于操作码是1个字节所以java最多有255个操作码。

感觉jvm是讲数据读到操作栈中操作,在写回栈帧的局部变量中。

JVM的性能分析

Posted on 2019-10-25 | Edited on 2022-09-21 | In java , jvm

jvm的常用工具

jps

作用:监视java进程状态的工具

命令格式:jps [option][hostid]

参数:
-m:输出启动时传给main函数的方法
-l:输出主类全名如果是jar包,输出路径
-v:输出虚拟机启动时Jvm的参数

jstat

作用:监视java进程各种运行状态的信息

命令格式:jstat[option] vmid [interval][s|ms][count]]

参数:主要分三类、类装在、垃圾收集、运行期编译状况

-class
-gc
-gccapacity
-gcutil
-gccaise
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcpermcapacity
-complier
-printcompilation

1
2
3
4
5
6
7
8
9
10
11
12
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 6.20 41.42 47.20 16 0.105 3 0.472 0.577

s0、s1:survivor
E:eden区
O:old区
M:metaspace
YGC:minorGc的次数
YGCT:minorGc的时间
FGC:FullGC的次数
FGCT:FullGc的时间
GCT:的时间

jinfo

作用:查看、修改虚拟机的配置。

命令格式:jinfo [option] pid

参数:
-flag <name> to print the value of the named VM flag
-flag [+|-]<name> to enable or disable the named VM flag
-flag <name>=<value> to set the named VM flag to the given value
-flags to print VM flags
-sysprops to print Java system properties

jmap

作用:java内存映射工具,可以查询finalize执行队列、java堆和永久代的详细信息、如空间使用率、当前采用的收集器等

命令格式: jmap [option] <pid>

参数:
-heap 打印java堆详细的信息
-histo[:live] 打印堆中对象统计信息,Live只统计存活的信息
-clstats 打印Classloder的信息
-finalizerinfo 显示F-Queue中等待Finalizer线程执行finalizer方法的对象
-dump:<dump-options> 生成java堆快照:live只打印存活的对象,如果不指定打印所有对象,format=b二进制格式的文件,file=<file>打印到某个<file>中。eg: jmap -dump:live,format=b,file=heap.bin <pid>
-F force. Use with -dump:<dump-options> <pid> or -histo
to force a heap dump or histogram when <pid> does not
respond. The “live” suboption is not supported
in this mode.
-J<flag> to pass <flag> directly to the runtime system

jhat

作用:配合jmap分析dump出来的jmap文件,不建议在应用服务器上使用,因为dump文件很大,并且很耗性能,建议在单独的服务器上执行
命令格式:jhat /dumpfile
使用方法:打开浏览器,访问jhat所在服务器的7000端口。查看内存泄漏用OQL工具和Heap Historygram功能

jstack

作用:java堆栈跟踪工具,定位java每一个线程当前时刻的执行快照,可以定位到线程运行的状况是否死锁等。

命令格式: jmap [option] <pid>

参数:
-F 当正常输出不被响应时候,强制输出程序堆栈
-l 显示关于锁的相关信息
-m 如果调用的是本地方法显示C/C++堆栈

HSDIS

作用:jit生成码反汇编插件,通过jvm设置的参数-XX PrintAssmbly把动态生成的代码还原成汇编代码

命令格式: jmap [option] <pid>

参数:
-F 当正常输出不被响应时候,强制输出程序堆栈
-l 显示关于锁的相关信息
-m 如果调用的是本地方法显示C/C++堆栈

可视化工具

  • JConsole

  • VisualVM

    综上,我们可以用jps jstat jinfo查看jvm的信息。jmap、jhat查看堆信息。jstack查看栈信息。我们可以用可视化工具来方便查看上面的信息推荐VisualVM

    下载地址: http://visualvm.java.net/download.html
    idea可以通过pulgins安装通过简单配置即可实现动态的VisualVM功能

具体案例分析

高性能硬件的场景

如果硬件性能较高,我们一般采用俩种方式

  1. 采用64位虚拟机,设置很大的堆内存
  2. 采用32位虚拟机,虽然堆内存最大设置4(还受限于平台对进程最大使用内存的限制有可能只有2g,windows平台)我们可以给一台服务器设置多个进程+前端反向代理的方法。

    第一种方案我们要注意64位虚拟机往往比32位虚拟机要慢(指针膨胀、内存补齐等需求),另外由于堆内存很大,我们一定要注意大对象导致频繁GC的问题,因为一次fullGC有可能需要停顿10几秒,对于交互多的服务体验会很差

共享缓存导致oom

由于共享缓存频繁的写入导致内存oom。需要检查缓存的使用率。

对外内存导致的oom

有以下几种对外内存

DirectMemory:主要用于nio场景,比如内存环境是2G。我们给堆内存分配了1.6G。那么堆外内存最多只能有0.4G不到,如果这时候大量请求过来会导致oom

线程堆栈:会出现StackOverflowError或者StackOutOfMemoryError

JNI的调用:由于是native方法所以也是堆外内存

Socket的读写Buffer:receive和send都有一个buffer分别是37kb和25kb。如果超出了会出现open many file的错误。

系统线程和gc线程

外部命令导致系统变缓慢

比如调用Runtime.getRuntime.exec()执行shell。java是fork了个进程,这个指令很重如果调用的频繁系统的负载会很重,建议采用api的方式。

外部接口超时导致系统崩溃

调用远程接口响应较慢导致连接大量被占用,同时对连接没做限制导致了虚拟机崩溃。需要设置超时时间,或者采用生产者消费者异步的方式。

JVM的垃圾回收机制

Posted on 2019-10-17 | Edited on 2022-09-21 | In java , jvm

jvm的内存分为线程独享的线程栈、程序计数器;线程共享的方法区、堆区,线程栈在编译时候就确定好了大小,方法执行完成后自动就可以回收所以是静态的。而方法区、堆区只有在运行时才知道内存分配的大小所以内存回收是动态的。本章主要讨论的是堆和方法区的垃圾回收

对象是否可被回收算法

引用计数算法

给对象添加一个引用计数器,当被引用时候引用计数+1,在垃圾回收时候只要应用计数为0就可以回收对象了。算法很简单,弊端是无法解决循环应用问题。
即:

1
2
a.instance=b;
b.instance=a;

这时候a和b是无法被回收的。

可达性分析算法-java采用的方式

我们通过对象GCRoot做可达性分析,如果对象的引用链没有GCRoot对象,说明对象是可以被回收的。jvm中适合做GCRoot对象的有

  • jvm方法栈中的局部变量表中的变量
  • jvm本地方法栈中的native对象
  • 方法区中的静态变量
  • 方法区中的常量

对象的引用

对象按照应用的按照从强到弱分别如下:

  • 强引用:我们new一个对象默认就是强引用。必须等到GcRoot不可达
  • 软引用:SoftReference对象,不管内存是否足够,下一次gc都会回收该对象
  • 弱引用:WeakReference对象,当内存不足时候时候gc会回收弱引用对象
  • 虚引用:PhantomReference,又称幽灵引用,不对对象的生命周期造成影响,get()方法返回的也是null,只在对象被回收时候系统会收到一个通知。使用场景堆外内存回收。系统调用unfase方法申请一块堆外内存,然后在堆中会持有一个虚引用的对象,当该对象被回收时候会清理堆外内存

finalize方法以及对象的自救

对象被回收需要通过标记、回收两部,第一步先标记可会回收的对象,然后检查对象是否实现了finalize()方法,如果对象实现了finalize方法并且在gc时候没有被系统调用过,则认为是可finalize的,会放在一个优先级比较低的F-Queue队列中,之后会有finalize线程执行finalize()方法,如果这时候把this赋值给别的标量则完成了对象的自救。

注意:

  • 只会执行一次;
  • finalize的执行由于优先级较低即为不靠谱不推荐使用。比如在finalize方法中执行一些清理操作我们可以放在finnal中执行

回收方法区

永久代的回收效率比较低(类信息、常量、静态变量),主要回收类信息、常量

回收常量:当该常量没有任何应用

回收类信息:

  1. 没有任何该class的实例
  2. 该类的ClassLoader被回收
  3. Class没有任何访问、即通过反射创建该Class的元素

gc的算法

mark-swap 标记清理

标记对象后将需要回收的对象进行销毁、会造成内存碎片,可能导致大对象没法分配

复制算法

年轻代会采用这种算法,jvm认为大量对象是朝生夕死,所以会将内存分为Eden和俩个Survivor空间,默认比例是8:1:1,平时只会用到一个Eden和1个Survivor。当gc时候会将所有对象赋值到没用的那个Survivor空间,然后清掉Eden和原来的Survivor空间。如果剩下的Survivor不足会进入老年代。

mark-compact 标记整理

标记之后让所有对象向一端移动,超过这个范围的对象会被回收。

HotSpot的算法实现

枚举根节点

从GcRoot找引用链,该操作对时间很敏感,因为这一步操作对数据一致性要求很高,所以在执行时候虚拟机必须要StopTheWorld,否则枚举根节点的结果就不准确了。方法区和堆内存往往都很大,如果都需要分析一遍性能会严重受影响。

HotSpot采用了OopMap对象,需要虚拟机的解释器和JIT编译器支持,由他们来生成OopMap,把对象偏移量内存放的是什么数据保存起来,gc就很快知道了对象的信息。

安全点 safe point

虽然OopMap在gc时候能很方便的完成枚举根节点,但是影响OopMap数据的操作很多如果每一步操作都创建OopMap会很耗费内存。为了解决这个问题HotSpot设置了safePoint的概念,只有程序执行到safePoint,才会停止线程开始gc,并且在gc前创建OopMap。即每一个safePoint会生成一个OopMap对象。

安全点选定太少,GC等待时间就太长,选的太多,GC就过于频繁。选定原则是”具有让程序长时间执行的特征“,也就是在这个时刻现有的指令是可以复用的。一般选在方法调用、循环跳转、抛出异常的位置。

现在的问题是在Safe Point让线程们以怎样的机制中断,方案有两种:抢先式中断、主动式中断。

  • 抢先式中断:GC发生时,中断所有线程,如果发现有线程不再安全点上,就恢复线程让它运行到安全点上。现在几乎不用这种方案。
  • 主动式中断:设置一个标志,和安全点重合,再加上创建对象分配内存的地方。各个线程主动轮询这个标志,发现中断标志为真就挂起自己。HotSpot使用主动式中断。

安全区域 safe region

如果线程没有分配cpu时间,必须线程处于sleep或blocked状态,就无法响应JVM的中断请求,走到安全点去挂起。Safe Region解决了这一问题。

安全区域是指在一段代码片段中,引用关系不会发生变化,在该区域的任何地方发生GC都是安全的。当代码执行到安全区域时,首先标识自己已经进入了安全区域,那样如果在这段时间里JVM发起GC,就不用管标示自己在安全区域的那些线程了,在线程离开安全区域时,会检查系统是否正在执行GC,如果是,就等到GC完成后再离开安全区域。

java的垃圾回收器

HotSpot在jdk1.7之后的所有垃圾回收器,如图:

avator

Serial垃圾回收器

最早的最基本的垃圾回收器,jdk1.3时期新生代唯一的垃圾收集器。会停止jvm所有的工作线程。

是client模式下,新生代默认的垃圾收集器,因为没有线程交互开销所以在gc过程很高效,如果新生代内存不大,收集一次的耗时是可以接受的。

ParNew收集器

Serial的多线程版本。

是server模式下首选的新生代垃圾收集器。原因是jdk1.5之后的CMS收集器除了Serial以外只有它能和ParNew配合使用。

开启CMS后,-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

在单线程或者俩个线程时候由于有线程切换的问题表现不见得比Serial好,他适合线程数或者核数多的场景。默认开启的线程数等于cpu的数量,当然我们也可以通过参数-XX:ParallelGCThreads来控制

Parallel Scavenge收集器

更关注系统的吞吐量【运行代码时间/(运行代码时间+垃圾回收时间))】适用于内部计算、和外部没有交互的服务,主要参数:

  • -XX:MaxGCPauseMillis:设置停顿时间,保证垃圾回收时间尽量不超过这个阈值,采用所少新生代空间,会导致垃圾回收更频繁
  • -XX:GCTimeRatio:垃圾收集时间占总时间的比例。
  • -XX:+UseAdaptiveSizePolicy:值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。

Serial Old收集器

Serial的老年版垃圾收集,采用“标记-整理”算法,目前主要用于配合Parallel Scavenge和作为CMS的替代方案。

Parallel Old收集器

Parallel Scavenge老年版,使用多线程和“标记-整理”算法。适合吞吐量优先的场景。

CMS收集器

大名鼎鼎的ConcurrentMarkSweep。以获取最短停顿时间为目标的垃圾回收期,试用场景希望停顿时间较短的场景。

分为四个阶段:

  1. 初始标记(CMS Initial Mark):会stop the world。标记GcRoots能直接关联的对象。只做标记所以很快;
  2. 并发标记(CMS Concurrrent Mark):不会stop the world和用户线程一起处理。去做GcRoot Tracing;
  3. 重新标记(CMS Remark):会stop the world。处理初始标记到并发标记过程中新创建的对象,比初始标记长但是这时候创建的对象很少,所以停顿时间可接受;
  4. 并发清除(CMS Concurrrent Sweep):不会stop the world和用户线程一起处理。去做对象的清理操作;

如图:

avator

默认启动线程是(CPU数量+3)/4,所以在CPU核数变高时候CMS占用的线程资源会越来越少。

缺点:

  • 对cpu敏感会占用一部分用户线程,从而cpu负载很高时候导致程序变慢。
  • CMS无法处理浮动垃圾(即:因为在回收时候会产生的新对象),有可能导致另一次GC。因为在做垃圾回收时候用户线程在工作所以要预留一部分空间给用户线程。默认是92%(since 1.6)
  • 内存碎片问题:由于标记清除会带来内存碎片问题导致明明很大的空间但是无法申请内存,这时候就要进行一次FullGC。
    • CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
    • 虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full,然后在来一次压缩的默认是0,每次进行FullGc都压缩。

G1收集器

面向服务端收集器,特点:

  • 并行与并发:充分利用多核cpu,缩短stoptheworld时间。其他收集器需要停顿的它依然可以工作。
  • 分代收集:虽然依然分代,但是g1可以不借助其他收集器独自工作。
  • 空间整合:不会产生碎片。收集后提供规整内存。
  • 可预测的停顿:用户可指定消耗在gc上的时间不超过n毫秒

    G1不在区分新生代和老年代而是将堆内存分为一些大小相等的Region,Region分为新生代和老年代并且且同一年代的Region不连续的。
    G1可预测的停顿时间的实现原理是。是内部维护了一个垃圾回收的优先级列表,针对Region内可回收的对象的大小、多少、收益能排序,优先回收收益较大的Region。

    G1是如何解决不同Region之间的相互引用的,即新生代的Region引用了老年代的Region,它是通过每个Region持有的RememberSet。当一个Refrence发生写操作时候会生成一个WriteBarrier,暂时中断写操作,如果发现他们俩个处于不同的Region,则通过CardTable将应用信息记录到被引用对象Region的RememberSet,回收时候只需要RemberSet就可以知道准确的引用关系避免了全堆扫描。

    G1的垃圾回收步骤

  1. 初始标记:标记下GcRoots能直接关联的对象,并且修改TAMS(next top at mark start)值,能让下一阶段并发标记时候在正确的region中创建值。需要停顿,但是很短。
  2. 并发标记:同cms,可以和工作线程一起运行,进行可达性分析查找存活对象,耗时较长。不需要停顿,可并行执行。
  3. 最终标记:修正初始标记到并发标记。从初始标记到最终标记这段时间内创建的对象都会记录到Remember Set Log中,合并到Remember Set中这段时间需要停顿线程,但是可并行执行。
  4. 筛选回收:根据用户设置的阈值指定回收计划。这一步理论上是不需要停顿的,但是但是停顿用户线程会增加回收效率。

如图:
avator

综述:
我们一般ParNew+CMS或者是G1的方案。

内存分配

对象默认分配到eden区,如果开启TLAB会有限直接分配在线程栈中。

大对象会直接进入老年代,对象大小超过一个PretenureSizeThreshold会直接进入老年代,

长期存活的对象会晋升入老年代,每次垃圾回收对象的age会+1,如果到了MaxTenuringThreshold(默认值是15)会直接进入老年代。对象的动态年龄规划:当survivor区相同年龄的对象超过了survivor大小的一半,那么大于等于这个年龄的对象都可以直接进入到老年代。

共同担保机制:当MinorGC发生时候,如果老年代的连续空闲内存空间超过新生代使用的对象的空间即可正常发生MinorGC,否则查看HanleProotionFailure值,如果开启,则会尝试MinorGC,失败后触发FullGC。如果关闭直接进行FullGC

1…91011…23

Liu hao

励志当好厨子的程序员

229 posts
54 categories
81 tags
RSS
GitHub E-Mail
© 2018 – 2023 Liu hao
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.0.0