0%

这边文章是想总结一下软件系统的设计表达方式,通过什么样的方式可以让你更有效的表达你的系统设计,如何让别人更清晰的去理解你的系统,以及在多人协作开发的时候如何让别人理解不同模块间的职责,明确每个人要做的事情。旨在提高协作效率,让你的系统不再沉默。在软件开发的各层次设计中最主要的是也是开发人员最不喜欢的是写文档,而文档中的图表也是你表达的一个关键点,接下来是要总结下不同表达层次的图表使用示例。总结下平时用到的一些图表以及使用场景。

流程图

泳道图

思维导图

时序图

ER图

架构图

知识图谱

甘特图

模板来源: https://v3.processon.com/popular

这个标题看起有点鸡汤文,不过我还是建议对以下总结出的几点做些深入思考,这些会在今后的工作中越来越多的感受到它的作用。

寻找你行业内的专家

找到你所属行业内的专家,这些人往往做事高效并且很有才华。你要做的是跟随他们所关注的方向,学习他们做事的方法,思考如何应用到你的工作和生活上。找到他们,和他们去交流思考,提出自己的观点和想法。不要仅仅把眼光放到身边的人身上,这样会局限住你的视野。

每天都写新代码

工作重复枯燥?也许有时候我们只是懒得思考,用最顺手的方式把工作做完,容易形成惯性思维。为什么会有很多的复制粘贴?简单的修改来适配当前需求,这里我们更需要的是想想能不能把这段逻辑抽象出来变得更通用,整个模块的设计是否不够合理,多想一想多做一点,下一次再来需求也许可以提升十倍的效率。

底层的原理更重要

客观的说,更快进步的方法之一是忽略掉那些并不能提高技能的东西,比如语言语法和配置工具,这些技能属于“知其然”,而你更需要的是“知其所以然”。有一次去医院科室挂号使用的是先到先叫的模式,而在急诊室挂号是按照轻重缓急分成四个等级的,危重病人优先抢救的模式。这不就和操作系统中的任务调度概念是一样的,优先级调度模式,这些底层的概念才是一通百通真正提高帮助你的东西。我在尝试去找行业经典论文看。

学会调研

作为程序员会比较容易脑子一热,有一个想法很容易趁热着急写代码,但往往缺乏思考写出来的代码不能尽如人意。这时候你更需要的是慢下来,好好思考一下,也许这些别人已经做过,有更好的方案,看看别人是如何做的。先调研再实施,这样会彻底改变你解决问题的思路。

学好英语

真的是这样,如果你英语不好,那么会比别人走更多的弯路,就像走在密林深处看不清路一样。不得不承认很多优秀框架的官方文档还是英文为主,如果再经过翻译里面的很多语义语境会丢失,在项目的社区中,你还能与作者们去交流你学习中遇到的问题。

如何去做

说了这么多,看着就好像道理我都懂,但是我不知道怎么做。我这里先总结几个点,也是自己在不断尝试学习的方法。

  1. 看行业经典论文,比如 mapreduce、raft 这些都是一通百通的底层概念。
  2. 研究优秀框架的源代码,理解核心原理,尝试造轮子。
  3. 每天学英语,尝试在开源社区与作者们进行互动。
  4. 找到一两位行业专家,向他们学习和请教问题。
  5. 坚持以上几点。

end.

ORM(Object/Relational Mapper),即“对象-关系型数据映射组件”。对于O/R,即 Object(对象)和Relational(关系型数据),表示必须同时使用面向对象和关系型数据进行开发。本文简述通过 Java 动态代理机制实现关系数据与 POJO 对象的映射。

代理

静态代理

静态代理其实就是指设计模式中的代理模式。
代理模式为其他对象提供一种代理以控制对这个对象的访问。

静态代理模式在增强现有的接口业务功能方面有很大的优点,但是大量使用这种静态代理,会使我们系统内的类的规模增大,并且不易维护。

动态代理

为了解决静态代理的问题,引入动态代理的概念,在编译时或者运行时,可以在需要代理的地方动态生成代理,减轻代理类和类在系统中冗余的问题。

Java 动态代理基于经典代理模式,引入了一个 InvocationHandler,InvocationHandler 负责统一管理所有的方法调用。

InvocationHandler

InvocationHandler 接口定义:

