pull/1/head
595208882@qq.com 3 years ago
parent 45ab312089
commit 5f8d08119a

@ -1723,6 +1723,261 @@ LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在
**如何实现LRUCache**
LRULeast Recently Used**最近最少使用算法**。LruCache就是利用 LinkedHashMap 的一个特性( `accessOrdertrue`基于访问顺序 )再加上对 LinkedHashMap 的`数据操作上锁`实现的缓存策略。
- `LruCache`是通过`LinkedHashMap`构造方法的第三个参数的`accessOrder=true`实现了`LinkedHashMap`的数据排序基于访问顺序 最近访问的数据会在链表尾部在容量溢出的时候将链表头部的数据移除从而实现了LRU数据缓存机制
- 然后在每次 `LruCache.get(K key)` 方法里都会调用`LinkedHashMap.get(Object key)`
- 如上述设置了 `accessOrder=true` 后,每次 `LinkedHashMap.get(Object key)`都会进行 `LinkedHashMap.makeTail(LinkedEntry<K, V> e)`
- LinkedHashMap 是`双向循环链表`,然后每次 `LruCache.get -> LinkedHashMap.get` 的数据就被放到最末尾了
- 在 put 和 `trimToSize` 的方法执行下,如果发生数据量移除,会优先移除掉最前面的数据(因为最新访问的数据在尾部)
- LruCache 在内部的get、put、remove包括 trimToSize 都是安全的(因为都上锁了)
- LruCache 自身并没有释放内存将LinkedHashMap的数据移除了如果数据还在别的地方被引用了还是有泄漏问题还需要手动释放内存
- 覆写entryRemoved方法能知道 LruCache 数据移除时是否发生了冲突,也可以去手动释放资源
- maxSize 和 sizeOf(K key, V value) 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位
#### LruCache的唯一构造方法
```java
/**
* LruCache的构造方法需要传入最大缓存个数
*/
public LruCache(int maxSize) {
...
this.maxSize = maxSize;
/*
* 初始化LinkedHashMap
* 第一个参数initialCapacity初始大小
* 第二个参数loadFactor负载因子=0.75f即到75%容量的时候就会扩容
* 第三个参数①accessOrder=true基于访问顺序排序②accessOrder=false基于插入顺序排序
*/
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
```
#### LruCache.get(K key)
下述的 get 方法表面并没有看出哪里有实现了 LRU 的缓存策略。主要在 mapValue = map.get(key);里,调用了 LinkedHashMap 的 get 方法,再加上 LruCache 构造里默认设置 LinkedHashMap 的 accessOrder=true。
```java
/**
* 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。
* 如果值返回了,那么它将被移动到双向循环链表的的尾部。
* 如果如果没有缓存的值,则返回 null。
*/
public final V get(K key) {
...
V mapValue;
synchronized (this) {
// 关键点LinkedHashMap每次get都会基于访问顺序来重整数据顺序
mapValue = map.get(key);
// 计算 命中次数
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 计算 丢失次数
missCount++;
}
/*
* 官方解释:
* 尝试创建一个值这可能需要很长时间并且Map可能在create()返回的值时有所不同。如果在create()执行的时
* 候一个冲突的值被添加到Map我们在Map中删除这个值释放被创造的值。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
/***************************
* 不覆写create方法走不到下面 *
***************************/
/*
* 正常情况走不到这里
* 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
* 因为默认的 create(K key) 逻辑为null
*/
synchronized (this) {
// 记录 create 的次数
createCount++;
// 将自定义create创建的值放入LinkedHashMap中如果key已经存在会返回 之前相同key 的值
mapValue = map.put(key, createdValue);
// 如果之前存在相同key的value即有冲突。
if (mapValue != null) {
/*
* 有冲突
* 所以 撤销 刚才的 操作
* 将 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
} else {
// 拿到键值对,计算出在容量中的相对长度,然后加上
size += safeSizeOf(key, createdValue);
}
}
// 如果上面 判断出了 将要放入的值发生冲突
if (mapValue != null) {
/*
* 刚才create的值被删除了原来的 之前相同key 的值被重新添加回去了
* 告诉 自定义 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 上面 进行了 size += 操作 所以这里要重整长度
trimToSize(maxSize);
return createdValue;
}
}
```
#### LinkedHashMap.get(Object key)
主要看`if (accessOrder)`逻辑即可,如果`accessOrder=true`那么每次get都会执行N次 `makeTail(LinkedEntry<K, V> e)` 。多了一步mainTail动作把获取的数据移到双向链表的尾部tail。
```java
/**
* Returns the value of the mapping with the specified key.
*
* @param key the key.
* @return the value of the mapping with the specified key, or {@code null} if no mapping for the specified key is found.
*/
@Override
public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e); //把访问的节点迁移到链表的尾部
return e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { //从数组中获取
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e); //把访问的节点迁移到链表尾部。
return e.value;
}
}
return null;
}
/**
* Relinks the given entry to the tail of the list. Under access ordering,
* this method is invoked whenever the value of a pre-existing entry is
* read by Map.get or modified by Map.put.
*/
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e 在链表中删除该节点e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;
// Relink e as tail 在尾部添加
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}
```
#### LruCache.put(K key, V value)
```java
public final V put(K key, V value) {
...
synchronized (this) {
...
// 拿到键值对,计算出在容量中的相对长度,然后加上
size += safeSizeOf(key, value);
...
}
...
trimToSize(maxSize);
return previous;
}
```
注意:
- `put` 开始的时候确实是把值放入 `LinkedHashMap` 了,不管超不超过你设定的缓存容量
- 然后根据 `safeSizeOf` 方法计算 此次添加数据的容量是多少,并且加到 `size`
- 说到 `safeSizeOf` 就要讲到 `sizeOf(K key, V value)` 会计算出此次添加数据的大小
- 直到 `put` 要结束时,进行了 `trimToSize` 才判断 `size` 是否 大于 `maxSize` 然后进行最近很少访问数据的移除
#### LruCache.trimToSize(int maxSize)
会判断之前 `size` 是否大于 `maxSize` 。是的话,直接跳出后什么也不做。不是的话,证明已经溢出容量了。由 `makeTail` 图已知,最近经常访问的数据在最末尾。拿到一个存放 key 的 Set然后一直一直从头开始删除删一个判断是否溢出直到没有溢出。
```java
public void trimToSize(int maxSize) {
/*
* 这是一个死循环,
* 1.只有 扩容 的情况下能立即跳出
* 2.非扩容的情况下map的数据会一个一个删除直到map里没有值了就会跳出
*/
while (true) {
K key;
V value;
synchronized (this) {
// 在重新调整容量大小前,本身容量就为空的话,会出异常的。
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
// 如果是 扩容 或者 map为空了就会中断因为扩容不会涉及到丢弃数据的情况
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
// 拿到键值对,计算出在容量中的相对长度,然后减去。
size -= safeSizeOf(key, value);
// 添加一次收回次数
evictionCount++;
}
/*
* 将最后一次删除的最少访问数据回调出去
*/
entryRemoved(true, key, value, null);
}
}
```
### ConcurrentHashMap
ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap)它继承于AbstractMap类并且实现ConcurrentMap接口。ConcurrentHashMap是通过“锁分段”来实现的它支持并发。

