JVM知识体系

对象的创建

创建对象需要有一个模板

  • 类加载流程

    • 源代码通过jdk中的编译工具javac编译为.class文件,通过类加载器以及加载机制被加载到JVM中,在不同的操作系统下运行

  • 类加载器/类加载机制

    • class文件被放入JVM中靠的就是方法论以及落地实现,靠着类加载机制通过类加载器去将类文件加载进JVM

      • 方法论

        • 装载

          • 查找和导入类文件

            • 先通过类的全限定名获取类的二进制字节流,字节流转化为运行时的数据结构存放到JVM的方法区中,在此之前要对文件格式进行验证,验证通过之后才能进入方法区

        • 链接

          • 验证

            • 保证被加载类的正确性,文件格式,字节码,元数据,符号引用的验证

          • 准备

            • 给类的静态变量分配内存,并且初始化为默认值

          • 解析

            • 将类的符号引用转化为直接引用,符号引用描述所引用的目标,直接引用直接指向目标的指针,方便后续代码的执行

        • 初始化

          • 为静态变量赋予正确的初始值

      • 落地

        • 类加载器

        • 双亲委派机制

          • 加载的原则

            • 一个类加载器接受到类加载的请求,他不会先去加载这个类,而是问自己的父类是否加载这个类,一直到顶层的类先进行加载,如果顶层的类加载了这个类,那么自己就不需要在加载,只有当自己的父类不需要加载时,自己才去加载这个类,通过这样的一个机制可以保护类不被重复加载,确保类的唯一性,也避免了核心类库被篡改,同时规划了类加载的过程,保证这个过程更加清晰和可控

          • 打破这种机制

            • 继承类加载器,重写里面的加载方法,加载方法必然都是由父类先加载,此时重写这个方法就可以打破这个机制

存放对象的空间

  • 运行时数据区

    • 方法区

      • 存放类信息,静态变量,常量,即时编译后的代码,是各个线程的共享区域,随着虚拟机的创建而创建,这些信息需要在程序运行时被加载到内存中,并且需要在整个程序的生命周期内保持不变。方法区提供了一个稳定的存储区域,用于存放这些类相关的信息。

      • 存放对象信息以及数组,同时也是各个线程的共享区域,随着虚拟机创建而创建,堆内存提供了动态分配和释放对象内存的能力,使得程序可以根据需要灵活地创建和管理对象。

        • JVM内存模型

        • 堆内存常见问题

          • 如何理解Minor/Major/Full GC? Minor GC:新生代 Major GC:老年代 Full GC:新生代+老年代 为什么需要Survivor区?只有Eden不行吗? 如果没有Survivor,Eden区每进行一次Minor GC,并且没有年龄限制的话 , 存活的对象就会被送到老年代。 这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。 老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。 执行时间长有什么坏处?频繁的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。 对老年代的空间进行增加: 假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长,也就是每次响应时间都大大增加,严重影响用户体验。 假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。 Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。 为什么需要两个Survivor区? 最主要的原因就是减少空间的碎片化,如果只有一个Survivor区域,当进行一次minor GC的时候,Eden区所有的对象就会移动到survivor区,此时survivor区存放Eden区移动的对象,必然会出现一个内存不连续的问题,也就会导致可以存放的内存对象减少,GC次数增加 但是如果有两个survivor区,就可以避免survivor区内存不连续的问题,永远有一个空间是空的,牺牲10%的空间去避免survivor区域过于碎片化 新生代中Eden:S1:S2为什么是8:1:1? 新生代中的可用内存:复制算法用来担保的内存为9:1 可用内存中Eden:S1区为8:1 即新生代中Eden:S1:S2 = 8:1:1 担保内存:对于大内存对象能临时存放在old区的内存 既然堆区有这么多区域,那么我们的对象创建之后会存储到哪个区域呢? 一般情况下,新创建的对象会分配到Eden区,一些特殊的,特别大的对象则会被分配到old区 比如有对象A,B,C等创建在Eden区,但是Eden区的内存空间肯定有限,比如有100M,假如已经使用了100M或者达到一个设定的临界值,这时候就需要对Eden内存空间进行清理,即垃圾收集(Garbage Collect)--------->垃圾收集的时机 经过GC之后,有些对象就会被清理掉,有些对象可能还存活着,对于存活着的对象需要将其复制到Survivor区,然后再清空Eden区中的这些对象 对survivor区的理解? servivor区分为两块区域s0和s1,也可以叫做From和To 在同一个时间点,s1和s0只能有一块区域有数据,另一个区域为空 我们可以探讨一下GC的过程,一开始只有Eden区和s0区域有数据,s1是空的,接着进行一次GC操作,s0区域中对象的年龄就会+1 我们知道这个时候Eden区所有存活的对象都会存活的对象都会被复制到s1区,也就是说s0区的对象有两个去处: 1是对象年龄达到年龄阈值,往old区移动,2是被复制到s1区,此时Eden区和s0区的所有内容在GC都被清空,s0和s1区域交换位置 由此得知,不管怎么进行垃圾回收,s0和s1区域总有一块是空的 Minor GC会一直重复这样的过程,直到s0区被填满,然后会将所有对象复制到老年代中。 对old区的理解? 一般Old区都是年龄比较大的对象,或者相对超过了某个阈值的对象(大对象) 在Old区也会有GC的操作,Old区的GC我们称作为Major GC。 对象从创建到回收的流程图?