1
2
3
4
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

每一个动态代理类都必须要实现 InvocationHandler 这个接口,通过代理类的实例调用一个方法时,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke 方法来进行调用。

Proxy

Proxy 这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法,可以获得一个动态的代理对象:

1
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,  InvocationHandler h)  throws IllegalArgumentException

实现

参照 mybaits 的用法实现基本的映射能力。

注解

首先定义了三个注解,一个作用在类上 DaoMapper 作用在类上标记这是一个映射类,然后定义注解 Selector 作用在方法上标记查询作用,定义注解 Param 作用在参数上为预编译位的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DaoMapper {
}

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Selector {
String value();
}

@Documented
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
String value();
}

定义一个实体类,与数据库的表字段映射上。增强 feature 可以自动做驼峰转换,这里没有实现。

1
2
3
4
5
6
7
8
9
@Data
public class BaseLineModel {
public static final String TABLE = "baseline";

private Integer id;
private String report_name;
private Integer report_period;
private LocalDateTime creation_date;
}

定义dao层接口,加上注解

1
2
3
4
5
6
@DaoMapper
public interface BaseLineDao {

@Selector("select * from "+ BaseLineModel.TABLE +" where report_name = #{reportName}")
BaseLineModel select(@Param("reportName") String report_name);
}

JDBC OP

做到一个很简单的 JDBC 操作工具类,字段映射处理也写到了这里。实现了查询操作,将入参 sql template 以及参数按顺序传入,生成 prepareStatement 后执行,再将返回结果映射到 model 对象。这里的连接池管理、自动重连、配置管理等增强 features 非重点,不做实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 查询
* @param clazz model类
* @param sql
* @param params
* @param <T>
* @return
*/
public <T> T query(Class<T> clazz, String sql, Object... params) throws SQLException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
Object model = clazz.newInstance();
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cat", "root", "123456")) {
PreparedStatement statement = conn.prepareStatement(sql);
int flag = 1;
for (Object obj : params) {
setValue(statement, flag, obj);
flag++;
}
ResultSet resultSet = statement.executeQuery();
resultSet.afterLast();
resultSet.previous();
fullRes(resultSet, model);
}
return (T) model;
}

映射函数,通过自动寻找 setter 方法填充结果,这里只实现了三种字段。

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
private static void fullRes(ResultSet resultSet, Object model) throws SQLException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Field[] declaredFields = model.getClass().getDeclaredFields();
for (Field field : declaredFields) {
String fieldName = field.getName();
if (fieldName.toUpperCase().equals(fieldName)) {
continue;
}
String setFuncName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
String fieldType = field.getGenericType().toString();

Object object = resultSet.getObject(fieldName);
if (fieldType.equals("class java.lang.String")) {
Method m = model.getClass().getMethod(setFuncName, String.class);
m.invoke(model, object);
} else if (fieldType.equals("class java.lang.Integer")) {
Method m = model.getClass().getMethod(setFuncName, Integer.class);
m.invoke(model, object);
} else if (fieldType.equals("class java.time.LocalDateTime")) {
Method m = model.getClass().getMethod(setFuncName, LocalDateTime.class);
if (object instanceof Timestamp) {
object = ((Timestamp) object).toLocalDateTime();
}
m.invoke(model, object);
}
}
}

动态代理部分

定义一个 MapperMethod 类,实例化的时候提取接口方法的注解信息解析成 JDBC 需要的参数以及记录接口方法的返回对象, execute 执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

