1, 为啥要学习jvm
我们知道一个 Java 应用部署在线上机器上,肯定时不时会出现问题。除去网络、系统本身问题,很多时候 Java 应用出现问题,就是 Java 虚拟机的内存出现了问题。要么是内存溢出了,要么是 GC 频繁导致响应慢等等。 那如何解决这些问题呢?首先,你必须学会看懂日志吧。那么你就必须要看得懂 GC 日志,这是 Java 虚拟机内容的一部分。你看懂了 GC 日志,那么你就得明白什么是年轻代、老年代、永久代、元数据区等,这些就是 Java 虚拟机的内存模型。你懂了 Java 虚拟机的内存模型,那你就得知道 Java 虚拟机是如何进行垃圾回收的,它们使用的垃圾回收算法是怎样的,它们有何优缺点。接下来就是各种垃圾回收器的特性。 你看,这一切东西都是相关联的。你想要解决线上的 Java 应用崩溃问题,那么你就必须学会 GC 日志。要看懂 GC 日志,就必须学习 Java 虚拟机内存模型。要看懂 Java 虚拟机内存模型,你就要学会垃圾回收机制等等。
2, 怎么学习jvm
在行业中jvm 最厉害的是周志明的深入理解java虚拟机。 目前是第三版, 大家可以看看。写的非常的棒。 但是周大佬的jvm的书很晦涩难懂。 大家在平常做业务开发的时候也没有时间去阅读。 所以要是能够图像化,动态化就很好。 这里我在最大的同性交友的网站找到了一个比较好的jvm github.com/visualizit/… 。 我把他分享一下, 跟大家演示解说一下。 写的不好, 请多多指教。
3. 下载代码,解说代码层次结构
我们看到他时一个典型的springboot 的web 项目 , 前后端不分离的那种。
两个视图层的类, 一个是新增对象实体入参, 一个是gc的操作
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class NewRequest {
private int size;
private int count = 1;
private int randomSizeMax;
private int delay = 100; //延迟多少毫秒
private boolean debug = false;
private GcChoice gcChoice = GcChoice.YOUNG;// gc 的类型
}
// 新new 对象的接口
@RequestMapping(value = "/new", method = RequestMethod.POST)
public @ResponseBody
List<ObjectBO> newObjects(@RequestBody NewRequest request) {
Heap heap = this.generationalHeap;
if (request.getGcChoice() == GcChoice.G1_YOUNG) {
heap = this.g1Heap;
}
return gcSupervisor.newObjects(heap, request);
}
//开始启动的操作
@RequestMapping(value = "/debug/go", method = RequestMethod.POST)
public @ResponseBody
Boolean go() {
return gcSupervisor.go();
}
// 暂停的操作
@RequestMapping(value = "/debug/step", method = RequestMethod.POST)
public @ResponseBody
Boolean step() {
return gcSupervisor.step();
}
//取消的操作
@RequestMapping(value = "/debug/pause", method = RequestMethod.POST)
public @ResponseBody
Boolean pause() {
gcSupervisor.setDebug(true);
return true;
}
//停止的操作
@RequestMapping(value = "/debug/stop", method = RequestMethod.POST)
public @ResponseBody
void stop() {
gcSupervisor.stop();
}
还有websocker 的长链接。 视图model 层就是 一些gc 参数, 对象参数 对应的实体。 这些个实体包括 年轻代。伊甸园区,老年代, 对象引用, 复制,堆,动态链接等。 还是很全的, 看来作者很用心。 这里面最重要的就是jvm config
public class JvmConfig {
public static final int Xms = 300; // minimum heap size
public static final int Xmx = Xms; // maximum heap size
public static final int SurvivorRatio = 8; // survivor:eden = 1:8
public static final int NewRatio = 2;// young:old = 1:2
public static final int MaxTenuringThreshold = 3;// default: 15
private static final int G1HeapRegionSize = 25;
// 年轻代所占堆内存的大小
public static final int getYoungSize() {
return Xmx * 1 / (JvmConfig.NewRatio + 1);
}
//老年代所占堆内存的大小
public static final int getOldSize() {
return Xmx - getYoungSize();
}
// 伊甸园的大小
public static final int getEdenSize() {
return getYoungSize() * JvmConfig.SurvivorRatio / (JvmConfig.SurvivorRatio + 2);
}
public static final int getRegionSize() {
return G1HeapRegionSize;
}
// 幸存者区的大小,这里不区分 幸存者1 区,和二区。
public static final int getSurvivorSize() {
return (getYoungSize() - getEdenSize()) / 2;
}
}
4,动态演示
从代码结构来看 前后端的通讯使用的是websock,后端是springboot +websockt stomp 前端是websocket js .大家可以下载代码看看。 我们下载项目跑起来
上面的垃圾回收算法,有三个,我们来看第一个默认的年轻代标记复制算法。
我们先来运行一下看一下动画效果。
我们看到年轻代分为伊甸园区,幸存者0区,幸存者1 区。
这时候我们看一下代码中jjvm 的配置
最大对象包括年轻代和老年代=300,
幸存者和伊甸园的比例是1:8,
老年代和年轻代的比例是1:2
由此我们可以得出
伊甸园80
幸存者0:10
幸存者1:10
老年代:200
从图上伊甸园的格子8X10 我们也能看出和我们的猜想是一样的。
首先伊甸园的格子数值是对象的编号,下面幸存者0 和1区的 格子里面有两个数值,第二个是年龄。也就是被回收一次,年龄加一,超过最大年龄就送到老年代。 默认是15 ,现在是3.
我们再接着往下面跑。
我们看到对象,从su0 到su1 , 并且38 号对象被回收两次。所以他的年龄就是2
我们再跑一下
这时候我们再看到38号对象年龄达到了三次,进入了老年代。 因为上一次的144,82,135 在新的一小gc 的时候没有被引用,直接被清理了。所以他们就消失了,只要多次被引用的才会在so-s1 相互复制,最后到old 去。 这个引用关系有两种,可达性算法,和引用计数法。这里就不多说了,大家自己回去看看书吧。
今天的标记复制算法就说到这里了, 记住标记复制算法主要发生在年轻代,因为年轻代的对象多,产生的快。使用标记复制算法最大可用90%的空间。
一个伊甸园区加上幸存者区, 但是这个需要老年代的分配担保。
要是伊甸园区不够,全部进入老年代就直接引发fullgc .
总之jvm 还是挺有趣的,为了方便大家学习,我搞了一个在线的,公众号分享干货的你回复jvm就行。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。