- 虚拟机栈
​
    - 虚拟机栈是线程私有的,独有的,随着线程的创建而创建。 

每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。 调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出

- 程序计数器
​
- 本地方法栈

对象创建的过程

对象创建完之后长啥样

  • 对象创建完成后,在堆内存中分配了一块内存空间用于存储对象的数据,并在方法区中存储了对象的类型信息。对象的具体存储结构会根据 JVM 的实现和对象的具体特征而有所不同,但通常会包括对象头、实例数据以及对齐填充。

对象的使用*

方法的调用过程

  • 方法调用指令: 当程序调用一个方法时,会在字节码中生成对应的方法调用指令。这些指令告诉 JVM 要调用哪个方法以及如何传递参数。

压栈: 在调用方法时,JVM 会创建一个栈帧(Stack Frame)并压入栈中。栈帧用于存储方法的局部变量、方法参数、操作数栈等信息。

方法执行: JVM 开始执行方法体中的代码。在方法执行过程中,会涉及到局部变量的读写、操作数的计算等操作。

返回值: 当方法执行完毕时,会将方法的返回值(如果有)压入调用方法的栈帧中,并将控制权返回给调用者。

弹栈: 方法执行完毕后,JVM 会弹出该方法的栈帧,将控制权返回给调用方法的栈帧。

- 栈帧的内部逻辑
​
    - 局部变量表(Local Variable Table): 局部变量表存储了方法中使用的局部变量。局部变量可以是各种数据类型,包括基本数据类型和对象引用。局部变量表的大小在编译时确定,并且在方法执行期间不会改变。

操作数栈(Operand Stack): 操作数栈用于存储方法执行过程中的操作数。方法中的计算过程通常涉及到将操作数推入操作数栈、从操作数栈中弹出操作数进行计算等操作。

动态链接(Dynamic Linking): 动态链接用于支持方法调用时的动态绑定。在 Java 中,方法调用时的绑定通常是在运行时确定的,而不是在编译时确定的。动态链接将方法调用和实际方法实现之间的连接延迟到运行时进行。

方法返回地址(Return Address): 方法返回地址存储了方法执行完毕后需要返回的位置。当方法执行完毕时,程序需要知道从哪里继续执行,方法返回地址就是指示了这个位置。

额外信息: 栈帧还可能包含一些额外的信息,例如异常处理信息、synchronized 同步信息等。

对象的销毁

原因:空间的有限性

  • 一个对象被创建之后,如果一直存活着,即使他没有作用了,不去销毁,我们的内存空间就会越来越少,内存不是一次性用品,无用的对象,不应该存在于资源有限的空间,就像一个不能带来价值的人,不可能让他存在公司一直占用着资源

判断哪些对象可以销毁

  • 可达性分析

    • 通过作为GC Root的对象,开始向下寻找,确保对象是否可达

    • 能够作为GC Root的对象

      • 类加载器,thread,static成员,常量引用,本地方法

      • 为什么能作为GC Root

        • 特点:至少在方法执行的过程中,不会被回收,他们被认为是活动对象,比如thread,本地方法,常量引用;或者是选取的存活时间长的对象作为GC Root,比如类加载器,static成员

  • 引用计数法

    • 只要该对象被其他对象所引用,他就不是垃圾

    • 缺陷:如果两个对象相互引用,那么就一直保证不会回收的前提,