public class MapperMethod<T> {
private String sql;
private Class<?> resType;
private int[] paramsIndex;


public MapperMethod(Method method) {
this.resType = method.getReturnType();
String sourceSql = method.getAnnotation(Selector.class).value();
Parameter[] parameters = method.getParameters();
int flag = 0;
this.paramsIndex = new int[parameters.length];
for (Parameter parameter: parameters) {
String paramName = parameter.getAnnotation(Param.class).value();
String paramFullName = String.format("#{%s}", paramName);
int indexOf = sourceSql.indexOf(paramFullName);
this.paramsIndex[flag] = indexOf;
flag++;
this.sql = sourceSql.replace(paramFullName, "?");
}
}

public Object execute(Object[] objects) {
JdbcUtil jdbcUtil = new JdbcUtil();
try {
return jdbcUtil.query(this.resType, this.sql, objects);
} catch (SQLException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return null;
}
}

定义动态代理类,在实例化的时候记录代理接口,以及代理方法类缓存,调用接口的时候会被动态代理到 invoke 函数执行,然后交由 MapperMethod 代理方法实例执行。

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;

public class MapperProxy<T> implements InvocationHandler {

private final Class<T> mapperInterface;

private final Map<Method, MapperMethod> methodCache;

public MapperProxy(Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}

@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(objects);
}

private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (Objects.isNull(mapperMethod)) {
mapperMethod = new MapperMethod(method);
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}

最后代理工厂类,接收被 DaoMapper 作用的接口,并通过 newInstance 方法创建代理类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MapperProxyFactory<T> {

private final Class<T> mapperInterface;

private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

public MapperProxyFactory(Class<T> mapperInterface) {
if (Objects.isNull(mapperInterface.getAnnotation(DaoMapper.class))) {
throw new RuntimeException("缺少注解 DaoMapper");
}
this.mapperInterface = mapperInterface;
}


public T newInstance() {
final MapperProxy<T> mapperProxy = new MapperProxy<>(mapperInterface, methodCache);
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
}

执行,创建一个代理工厂,然后创建 BaseLineDao 的代理对象, 调用 select 方法,实际上调用到代理对象的 invoke 方法,然后交由 mapperMethod.execute 方法执行:

1
2
3
4
5
6
public static void main(String[] args) {
MapperProxyFactory mapperProxyFactory = new MapperProxyFactory(BaseLineDao.class);
BaseLineDao baseLineDao = (BaseLineDao) mapperProxyFactory.newInstance();
BaseLineModel test1 = baseLineDao.select("TEST1");
System.out.println(test1);
}

扩展

TODO:

  1. Java动态代理与 cglib 动态代理的异同点。
  2. 动态代理的实现原理。

总结

通过这个个简单的实践,了解了 Java 动态代理的使用方法以及对象关系数据的映射处理。

参考

https://zhuanlan.zhihu.com/p/60805342
https://www.zhihu.com/question/20794107/answer/658139129

锁解决的问题是并发操作引起的脏读、数据不一致问题。

基本原理

volatile

在Java中允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保使用排它锁来单独获得这个变量,Java中提供了 volatile,使之在多处理器开发中保证变量的可见性,当一个线程改变了共享变量,另一个线程能够及时读到这个修改的值。恰当的使用它会比 synchronized 成本更低,因为不会引起上下文的切换和调度。

synchronized

通过锁机制实现同步,在Java中每一个对象都可以作为锁,有以下三种形式:

  • 对于普通同步方法,锁的是当前实例对象。
  • 对于静态同步方法,所得是当前类 class 对象。
  • 对于同步方法块,锁的是括号内指定的对象。

为了减少获得锁和释放锁带来的性能消耗,Java SE 1.6 引入了偏向锁和轻量级锁。偏向锁的核心思想是:如果一个线程获得了锁,就进入偏向模式,当这个线程再次请求锁时,如果没有其它线程获取过该锁,无需再做任何同步操作,可以节省大量锁申请的操作,来提高性能。如果偏向锁获取失败,会通过轻量级锁的方式获取,如果获取成功则进入临界区,如果失败则表示有其它线程争夺到锁,当前线程锁请求会膨胀为重量级锁

锁粗化 是指在遇到一连串连续的对同一个锁不断的进行请求和释放的操作时,会把所有的锁操作整合成对锁的一次请求,减少锁请求的同步次数。

锁消除 是指在编译期,通过对上下文的扫描,去除不可能存在共享资源竞争的锁。

自旋锁 是指在锁膨胀后,避免线程真正的在操作系统层面被挂起,通过对线程做几个空循环,以期望在这之后能获取到锁,顺利的进入临界区,如果还获取不到,则会真正被操作系统层面挂起。

CAS

指的是比较并交换,它是一个原子操作,比较一个内存位置的值并且只有相等时修改这个内存位置的值并更新值,保证新的值总是基于最新的信息计算的。在 JVM 中 CAS 操作是利用处理器提供的 CMPXCHS 指令实现。是实现我们平时所说的自旋锁或乐观锁的核心操作。

优点是竞争小的时候使用系统开销小;对应缺点是循环时间长开销大、ABA问题、只能保证一个变量的原子操作。

ABA 问题

问题产生原因是两个线程处理的时间差导致,具体如下图:

解决 ABA 问题可以增加一个版本号,在每次修改值的时候增加一个版本号。

产生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);

public static void main(String[] args) {
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
},"t1").start();

