jVM总结

作者: 爱看书的独角兽 | 来源:发表于2022-06-20 19:01 被阅读0次
    JVM知识框架.png

    1.类加载过程

    话不多少,先上图!


    类加载过程.png
    public class User {
        private String username;
    
        private String password;
    
        private String avater;
    
        private Integer age;
    
        private UserInfo userInfo;
    }
    
    //用户详细信息  垂直结构设计
    public class UserInfo {
    
        private String telephone;
    
        private String address;
    
        //......
    }
    

    首先在讲之前宏观上我们要理解一点,JVM本身是运行在操作系统上的,它在启动时会向操作系统申请一片内存空间供其自身使用,所以以下我说的内存都是JVM虚拟机中的内存!

    从图上来了解,类加载的一般过程分为:
    加载 —》验证—》准备—》解析—》初始化—》使用—》卸载
    加载:jvm在启动时,从主函数main入口进入,首先加载User这个类文件进入内存,然后在加载User类的时候发现UserInfo这个类,这时会先加载UserInfo.class进入内存,然后再加载User.class文件。
    验证:根据JVM虚拟机规范,来校验加载进来的class文件内容,判断是否符合规范
    准备阶段:验证通过之后,接下来就是给类分配内存空间,并且对类中的变量给定一个默认值
    解析:这个阶段主要是把类中的一些符号替换成直接引用
    初始化(核心):对变量进行赋值操作
    这里存在一个问题:类是在什么时候进行初始化的呢?答案是在使用new关键字时,此时会创建一个对象实例,也就是初始化。

    说完过程,下面我们来详细讲解一下类是如何加载的吧!
    老规矩,先来介绍几个名词:
    类加载器:负责加载类资源的组件,主要包括启动类加载器、扩展类加载器、引用程序加载器(不排除用户创建自定义加载器),每个加载器都有各自的任务分工。
    启动类加载器:Bootstrap ClassLoader,主要负责加载Java目录下的核心类(lib目录下的类)的
    扩展类加载器:Extension ClassLoader,这个类加载器其实也是类似的,负责加载 Java 安装目录下的“lib\ext”文件中的类
    应用程序加载器:顾名思义,负责加载我们写好的Class类
    双亲委派机制:Application ClassLoader,JVM中的类加载器是有亲子层级关系的,按照上面介绍顺序依次向下,所以基于这种亲子层级关系,产生了双亲委派的方式去加载类。具体来说,假设启动类加载器去加载一个类,首先会委派自己的父类去加载,最后会传导到最顶层的启动类加载器,加载器根据自己的职能分工去对应的目录中加载相关的类,如果自身没有加载,则会下推权限交给子类进行加载,直到加载完成
    概念介绍完了,简单总结就是每个加载器有各自的职能分工,在加载类的时候按照固定规则进行加载。
    那么读者肯定会有一个疑问,为什么要使用双亲委派这种规则呢?
    解答一下!想象一个场景,当我们自己定义一个String类时,JVM会出现什么情况呢?大家可以动手操作一下,看看编译器报什么错,这里我就不演示了。之所以设计这种机制,主要是避免多层级加载器加载重复的类,保证Java API的核心类不被修改,形成沙箱安全。
    扩展:Tomcat也是一种JVM,但是他的类加载机制是不遵循双亲加载机制的,Tomcat自定义了很多内置的加载器,如webApp classLoaderer,Share ClassLoader,catalina ClassLoader等,并没有使用扩展加载器和启动类加载器。

    2.内存模型

    还是先上图!

    jvm内存模型.png
    在类都被加载到内存之后,我们就必须了解这个jvm内部是如何处置这些文件的,如图所示,在JVM中,主要由五大组件构成——堆、方法区、虚拟机栈、本地方法栈、程序计数器。
    所以,要了解JVM的内存模型,主要是了解这五大组件。

    1.堆(重点)

    这个区域存放的是我们创建的各种对象实例,是线程共享的区域,几乎所有类的实例和数组分配的内存都来自于它,而在堆中,可以细分为新生代、老生代。新生代中有分为eden区,幸存者区(from区,to区)这部分与GC有关,我们后面会细讲,总的来说,这是JVM中占内存较大的一块,所以我们进行GC时一般在堆中。注意:在java1.8之前,堆还有个永久代,1.8之后永久代更名为元空间,直接放在操作系统的内存中了

    2.方法区

    方法区存放的一般就是我们加载进来的类元信息(类信息、字段信息、方法信息,常量),运行时常量池等,在1.7之前,还包括字符串常量池、静态变量,1.7之后迁移到了堆中。(主要考虑也是GC的效率问题)

    3.虚拟机栈

    线程私有的,由一个个栈帧组成。线程中调用方法时会形成一个栈帧压入虚拟机栈中,栈帧的组成部分有:局部变量表、操作数栈、动态链接、方法返回地址。
    局部变量表:主要存放了编译期可知的各种数据类型,对象引用(句柄引用、直接引用)
    操作数栈:主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果
    动态链接:主要服务一个方法需要调用其他方法的场景。
    方法返回地址:return语句返回的程序指令地址

    本地方法栈

    线程私有;虚拟机栈一般运行的java代码方法,而我们知道,在java中有些方法属于本地方法,用native关键词修饰的,这些方法都是用C语言写的。在hotpot 虚拟机中本地方法栈与虚拟机栈合并了

    程序计数器

    线程私有;记录当前线程指令执行的行号位置,对代码进行流程控制,也方便在线程进行上下文切换后可以正常往下执行。

    内存模型主要讲的是我们创建的对象以及执行方法是如何在内存中运行的,不同的区域存放不同类型的数据,有条不紊,与双亲委派机制的思想相似,也是有着明确的职能分工,方便管理。

    3.垃圾回收机制

    JVM中最受人推崇的应该就是垃圾回收机制了,想想我们写C语言代码的时候,要申请内存空间,用完之后还得是自己释放,那种被内存支配的感觉我想写过的人都应该知道。
    言归正传,在JVM中,它会帮我们自动分配管理内存,用完之后还会自动回收内存,为我们省去了很多功夫,那么现在我们就来详细了解一下JVM的垃圾回收机制,由内存模型中的介绍,我们可以知道,创建好的实例对象是放在堆中,所以,一般我们回收内存指的就是堆中的内存,所以现在,我们就来具体说说堆内存中的结构!

    堆内存结构.png
    从图上可以看出,堆中主要分为新生代(eden区,from区,to区),老生代。对象刚创建是一般是在新生代的eden区,大部分对象回收都在这里,GC一次活下来的对象会转移至from区,然后每一次GC存活的对象会不断在from 和to两个区域切换,当GC次数达到指定值后,还存活下来的对象会进入老生代(这里涉及一个动态年龄转换算法)。
    要进行GC,首先,我们先得判断哪些对象属于“垃圾”,这里所说的“垃圾”是指不被使用的对象。JVM采用一下两个常用的算法:
    引用计数法:当对象被引用时+1,引用失败-1,当计数器为0时,表示可以回收。(这种方式会存在一个问题,就是当对象存在循环依赖时,就无法确定该对象是否可以回收)
    可达性分析法:从GC Root出发向下搜索,当对象与GC Root之间无任何直接或间接引用是,说明该对象不可用,可以回收。
    这里我们稍微解释一下GC Roots:它表示一组正在活跃的引用,什么是正在活跃的呢?上次我们将虚拟机栈的时候不是有提到局部变量表吗,局部变量中存放着对象的引用,当我们线程中的虚拟机栈栈顶的局部变量表中的引用就可以称为正在活跃的引用,那么,由所有线程的虚拟机栈顶引用也就构成了一组活跃的引用。
    在JVM中,用的是可达性分析算法来判断对象是否属于“垃圾”!
    通过可达性分析我们可以标记那些没有被GC Roots引用的对象,标记完成之后,这时候就可以进行清除了!那么问题又来了,JVM是怎么清除的呢?
    这里就要介绍几种常用的回收算法了。
    1.标记清除算法:将标记后的对象直接清除(这样做存在效率问题以及容易造成内存碎片)
    2.标记复制算法:将内存分为两块区域,对于标记的对象先复制到另一片内存,然后直接清理整块内存
    3.标记整理算法:将要存活的对象依次整理(移动)到一端,然后清理边界以外的内存
    4.分代收集算法:Hotpot使用;将堆进行分代管理,新生代一般存在大量回收对象,存活对象较少,可采用标记复制算法;老生代存在大量存活对象,没有额外空间进行担保,可采用标记清除或者标记整理算法;

    注意:大家有没有发现一个问题,既然分代收集只是与新生代和老生代有关,为什么我们还要在新生代中细分eden区、from区和to区呢?这里涉及到对标记复制算法的一个优化,我们都知道了标记复制算法是需要分配两块内存的,意思就是我们对新生代的内存利用率最高只有50%,JVM觉得这个利用率太低了,所以提出了一个解决方案——大多数对象刚创建时进入eden区(所以给这个区域80%的内存),在eden区快满的时候,进行GC,这时只有少量对象能够存活,将存活对象放入from区,清空eden区域,当第二次GC时,会把eden区域与from区域的对象放入to区,这样to区就变成了from区域,然后清空eden区域和原先的from区域(现在的to区)。这样我们的内存利用率最高就可到达90%了。(from与to区域各占10%)——有同学可能又会问了,要是一块From区域存不下那么多对象时怎么办呢?有这个疑问非常好,带着这个疑问我们接着往下看!其实也很简单,当幸存者区无法放下GC存活的对象时,这时JVM会将这些存活对象直接放入老生代,这里涉及到一个动态年龄算法,感兴趣的可以了解一下,这时如果老生代也无法存放这些对象时,便会触发一次Full GC,即针对整个堆内存做一次GC操作。

    重点:说完算法,现在我们就来具体讲讲JVM中有哪些垃圾收集器,他们又分别使用了那些回收算法。
    1.Serial 收集器与Serial old收集器
    Serial是最原始的收集器,采用单线程设计回收内存,好处是简单高效,减少多线程开销。但是因为在回收时要暂停用户线程,所以造成用户体验不好。新生代采用标记复制算法,老生代采用标记整理算法
    2.ParNew收集器
    在Serial的基础上,采用多线程进行垃圾回收。同样,新生代采用标记复制,老生代采用标记整理。此外,它还能和CMS收集器(后面会讲到)配合使用
    3.Parallel Scavenge收集器和Parallel old收集器
    与ParNew差不多,也是采用多线程机制进行回收,新声代采用标记复制,老生代采用标记整理算法。它们更多的关注CPU的吞吐量(高效利用CPU资源),这也是JDK1.8的默认收集器组合!
    4.CMS收集器
    如果说上面两个是关注吞吐量,那么CMS则更多的是关注用户体验,它是一种以最短回收时间为目标的收集器,也是JVM虚拟机真正意义上的一款并发收集器,它的优点是并发高效,低停顿,减少等待时间。所以它的操作步骤相对较为复杂,下面具体介绍:
    初始标记:暂停所有用户线程,标记roots 的引用对象,时间很短
    并发标记:开启用户线程和GC线程,用闭包的结构去记录所有可达对象,但是需要注意的是,此时用户线程是同时开启的,所以记录的可达对象不具备实时性。但是GC线程会去跟踪这些对象。
    重新标记:暂停用户线程,对上面的标记做修正处理,时间比初始标记要长,比并发标记要短
    并发清除:开启用户线程,同时对未标记的对象做清除处理

    CMS的优点多,但是缺点也很明显,那就是它所采用的标记清除算法不可避免会造成内存碎片,无法处理浮动垃圾,对CPU资源过于敏感!

    5.G1收集器
    应该说这是目前具有里程碑式的收集器了,集所有优点于一身!是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
    操作步骤与CMS收集器相似:
    1.初始标记
    2.并发标记
    3.最终标记
    4.筛选清除
    不同的的是,G1追求的并不是每次回收全部垃圾对象,G1每次回收的垃圾是根据我们设定的参数——stop the world时间来决定要回收多少垃圾内存!具体而言:
    G1沿用了分代策略,但是进一步将堆内存划分成一个个区域Region,一般默认是2048个Region,具体大小为堆内存/2048,其中60%为新生代。具体GC分为Minor、Mixed、Full GC;

    Minor(新生代)GC:根扫描、更新 &&处理Rset、标记复制,首先通过卡表找出GC Roots的引用对象进行标记,然后处理Rset有关联的对象,这里补充一下Rset的概念,Rset主要是为了解决跨代引用的问题,在新生代中存在一些老生代引用对象,这些对象是不能被立即回收的,所以触发GC时我们采用Rset策略将这些新生代中的对象进行GC Roots扫描,最后采用标记复制算法进行清除
    Mixed GC:从名字也可以看出来,这是一次混合GC,包含新生代和部分老生代。操作步骤总体来说和CMS差不多,需要注意的就是在进行并发标记是,CMS采用的方式是扫描所有线程栈和整个新生代做roots,但是在G1中采用的是STAB算法——在GC开始时就对所有存活对象进行一次快照,然后在并发阶段对比快照上修改了引用的对象,判断是否存活,加入GC roots中,最后进行标记复制算法清除。
    Full GC:在Mixed GC无法满足线程分配内存时,导致老生代内存塞满无法进行Mixed GC,这时就会降级成Serial Old收集垃圾。

    4.性能调优

    这里给大家分享一些常见的JVM性能调优参数设置!

    -Xms<heap size>[unit] 
    -Xmx<heap size>[unit]
    -Xms2G -Xmx5G
    -XX:NewRatio=1
    -XX:PermSize=N //方法区 (永久代) 初始大小
    -XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
    -XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
    -XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存
    -XX:+UseSerialGC
    -XX:+UseParallelGC
    -XX:+UseParNewGC
    -XX:+UseG1GC
    -XX:+UseGCLogFileRotation 
    -XX:NumberOfGCLogFiles=< number of log files > 
    -XX:GCLogFileSize=< file size >[ unit ]
    -Xloggc:/path/to/gc.log
    

    在工作中这些应该常用的,有的时候碰到具体问题我们具体分析,JVM分析工具有很多,这里我就不再啰嗦了!

    面试总结系列第三面——欢迎留言讨论,共同进步!*

    相关文章

      网友评论

          本文标题:jVM总结

          本文链接:https://www.haomeiwen.com/subject/ekhrvrtx.html