如何销毁,追求吞吐量和停顿时间的极致

  • 方法论:垃圾回收算法

    • 标记-清除

      • 先扫描堆中所有对象,将需要清除的对象标记出来,比较费时,然后将标记出来的垃圾对象进行清理

      • 缺点:碎片化太多,清除之后空间不连续,当下一次新对象来的时候更容易触发GC

    • 复制算法

      • 留一半内存作为保留空间,当一半内存使用完成之后,将另一半内存拿出来使用,然后清理已经使用的那一半

      • 缺点:空间利用率低

    • 标记-整理

      • 扫描堆中所有对象,将要清除的垃圾对象标记出来,然后将不需清除的对象移动到一端,清除其他空间中的垃圾对象

      • 缺点:将对象进行移动的过程中一定程度降低了效率

    • 分代算法

      • 新生代对象朝生夕死,存活时间比较短,而标记是一个比较耗时的过程,对于短时间存活的对象进行一个全盘扫描后标记没什么意义,复制效率比较高

      • 老年代存活时间比较长,使用复制算法复制的对象太多,反而降低了效率,比较适合标记类算法,清除掉少量的垃圾,收益更高

  • 落地:垃圾回收器

    • 新生代

      • Serial

        • 单线程收集效率比较快

      • ParNew

        • 多线程收集效率比较快,并发运行

      • Parallel Scavenge

        • 吞吐量优先,并行

    • 老年代

      • Serial Old

        • 单线程收集效率快,serial的老年代版本

      • Parallel Old

        • 强调吞吐量,并行, Parallel Scavenge老年代版本

      • CMS

        • 强调停顿时间,并发,并发标记和并发清理,可以和用户线程一起运行

        • 由于清理过程和用户线程一起运行,每次清理的时间比较长,吞吐量下降

    • G1

      • 新老年代都适用

        • 采用标记整理,不会导致空间碎片

        • 可以设置停顿时间,有个筛选回收的过程,在满足用户体验的情况下最大程度的去保证吞吐量

        • 新生代和老年代不再是物理隔离了,而是划分为一个一个的区域,便于筛选回收的过程

  • 对吞吐量和停顿时间极致追求的实践-调优

    • 调优过程无非就是根据具体问题去具体分析,根据垃圾器的选择,参数的调整,去对性能优化不同场景的问题给出不同的答案

      • 常见面试题

        • 关于G1和CMS如何做选择?

          • 性能需求

            • 如果对停顿时间有着过于苛刻的追求,着重在乎用户体验,CMS无疑是更好的选择,而G1对于停顿时间与吞吐量的一个平衡,对停顿时间一个更准确的控制,是选择它的首选

          • 稳定性

            • 对于系统的长时间运行的情况,G1无疑是更好的选择,CMS采取的标记清除算法,导致更多碎片化的情况,是一个不稳定的因素

        • 为什么大内存情况下要选择G1

          • 在内存中,G1的分段内存有利于它更好的管理大内存的情况

    • 调优的意义

      • 提高运行效率,缩短程序响应时间,优化用户体验

      • 优化内存使用,合理的分配和管理内存,减少内存溢出和内存泄露的问题,提高应用程序的稳定性和可靠性

      • 优化CPU使用,通过不断地调优,减少垃圾回收的次数和时间,降低垃圾回收时占用的CPU资源

      • 提高系统吞吐量,通过不断的调优,提高回收时的吞吐量,GC占比时间少,系统运行用户线程时间多,增大系统的并发量

    • 调优的实操

      • 定位问题

        • 内存溢出

          • 出现原因

            • 申请内存的时候,JVM没有足够的内存资源进行分配

          • 排查方法

            • 使用内存监控工具(如Java的JVisualVM、MAT等)来监视程序的内存使用情况,查看是否存在异常的内存占用。

          • 解决方案

            • 最直观的解决方法就是增大内存,针对于堆内存溢出,还可以使用缓存机制去避免对象的反复的创建

        • 内存泄露

          • 出现原因

            • 程序中申请内存的资源无法及时被回收,导致内存无法被程序使用,造成系统资源的内存浪费,内存溢出过多就会导致内存泄漏

          • 排查方法

            • 一般内存泄漏都是开发人员在写代码的时候的遗漏,比如静态集合类,生命周期比较长,对象可达但是无用,我们可以dump出堆文件,结合VisualVM、MAT这些内存监控工具定位内存泄漏的具体原因

          • 解决方案

            • 分析出具体的原因之后,我们定位到对应的代码,资源链接没有关闭,将资源链接放入finally代码块中保证它的关闭状态,避免一些循环引用的对象,保证代码不会形成重复引用

        • cpu飙升

          • 出现原因

            • 无限循环的情况,频繁的触发GC引起的CPU飙升

          • 排查方法

            • 还是定位问题所在,top - c命令查看cpu使用率情况,找到异常CPU占比的进程,demp出快照,结合工具查看具体原因

          • 解决方案

            • 定位到所在问题之后对于无限循环的代码块做一个审查和改进;针对由于频繁GC大量消耗CPU的情况适当的增大内存,选择适当的垃圾收集器,去减少这样的GC

        • 响应时间长

          • 出现原因

            • GC频繁导致STW时间变长

          • 排查方法

            • 同时也是进行一个进程的监控,定位,找到具体的原因

          • 解决方案

            • GC频繁导致的,就减少GC的发生,适当增大内存大小,不断测试选择最合适的垃圾收集器,对于大内存情况还可以选取G1作为垃圾收集器,控制垃圾收集的停顿时间,避免响应时间过长影响用户体验的问题

        • 定位OOM问题的基本步骤

          • 查看错误日志信息,分析错误的类型,出错的位置去确定一个大概的情况

          • 使用命令去定位问题,jps获取进程的PID,jstack获取排查线程的堆栈信息

          • jmap生成堆内存的快照,配合工具(VisualVM MAT)具体分析排查问题

  • 35
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值