new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t修改后的值:" + atomicReference.get());
},"t2").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
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);

public static void main(String[] args) {
new Thread(() -> {
System.out.println("t1拿到的初始版本号:" + atomicStampedReference.getStamp());

//睡眠1秒,是为了让t2线程也拿到同样的初始版本号
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
},"t1").start();

new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println("t2拿到的初始版本号:" + stamp);

//睡眠3秒,是为了让t1线程完成ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最新版本号:" + atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,atomicStampedReference.getStamp() + 1) + "\t当前 值:" + atomicStampedReference.getReference());
},"t2").start();
}

算法概述

Raft 算法是解决分布式系统一致性问题的,与 Paxos 实现的功能相同,相对来说更容易实现和理解。这些一致性协议可以保证在集群中大部分节点(半数以上节点)可用的情况下,集群依然可以工作并给出一个正确的结果。
Raft 将一致性问题分解为多个子模块解决:

  • Leader 选举 Leader election
  • 日志同步 log replication
  • 安全性 safety
  • 日志压缩 log compaction
  • 成员变更 membership change

Raft 将系统中的角色分为:

  • Leader 接受客户端请求,并且向 Follower 同步请求日志,当日志同步到大多数节点上后告诉 Follower 提交日志。
  • Follower 接受并持久化 Leader 同步的日志,在 Leader 通知可以提交后提交日志。
  • Candidate 是选举过程中的临时角色。

Raft 要求系统在任何一个时刻最多只有一个 Leader,正常工作期间只有 Leader 和 Follower。
Raft 算法角色状态转换如下:

Follower 只响应其它服务器的请求,如果 Flower 超时没有接受到 Leader 的消息,它会成为一个 Candidate 状态并开始一次 Leader 选举,收到大多数服务器投票的 Candidate 会成为新的 Leader,Leader 在宕机之前会一直保持 Leader 状态。

Raft 算法将时间分为一个个的任期 term,每一个 term 的开始都是 Leader 选举,在成功选举 Leader 之后,Leader 会在整个 term 内管理整个集群,如果 Leader 选举失败,这个 term 就会因为没有 Leader 而结束。

Leader 选举(Leader election)

Raft 使用心跳触发 Leader 选举。当服务器启动时,初始化为 Follower。Leader 向所有 Follower 周期性发送 heartbeat。如果 Follower 选举超时,会等待一段随机时间后再发起一次 Leader选举。选举出 Leader 后,会定期向所有 Follower 发送 heartbeat 维持状态,如果 Follower 一段时间没有收到心跳则认为 Leader 已经挂了,再次发起Leader选举过程。

日志复制 (log replication)

Leader 选举出来后,就开始接收客户端的请求,把日志条目加入到日志处理中,然后并行的向其它服务器发起请求复制日志条目。当这条日志被复制到大多数服务器中,Leader会把这条日志状态改变向客户端返回执行结果。

如果某个Follower没有复制成功,则Leader会无限的重试直到Follower最终存储了所有的日志条目。日志由有序编号和日志条目组成,每条日志条目包含它被创建时的任期号 term,和用于状态机执行的命令。

安全性 (safety)

Raft增加两条极限值来保证安全性:

  1. 拥有最新已提交的log entry 的 Follower 才有资格成为 Leader
  2. Leader只能推进commit index 来提交当前term的已经复制到大多数节点上的日志,旧的term日志会跟随当前term的日志来间接提交。

日志压缩 (log compaction)

通过定期记录 snapshot 来解决,每个副本独立的对自己系统状态进行snapshot,并且是已提交的日志进行。snapshot 包含日志元数据,最后一条已提交的 log entry 的 log index 和 term。Leader会发送snapshot给最后日志太多的Follower,或者新加入的机器。
copy-on-write https://blog.csdn.net/u012501054/article/details/90241124
做一次snapshot可能耗时过长,会影响正常日志同步。可以通过使用copy-on-write技术避免snapshot过程影响正常日志同步。

