当前位置:网站首页>从0开始设计JVM ,忘记名词跟上思路一次搞懂

从0开始设计JVM ,忘记名词跟上思路一次搞懂

2022-08-10 23:47:00 闲猫

必要性

来回看jvm的东西,再拿起来还是记不住,--XX***参数一大堆,各种组件和名词,根本记不住,调优起来缺不清楚怎么设置这些参数。看一些经验行的比例,在解决问题的时候还是不清楚。

原因是你没有正真搞清楚JVM

怎么解决? 你自己设计一个JVM,过一遍逻辑,把JVM中各部分结构化,有机的组合在一块,存在是为了解决问题,妥协是为了平衡优缺点。

需求

有C,C++语言了,也有局限,对平台依赖严重,空间需要自己管理,很麻烦一不小心就遗忘导致了内存泄露。怎么解决?

怎么实现一次实现到处运行

Jvm:Java visual machine。

抽象出字节码,JVM去根据字节码适配到各个平台就行,上面的操作就不需要考虑平台差异性喽。

基于Jvm语言包括:Java,kotlin,Scala,Clojure,Groovy,Jython,JRuby,Ceylon,Eta,Haxe 

加载和执行字节码需要啥

需求:

字节码就是执行的逻辑,加载到内存,需要所有人都可以看的到

静态变量是公用的,放哪呢

根据类创建的对象,这部分得可以共享,否则一个用户干点事所有的基础对象都得自己创建。对象包括放对象引用的地址信息,对象数据空间。

每个用户都有自己的隐私,也需要给每个用户独占的一块空间

最后是控制代码执行的指针,即:现在代码执行到那了,也需要保存下来。对单核机器来说,当前需要执行的指针只有一个,如果要挂起就保存起来,用当前执行的用户的指针就行

上面都是静态的空间,下来说动的部分:

首先需要加载字节码,其次是需要根据字节码执行代码,分配空间,创建线程,按照方法调用逻辑执行,执行的过程中对没用的垃圾对象要及时回收

组件架构图:

蓝色:线程独占

橙色:共享

加载过程

有一个类来将class文件加载到内存中,放在方法区域中

安全检查这个类,确保加载的类符合 JVM 规范和安全

如果没问题,就给静态变量分配空间,并初始化,这类数据是类唯一的,可以也放在方法区内。

如果static String a=”abc”,需要将”abc”放在常量池中,并将a赋值常量池值”abc”的引用。

这时这个类就就绪了。

卸载,GC将字节码删除的过程

一个和一批加载自然不同,比如:java本身提供一些类,如果你的类和java的类重名了咋办,如果没有啥规则就乱套了。

再一个,如果java的字节码,你自己写个类加载器,那加载到内存中谁知道出现啥问题,你也不清楚人家底层呀,更模板模式一样,即使允许你自己写类加载器,也只给你预留部分你自己可以实现的部分才可以。

重名咋办:那优先用java提供的

类加载器:java提供的类由Java自己的类加载器加载;自己写的类,java给个默认的加载器,也可以自定义一个加载器,但必须是继承人家的ClassLoader类,只有部分方法可以自定义。

垃圾对象回收

垃圾回收:1. 哪些算是垃圾;2. 怎么回收。

垃圾认定:程序没法找到这个对象,自然就没用了,表现为这个对象的引用是否被别人保留,没有引用也就没法找到该对象了。或者给每个对象搞一个计数器,谁用记录,最后计数器为0就没人用了。

怎么回收:

  1. 标记清除:检查可到达时标记;回收的时候将数据擦除进行回收。一大块空间用过一遍就会很多碎片产生,但简单基本不动
  2. 标记复制:分配的时候挨个分配空间,检查可到达并标记垃圾对象,再拿出一块空间将有用的对象赋值过去。需要赋值就需要一半的空间冗余存储,不能分配,否则就没法复制了。每次都得复制全部对象,即使啥没动也得复制一遍。
  3. 标记压缩:同样,刚开始标记好,内存是一条线,将有用对象从0x0开始码齐。最坏的情况是复制全部。

实际逻辑:标记操作同时进行,从根(GCRoot)开始,找有用的对象,找到就改复制复制,该压缩压缩。

 

 

结合实际设计:任何一种压缩算法都不是最优的,最好情况是集成所有算法的有点。那就需要对对象分类了,一个进程启动后启动类对象基本不变,方法中局部变量用完就得回收,线程之间共享的对象就不确定啥时候该回收了。很容易分为:确定永久不变的;王八级;朝生夕死级;不确定的生存时间的。 还有一个维度就是大小,大对象很很好性能,尽可能不动,动的时候尽可能就是回收(只复制活对象,垃圾是不动的)。

除了从语法上就可以看出永久不变的,直接放在一个区域。

其他其实都是存活时间[一次gc,无穷大),可以gc抽象为过年,每一次gc没有死掉的,对象长一岁。那这个节点怎么确定,基于统计数据吧。

  1. 1岁以下,即第一次gc就回收的
  2. 老年定义,15岁(默认)
  3. 1-15岁对象

 详细逻辑分析:

  1. 婴儿区Eden-中间年龄:标记复制算法,空间不够用了,就GC将有用的对象符合到中间区域
  2. 中间区域Survivor:按照没有分为两个算,现在Eden要复制一批到Survivor区域,如果空间够就在后边分配就行,如果不够Survivor也需要进行压缩下,再在后边接着分配。那这次Survivor算不算增加一岁呢。那如果Survivor即使压缩空间后空间也不够呢,怎么复制过去。
  3. 如果分为两个就好多了:A-(Eden + From)作为复制起始,复制有用的对象到To空间,From中超过年龄的直接复制到Old中,如果Eden + From)>To,为了可用将稍微老一点的对象放在Old中。复制完,交换From和To,继续在Eden空间不够用的时候触发Gc功能。
  4. Old空间呢:只有当From复制超过15岁的对象没有空间给分配了,才进行GC。Old中放有长时间存在,或者大对象。如果也来个复制算法,太浪费空间了。如果直接进行压缩,浪费性能,想想中间有一个大对象没用了,还有必要把后面的压缩起来吗?直接分配就可以了,所以用标记清理算法。当然会产生碎片,当所有碎片,包括后面也没整块的空间了,那就需要来一次压缩了,没法子必须保证可用吧。

持久区域就没必要花了,反正也不GC

两个问题:

  1. 各个区域大小怎么划分
  2. 多少岁还是老年了

直接给答案吧:

  1. Eden:From:To = 8:1:1
  2. >=15 就算Old了

我个人意见是统计出来的,有人说15是因为存储空间原因,我不以为然,如果统计数据是32岁最合适,我就不信JVM不会设计一个超过4b的存储空间来存储这个值。


参考

(1条消息) GC分代年龄为什么是15?

JVM内存模型(通俗易懂)_抵制平庸 拥抱变化的博客-CSDN博客_jvm内存模型

原网站

版权声明
本文为[闲猫]所创,转载请带上原文链接,感谢
https://blog.csdn.net/weixin_42754896/article/details/126232227