213
JVM.md

@ -1142,6 +1142,20 @@ CMS垃圾收集器JVM参数最佳实践
**test、stage 环境jvm使用CMS 参数配置jdk8**
```shell
-server -Xms256M -Xmx256M -Xss512k -Xmn96M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -XX:InitialHeapSize=256M -XX:MaxHeapSize=256M -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=2 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSParallelRemarkEnabled -XX:+UnlockDiagnosticVMOptions -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -XX:MaxTenuringThreshold=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:SurvivorRatio=8 -Xloggc:../logs/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../dump
```
**online 环境jvm使用CMS参数配置jdk8**
```shell
-server -Xms4G -Xmx4G -Xss512k -Xmn1536M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -XX:InitialHeapSize=4G -XX:MaxHeapSize=4G -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=2 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSParallelRemarkEnabled -XX:+UnlockDiagnosticVMOptions -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -XX:MaxTenuringThreshold=10 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:SurvivorRatio=8 -Xloggc:../logs/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../dump
```
## GC场景
### Full GC场景
@ -1464,18 +1478,17 @@ du -sh *
## CPU过高排查
然后就是排查CPU的飙高的原因**CPU飙高的排查都是直接找到对应CPU占比最高的进程然后找到CPU最高的线程**。
总结一下可能导致CPU标高的原因可能是**一个GC线程频繁或者锁资源竞争频繁线程数过多**等原因。
**排查过程**
- **GC线程频繁**
- **锁竞争频繁(自旋)**
- 使用`top`查找进程id
- 使用`top -Hp <pid>`查找进程中耗cpu比较高的线程id
- 使用`printf %x <pid>`将线程id十进制转十六进制
- 使用` jstack -pid | grep -A 20 <pid>`过滤出线程id锁关联的栈信息
- 根据栈信息中的调用链定位业务代码
其中GC线程频繁有可能是**大对象(对象过多),内存泄漏**等原因导致内存紧张一直在执行GC但是每次执行的GC回收的垃圾都非常少。
一般CPU紧张都是线上实施排查并且一般大厂都会有自己自研的监控平台我们自己的监控平台对于我们每台服务器的健康状况健康分、服务期内的应用Mysql、Redis、Mq、Kafka、服务都会进行实施的监控报警所以一般都能都在出现问题前将问题解决掉。
在线上之间也提到过可以使用**top**、**jstack**命令排查CPU飙高的问题。这里有一段案例代码如下:
案例代码如下:
```java
public class CPUSoaring {
@ -1506,25 +1519,58 @@ public class CPUSoaring {
}
```
1首先通过**top**命令可以查看到id为**3806**的进程所占的CPU最高
- 第一步:首先通过**top**命令可以查看到id为**3806**的进程所占的CPU最高
![CPU过高排查-top](images/JVM/CPU过高排查-top.jpg)
- 第二步:然后通过**top -Hp pid**命令找到占用CPU最高的线程
![CPU过高排查-top-Hp-pid](images/JVM/CPU过高排查-top-Hp-pid.jpg)
- 第三步:接着通过:**printf '%x\n' tid**命令将线程的tid转换为十六进制xid
![CPU过高排查-printf](images/JVM/CPU过高排查-printf.jpg)
- 第四步:最后通过:**jstack pid|grep xid -A 30**命令就是输出线程的堆栈信息,线程所在的位置:
![CPU过高排查-jstack](images/JVM/CPU过高排查-jstack.jpg)
- 第五步:还可以通过**jstack -l pid > 文件名称.txt** 命令将线程堆栈信息输出到文件,线下查看。
这就是一个CPU飙高的排查过程目的就是要**找到占用CPU最高的线程所在的位置**,然后就是**review**你的代码定位到问题的所在。使用Arthas的工具排查也是一样的首先要使用top命令找到占用CPU最高的Java进程然后使用Arthas进入该进程内**使用dashboard命令排查占用CPU最高的线程。**,最后通过**thread**命令线程的信息。
## 内存打满排查
![CPU过高排查-top](images/JVM/CPU过高排查-top.jpg)
**排查过程**
2然后通过**top -Hp pid**命令找到占用CPU最高的线程
- 查找进程id`top -d 2 -c`
- 查看JVM堆内存分配情况`jmap -heap pid`
- 查看占用内存比较多的对象:`jmap -histo pid | head -n 100`
- 查看占用内存比较多的存活对象:`jmap -histo:live pid | head -n 100`
![CPU过高排查-top-Hp-pid](images/JVM/CPU过高排查-top-Hp-pid.jpg)
3接着通过**printf '%x\n' tid**命令将线程的tid转换为十六进制xid
![CPU过高排查-printf](images/JVM/CPU过高排查-printf.jpg)
**示例**
4最后通过**jstack pid|grep xid -A 30**命令就是输出线程的堆栈信息,线程所在的位置:
- 第一步top -d 2 -c
![CPU过高排查-jstack](images/JVM/CPU过高排查-jstack.jpg)
![img](images/JVM/20200119164639576.png)
5还可以通过**jstack -l pid > 文件名称.txt** 命令将线程堆栈信息输出到文件,线下查看。
- 第二步jmap -heap 8338
这就是一个CPU飙高的排查过程目的就是要**找到占用CPU最高的线程所在的位置**,然后就是**review**你的代码定位到问题的所在。使用Arthas的工具排查也是一样的首先要使用top命令找到占用CPU最高的Java进程然后使用Arthas进入该进程内**使用dashboard命令排查占用CPU最高的线程。**,最后通过**thread**命令线程的信息。
![img](images/JVM/20200119164641390.png)
- 第三步:定位占用内存比价多的对象
![img](images/JVM/20200119164633443.png)
这里就能看到对象个数以及对象大小……
![img](images/JVM/20200119164640148.png)
这里看到一个自定义的类,这样我们就定位到具体对象,看看这个对象在那些地方有使用、为何会有大量的对象存在。
@ -1606,6 +1652,137 @@ class OOM {
## Jvisualvm
项目频繁YGC 、FGC问题排查
### 内存问题
对象内存占用、实例个数监控
![img](images/JVM/20200119164751943.png)
对象内存占用、年龄值监控
![img](images/JVM/2020011916475242.png)
通过上面两张图发现这些对象占用内存比较大而且存活时间也是比较常所以survivor 中的空间被这些对象占用而如果缓存再次刷新则会创建同样大小对象来替换老数据这时发现eden内存空间不足就会触发yonggc 如果yonggc 结束后发现eden空间还是不够则会直接放到老年代所以这样就产生了大对象的提前晋升导致fgc增加……
**优化办法**:优化两个缓存对象,将缓存对象大小减小。优化一下两个对象,缓存关键信息!
### CPU耗时问题排查
Cpu使用耗时监控
![img](images/JVM/20200119164752266.png)
耗时、调用次数监控:
![img](images/JVM/20200119164749513.png)
从上面监控图可以看到主要耗时还是在网络请求没有看到具体业务代码消耗过错cpu……
## 调优堆内存分配
**初始堆空间大小设置**
- 使用系统默认配置在系统稳定运行一段时间后查看记录内存使用情况Eden、survivor0 、survivor1 、old、metaspace
- 按照通用法则通过gc信息分配调整大小整个堆大小是Full GC后老年代空间占用大小的3-4倍
- 老年代大小为Full GC后老年代空间占用大小的2-3倍
- 新生代大小为Full GC后老年代空间占用大小的1-1.5倍
- 元数据空间大小为Full GC后元数据空间占用大小的1.2-1.5倍
活跃数大小是应用程序运行在稳定态时长期存活的对象在java堆中占用的空间大小。也就是在应用趋于稳太时FullGC之后Java堆中存活对象占用空间大小。注意在jdk8中将jdk7中的永久代改为元数据区metaspace 使用的物理内存,不占用堆内存)
**堆大小调整的着手点、分析点**
- 统计Minor GC 持续时间
- 统计Minor GC 的次数
- 统计Full GC的最长持续时间
- 统计最差情况下Full GC频率
- 统计GC持续时间和频率对优化堆的大小是主要着手点我们按照业务系统对延迟和吞吐量的需求在按照这些分析我们可以进行各个区大小的调整
## 年轻代调优
**年轻代调优规则**
- 老年代空间大小不应该小于活跃数大小1.5倍。老年代空间大小应为老年代活跃数2-3倍
- 新生代空间至少为java堆内存大小的10% 。新生代空间大小应为1-1.5倍的老年代活跃数
- 在调小年轻代空间时应保持老年代空间不变
MinorGC是收集eden+from survivor 区域的当业务系统匀速生成对象的时候如果年轻带分配内存偏小会发生频繁的MinorGC如果分配内存过大则会导致MinorGC停顿时间过长无法满足业务延迟性要求。所以按照堆分配空间分配之后分析gc日志看看MinorGC的频率和停顿时间是否满足业务要求。
- **MinorGC频繁原因**
MinorGC 比较频繁说明eden内存分配过小在恒定的对象产出的情况下很快无空闲空间来存放新对象所以产生了MinorGC,所以eden区间需要调大。
- **年轻代大小调整**
Eden调整到多大那我们可以查看GC日志查看业务多长时间填满了eden空间然后按照业务能承受的收集频率来计算eden空间大小。比如eden空间大小为128M每5秒收集一次则我们为了达到10秒收集一次则可以调大eden空间为256M这样能降低收集频率。年轻代调大的同时相应的也要调大老年代否则有可能会出现频繁的concurrent model failed 从而导致Full GC 。
- **MinorGC停顿时间过长**
MinorGC 收集过程是要产生STW的。如果年轻代空间太大则gc收集时耗时比较大所以我们按业务对停顿的要求减少内存比如现在一次MinorGC 耗时12.8毫秒eden内存大小192M ,我们为了减少MinorGC 耗时我们要减少内存。比如我们MinorGC 耗时标准为10毫秒这样耗时减少16.6% 同样年轻代内存也要减少16.6% 即192*0.1661 = 31.89M 。年轻代内存为192-31.89=160.11M,在减少年轻代大小而要保持老年代大小不变则要减少堆内存大小至512-31.89=480.11M
堆内存512M 年轻代: 192M 收集11次耗时141毫秒 12.82毫秒/次
![img](images/JVM/20200119164911457.png)
堆内存512M 年轻代192M 收集12次耗时151毫秒 12.85毫秒/次
![img](images/JVM/20200119164911559.png)
**按照上面计算调优**
堆内存: 480M 年轻带: 160M 收集14次 耗时154毫秒 11毫秒/次 相比之前的 12.82毫秒/次 停顿时间减少1.82毫秒
![img](images/JVM/20200119164911817.png)
**但是还没达到10毫秒的要求继续按照这样的逻辑进行 11-10=1 1/11= 0.909 即 0.09 所以耗时还要降低9%。**
年轻代减少160*0.09 = 14.545=14.55 M; 160-14.55 =145.45=145M
堆大小: 480-14.55 = 465.45=465M
但是在这样调整后使用jmap -heap 查看的时候年轻代大小和实际配置数据有出入年轻代大小为150M大于配置的145M这是因为-XX:NewRatio 默认2 即年轻代和老年代12的关系所以这里将-XX:NewRatio 设置为3 即年轻代、老年大小比为13 ,最终堆内存大小为:
![img](images/JVM/20200119164913228.png)
MinorGC耗时 159/16=9.93毫秒
![img](images/JVM/20200119164912847.png)
MinorGC耗时 185/18=10.277=10.28毫秒
![img](images/JVM/2020011916490666.png)
MinorGC耗时 205/20=10.25毫秒
![img](images/JVM/20200119164912355.png)
Ok 这样MinorGC停顿时间过长问题解决MinorGC要么比较频繁要么停顿时间比较长解决这个问题就是调整年轻代大小但是调整的时候还是要遵守这些规则。
## 老年代调优
按照同样的思路对老年代进行调优同样分析FullGC 频率和停顿时间,按照优化设定的目标进行老年代大小调整。
**老年代调优流程**
- 分析每次MinorGC 之后老年代空间占用变化计算每次MinorGC之后晋升到老年代的对象大小
- 按照MinorGC频率和晋升老年代对象大小计算提升率即每秒钟能有多少对象晋升到老年代
- FullGC之后统计老年代空间被占用大小计算老年带空闲空间再按照第2部计算的晋升率计算该老年代空闲空间多久会被填满而再次发生FullGC同样观察FullGC 日志信息计算FullGC频率如果频率过高则可以增大老年代空间大小老解决增大老年代空间大小应保持年轻代空间大小不变
- 如果在FullGC 频率满足优化目标而停顿时间比较长的情况下可以考虑使用CMS、G1收集器。并发收集减少停顿时间
## 栈溢出
栈溢出异常的排查(包括**虚拟机栈、本地方法栈**基本和OOM的一场排查是一样的导出异常的堆栈信息然后使用mat或者Visual VM工具进行线下分析找到出现异常的代码或者方法。

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Loading…
Cancel
Save