成员变更 (membership change)

不同节点之间同步成员变更存在间隙,会导致一致性问题。Raft提出两阶段成员变更方法,集群从旧成员配置切换过度成员配置,叫做共同一致,是指旧成员配置和新成员配置组合,一旦共同一致被提交,系统再切换到新成员配置。

Raft与Multi-Paxos的不同:

QA

https://zhuanlan.zhihu.com/p/32052223

Java 类从源码到实例化对象需要经历几个过程

  1. 编写Java源码(.java文件)
  2. 编译成Java字节码(.class文件)
  3. 类加载器读取字节码转换成java.lang.Class实例
  4. JVM 通过 newInstance 等方法创建真正对象

ClassLoader 是 Java 最基本的类加载器,用来实例化不同的类对象。Java类的来源可以有内部自带的核心类$JAVA_HOME/jre/lib/,核心扩展类$JAVA_HOME/jre/lib/ext,动态远程加载的.class文件,分别由不同的 ClassLoader 来协作加载。

在 mac 机器上可以使用 mweb 来写博客,比较好用的地方就是可以直接把剪贴板的图片粘贴上来,缺点是 mac 键盘超难用并且不支持窗口内开启命令行。平时在家的时候都用 Ubuntu 台式机,博客使用 VS Code 编写,一直以来阻挡我的是图片的粘贴特别费劲,今天发现一个很好用的插件 pasteimage,可以直接将剪贴板图片粘贴到 markdown 使用,并且支持配置保存路径。

然后按照教程配置好参数:

1
2
3
4
5
6
{
"pasteImage.path": "${projectRoot}/source/resource/img",
"pasteImage.basePath": "${projectRoot}/source",
"pasteImage.forceUnixStyleSeparator": true,
"pasteImage.prefix": "/"
}

就可以直接将图片粘贴到 markdown 中,其中遇到个问题就是配置不生效,会导致文件直接保存到当前文件目录,具体配置方法可以参考下面连接。

https://www.crifan.com/vscode_how_to_config_setting_plugin/ 这篇文章写的很详细了。
https://github.com/mushanshitiancai/vscode-paste-image 这篇是配置教程,里面有些地方比较容易被误导。

对于Linux系统需要有 xclip 支持,使用的时候会给提示的。

另外记录一下 Ubuntu 的截屏和粘贴快捷键:

1
2
Ctrl + Shift + Print Screen  // 区域截屏到剪贴板
Ctrl + Alt + s // 在 VS Code 中粘贴

在 Chrome 上有个很好用的插件 FeHelper,应该是每个开发人员都在使用的,功能很全面。想着能不能搞个桌面版的,这样多屏环境下可以不用在众多 tab 页中找功能了。

桌面版实现,界面比较丑陋,不过用起来方便就行。这样可以在多屏环境下一个屏用来开啊,另一个屏可以观察辅助信息和使用小工具。

了解到 electron 是一个开源跨平台框架,可以使用 nodejs 做后端和 chromium 做前端开发。像 atom 和 vs code 使用这个框架开发的。觉得还是和方便的,主要是跨平台。而这个插件也是基于 nodejs 开发的,应该可以迁移过来。然后按照官方文档开发,通过 iframe load 不同页面。

项目地址:https://github.com/noogel/xyzToolbox

Mac 安装包下载地址:https://pan.baidu.com/s/1SYjVX2Dhz6TTbaif1Bk8RA 密码:64oo

拉取项目和子模块

1
2
3
git clone https://xxx.git
git submodule init
git submodule update

打包命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
环境安装
npm install

# Linux打包成AppImage文件
# 在Linux环境上执行
node_modules/.bin/electron-builder -l AppImage

# Windows打包成exe安装文件
# 在Windows环境下执行
node_modules/.bin/electron-builder -w nsis
node_modules/.bin/electron-builder -w --ia32 nsis

# 如果在非Windows上打包win程序,也可以借助docker 如下
# docker run --rm -it -v ${PWD}:/project electronuserland/builder:wine sh -c "node_modules/.bin/electron-builder -w nsis"

# Mac打包成dmg文件
# 在Mac环境下执行
node_modules/.bin/electron-builder -m dmg

打包参考链接

https://qii404.me/2019/07/10/electron.html

JVM Runtime Data Area

根据 JVM 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。

线程私有:程序计数器,虚拟机栈,本地方法栈。
线程共享:堆、元空间、直接内存

  1. 元空间(Metaspace),JDK 8 之前 HotSpot 虚拟机使用永久代来实现的方法区(方便内存管理), JDK 8 废弃了永久代,将原来永久代的字符串常量池、静态变量、类型信息等全部移到了元空间中。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

  2. 虚拟机栈(JVM Stacks),每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。

  3. 本地方法栈(Native Method Stack),与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。

  4. 程序计数器(Program Counter Register),程序计数器可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。

  5. 堆内存(Heap),堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在堆上进行分配。jdk1.8后,字符串常量池从永久代中剥离出来,存放在堆中。

  6. 直接内存(Direct Memory),直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

关于方区

GC

  • 引用计数算法,记录对象被引用的次数。
  • 可达性分析算法,通过一系列的“GC Roots” 根对象作为开始节点集,根据引用关系向下搜索,如果某个对象到 GC Roots 间没有任何引用链相连,表明对象不可达。

引用的概念

  • 强引用,普遍的引用赋值。
  • 软引用,在程序将要内存溢出的时候可以进行回收,回收后内存依然不够时则抛出异常。
  • 弱引用,生存周期为下一次垃圾回收为止。
  • 虚引用,被垃圾回收时触发一次系统通知。

垃圾收集算法 - 分代收集理论

  • 内存区域
    • 新生代
    • 老年代
    • 永久代(元空间、方法区)
  • 回收类型
    • 新生代收集
    • 老年代收集
    • 整堆收集
  • 回收算法
    • 标记 - 清除算法:将垃圾对象标记清除。容易造成内存空间碎片化,大对象申请问题,可能触发下一次垃圾收集动作。
    • 标记 - 复制算法:半区复制,浪费空间。新生代 eden 空间、两块 survivor 空间,比例是 8:1,每次只使用 Eden空间和一块survivor空间,进行垃圾回收时会将存活对象复制到另一块 survivor 空间,然后清理掉已经用过的 Eden 空间和 survivor 空间,这样整个新生代利用了 90% 的空间。当一次垃圾回收的存活对象超过一个surivor空间时会通过分配担保机制使用老年代空间。
    • 标记 - 整理算法:清除后将所有存活对象向内存空间一端移动。

经典垃圾收集器

https://juejin.im/post/5e197cc0e51d451c774dc56f

Java8 GC 默认使用的是 Parallel Scavenge (新生代) 和 Parallel Old (老年代)。

GC 日志

在启动命令中增加 -XX:+PrintGCDetails 输出详细GC日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* 堆内存溢出
*/
public class JvmDemo1 {

public static void main(String[] args) {
Random random = new Random();
List<Long> list = new ArrayList<>();
while (true) {
list.add(random.nextLong());
}
}
}
1
2
3
[GC (Allocation Failure) [PSYoungGen: 8097K->1008K(9216K)] 11954K->10351K(19456K), 0.0091685 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] 

[Full GC (Ergonomics) [PSYoungGen: 1008K->495K(9216K)] [ParOldGen: 9343K->9793K(10240K)] 10351K->10288K(19456K), [Metaspace: 3224K->3224K(1056768K)], 0.1013312 secs] [Times: user=0.16 sys=0.00, real=0.10 secs]

GC:表明进行了一次垃圾回收,前面没有Full修饰,表明这是一次Minor GC ,注意它不表示只GC新生代,并且现有的不管是新生代还是老年代都会STW。 Allocation Failure:表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。

JVM

JVM 堆栈配置参数

jps 查看 JVM 进程启动参数

默认元空间大小128M,最大元空间大小256M,初始化堆大小2G,最大堆大小5G,新生代512M,每个线程分配内存大小1M。eden空间和survivor空间的分配比率8:2,使用标记复制算法。

1
2
root@xxx:/usr/lib/jvm/java-8-oracle/bin# ./jps -v
1 xxx.jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -Xms2048m -Xmx5048m -Xmn512m -Xss1024k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails

jmap 生成堆快照

format 指定输出格式,live 指明是活着的对象,file 指定文件名。方便后面通过分析工具分析。

1
jmap -dump:live,format=b,file=dump.hprof pid

jstat 查看 JVM 进程已使用空间百分比

1
2
3
root@xxx:/usr/lib/jvm/java-8-oracle/bin# ./jstat -gcutil 1
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 72.37 40.12 3.59 96.52 94.68 113 4.433 0 0.000 4.433

S0 survivo0
S1 survivo1
E Eden空间
O 老年代
M 元空间使用率
CCS 压缩使用比例
YGC 新生代 GC 次数
YGCT 新生代 GC 耗时
FGC Full GC 次数
FGCT Full GC 耗时
GCT GC 耗时

jmap 查看进程堆的详细信息

jmap -heap pid

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
Attaching to process ID 3764, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.171-b11

using thread-local object allocation.
Parallel GC with 8 thread(s) //采用Parallel GC

Heap Configuration:
MinHeapFreeRatio = 0 //JVM最小空闲比率 可由-XX:MinHeapFreeRatio=<n>参数设置, jvm heap 在使用率小于 n 时 ,heap 进行收缩
MaxHeapFreeRatio = 100 //JVM最大空闲比率 可由-XX:MaxHeapFreeRatio=<n>参数设置, jvm heap 在使用率大于 n 时 ,heap 进行扩张
MaxHeapSize = 2095054848 (1998.0MB) //JVM堆的最大大小 可由-XX:MaxHeapSize=<n>参数设置
NewSize = 44040192 (42.0MB) //JVM新生代的默认大小 可由-XX:NewSize=<n>参数设置
MaxNewSize = 698351616 (666.0MB) //JVM新生代的最大大小 可由-XX:MaxNewSize=<n>参数设置
OldSize = 88080384 (84.0MB) //JVM老生代的默认大小 可由-XX:OldSize=<n>参数设置
NewRatio = 2 //新生代:老生代(的大小)=1:2 可由-XX:NewRatio=<n>参数指定New Generation与Old Generation heap size的比例。
SurvivorRatio = 8 //survivor:eden = 1:8,即survivor space是新生代大小的1/(8+2)[因为有两个survivor区域] 可由-XX:SurvivorRatio=<n>参数设置
MetaspaceSize = 21807104 (20.796875MB) //元空间的默认大小,超过此值就会触发Full GC 可由-XX:MetaspaceSize=<n>参数设置
CompressedClassSpaceSize = 1073741824 (1024.0MB) //类指针压缩空间的默认大小 可由-XX:CompressedClassSpaceSize=<n>参数设置
MaxMetaspaceSize = 17592186044415 MB //元空间的最大大小 可由-XX:MaxMetaspaceSize=<n>参数设置
G1HeapRegionSize = 0 (0.0MB) //使用G1垃圾收集器的时候,堆被分割的大小 可由-XX:G1HeapRegionSize=<n>参数设置

Heap Usage:
PS Young Generation //新生代区域分配情况
Eden Space: //Eden区域分配情况
capacity = 89653248 (85.5MB)
used = 8946488 (8.532035827636719MB)
free = 80706760 (76.96796417236328MB)
9.978989272089729% used
From Space: //其中一个Survivor区域分配情况
capacity = 42467328 (40.5MB)
used = 15497496 (14.779563903808594MB)
free = 26969832 (25.720436096191406MB)
36.49275037977431% used
To Space: //另一个Survivor区域分配情况
capacity = 42991616 (41.0MB)
used = 0 (0.0MB)
free = 42991616 (41.0MB)
0.0% used
PS Old Generation //老生代区域分配情况
capacity = 154664960 (147.5MB)
used = 98556712 (93.99100494384766MB)
free = 56108248 (53.508995056152344MB)
63.722715216167906% used

1819 interned Strings occupying 163384 bytes.

JVM调优

目的:对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,减少系统停顿时间。

步骤:

  1. 监控GC状态
  2. 生成 dump 文件
  3. 分析dump 文件(MAT 工具)
  4. 分析判断是否需要进行优化
    • Minor GC执行时间超过50ms;
    • Minor GC执行频繁,约10秒内一次;
    • Full GC执行时间超过1s;
    • Full GC执行频繁,高于10分钟1次;
  5. 调整GC类型和内存分配
  6. 不断分析调整

可视化分析工具 MAT

https://blog.csdn.net/wwlwwy89/article/details/74330544