• JVM学习笔记


    JVM学习笔记

    本篇为JVM重点知识介绍,JVM相关常见面试问题
    复习之前学的内容,同时补充以下知识点:JVM的双亲委派机制、伊甸区与老年代相关知识;
    在这里插入图片描述

    JVM包含两个子系统和两个组件:
    a) 子系统:ClassLoader,类加载器,和ExecutionEngine,执行引擎
    b) 组件:Run-Time Data Area,运行时数据区,和Native Interface,本地接口
    ①ClassLoader,类加载:根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area;
    ②Execution engine,执行引擎:执行classes中的命令;
    ③Native Interface,本地接口:与Native Library交互,是其他编程语言交互的接口;
    ④Runtime Data Area,运行时数据区:就是我们常说的JVM内存;
    流程:首先通过编译器,将Java代码编译为字节码,类加载器ClassLoader再把字节码加载到内存,放到Run-Time Data Area的方法区内,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎Execution Engine,将字节码翻译为底层系统指令,再交由CPU去执行,而整个过程中需要调用其他语言的本地库接口,Native Interface。

    Java程序运行机制详细说明

    1. 首先利用IDE集成开发工具编写.java文件;
    2. 利用编译器(javac命令)将源代码编译为字节码文件,后缀为.class;
    3. 运行字节码的工作是由解释器(java命令)来完成的;
      类的加载指的是将类的.class文件中的二进制数据读入内存,将其放在运行时数据区的方法区内,然后再堆区创建的java.lang.Class对象,用来封装类在方法区内的数据结构。

    堆栈的区别

    1. 物理地址:
      a) 堆的物理地址分配对对象是不连续的。因此性能较慢。因此在GC时各种不同的算法。
      b) 栈使用的事数据结构中的栈,FIFO,物理地址分配时连续的,所以性能好。
    2. 内存区别:
      a) 堆分配不连续,因此分配的内存是在运行时期确认的,因此大小不固定。一般堆大小远远大于栈。
      b) 栈是连续的,所以分配的内存大小在编译期就确认,大小固定。
    3. 存放的内存:
      a) 堆存放的是对象的实例和数组。因此该区更关注的事数据的存储。
      b) 栈存放的事局部变量、操作数栈、返回结果。该区更关注的事程序方法的执行。

    PS:
    静态变量放在方法区
    静态对象放在堆区
    程序的可见度:堆对于整个程序都是共享、可见的。
    栈只对线程可见。所以是线程私有。其声明周期和线程相同。

    双亲委派机制

    双亲的含义应该就是AppClassLoader有:ExtClassLoader和BootstrapClassLoader“两个”父加载器。
    首先介绍Java中的类加载器

    Java中的类加载器

    Bootstrap ClassLoader(启动类加载器),默认加载jdk\lib目录下jar中的诸多类。可以使用-Xbootclasspath指定。
    Extension ClassLoader(扩展类加载器),默认加载jdk\lib\ext目录下的jar中的诸多类。可以使用java.ext.dirs系统变量更改。
    Application ClassLoader(应用程序加载器),应用程序加载器,负责加载开发人员所编写的诸多类。
    User ClassLoader(自定义加载器),自定义类加载器,当存在上述加载器解决不了的特殊情况时,或者存在特殊要求时,可以自行实现类加载逻辑。
    关系图:
    在这里插入图片描述

    双亲委派

    通俗故事:

    1. 假设用户刚刚摸鱼时写了个Test类想进行加载,此时会发送给应用程序类加载器AppClassLoader;
    2. 然后AppClassLoader并不会直接去加载Test类,而是会委派于父类ExtClassLoader,来完成此操作;
    3. ExtClassLoader同样不会直接加载Test类,而是会继续委派父类BootstrapClassLoader;
    4. BootstrapClassLoader已经是顶层了,没有更高的父类加载器了,因此BootstrapClassLoader就从jdk\lib中搜索是否存在,因为这里是用户自己写的Test类,因此不会存在于jdk下,所以此时会给子类一个反馈;
    5. ExtClassLoader收到父类传回的反馈,知道父类加载器没有找到对应的类,爸爸靠不住,就只能自己来加载了,结果显而易见,自己也不行,只能给下面的子类加载器,AppClassLoader;
    6. AppClassLoader收到父类加载器的反馈,顿时明白,原来爸爸虽然是爸爸,但是终究不能管儿子的私事,所以此时,AppClassLoader就自己尝试去加载。
    7. 结果,就这样成功了,走了一大圈,兜兜转转还是自己干。
    什么是双亲委派
    为什么使用双亲委派

    专业性解释:①避免类的重复加载;②防止核心API被篡改;
    为了避免原始类被覆盖的问题。
    老子走过的路,小子不用走
    比如,用户编写了一个Object类,放入程序中加载。
    当没有双亲委派机制时,就会出现重复的Object类,给开发人员造成很大的困扰,本来就只需要基于JDK开发就好了,现在还得把JDK中的类全记住,避免编写重复的类。
    当存在双亲委派机制时,整个事情就不一样了,每次加载类时,都会遵循双亲委派机制,去问父类是否可以加载,如果可以呢,那就不需要再次加载了,这样事情就简单了。

    Tomcat为什么要自定义类加载器

    在这里插入图片描述

    如何打破双亲委派模型(太高级,尝试看一下)

    为什么要考虑这个问题?

    1. 自定义类加载器时,重写loadClass方法。
    双亲委派还有什么

    运行时数据区

    在这里插入图片描述
    蓝色部分是多个线程共享部分;
    绿色部分为单个线程独享部分;

    方法区

    解释器
    JIT编译器
    GC
    为什么需要GC

    垃圾是指JVM中没有任何引用指向的对象,如果不清理这些垃圾对象,那么他们就一直占用内存,而不能给其他对象使用,最终垃圾对象越来越多,就会出现OOM。

    垃圾标记阶段

    先找到垃圾对象。

    引用计数法:

    每个对象保存一个引用计数器属性,用户记录对象被引用的次数。
    a)优点:实现简单,计数器为0就是垃圾对象;
    b)缺点:
    ①无法解决循环引用问题;
    ②需要额外的空间记录;
    ③需要额外的时间维护应用计数。
    在这里插入图片描述

    可达性分析方法:

    以GCRoots作为起始点,然后一层一层找到对应的对象,被找到的对象就是存活对象,那么其他对象就是不可达对象,即垃圾对象。
    GCRoots包括:

    1. 线程中虚拟机栈中正在执行的方法中方法参数、局部变量所对应的对象引用;
    2. 线程中本地方法栈中正在执行的方法中方法参数、局部变量所对应的对象引用;
    3. 方法区保存的类对象的静态属性所对应的对象引用;
    4. 方法区保存的类对象的常量属性所对应的对象引用;
    5. 等等;
    标记清除算法

    STW,Stop The World;

    1. 标记阶段:
    2. 清除阶段:

    a) 缺点:效率不高;产生内存碎片;
    b) 优点:逻辑简单;

    复制算法

    Copying
    将内存分为两块,每次只使用其中一块,进行GC时,将可达对象赋值到另外没有被使用的内存块中,然后再清楚当前内存块中的所有对象,内存块交替使用。
    a) 缺点:耗费空间较大;可达对象多时,效率很低,因此适用于新生代,垃圾对象多的空间;对象内存之地变化之后,需要额外的时间修改对象的引用地址。
    b) 优点:没有内存碎片;没有标记和清除阶段,直接复制操作,不需要修改对象头

    标记-整理算法

    Mark-Compact算法
    第一阶段,从GCRoots找到并标记可达对象;
    第二阶段,将所有存活对象移动到内存的一端;
    最后清理边界外所有的空间;
    a) 缺点:需要修改对象引用地址;适用于垃圾对象少、可达对象多;效率低 ,三种当中最低的;
    b) 优点:没有内存碎片;不需要额外的内存空间;

    分代收集算法

    分代收集的理念
    不同对象的存活时间不一样,因此可以针对不同的对象采取不同的垃圾回收算法。

    • 新生代的对象存活时间比较短,那么就可以利用复制算法,它适合对象比较多的情况。
    • 老年代的对象存活时间比较长,所以不适合用复制算法,可以用标记-清除或者标记-整理算法,比如:
      a) CMS垃圾收集器采用的就是标记-清除算法;
      b) Serial Old采用的就是标记-整理算法;
      在这里插入图片描述
    CMS垃圾收集器

    整个垃圾收集过程变长了,但是STW时间变短了;
    在这里插入图片描述

    1. 初始标记;STW
    2. 并发标记;
    3. 重新标记;STW,时间很短;
    4. 并发清理;
    5. 并发重置;
      当出现新对象要进入老年代,但是空间不够时,会导致“concurrent mode failure”,此时可以利用Serial Old进行一次垃圾收集,就是做一次全局STW。
    G1垃圾收集器

    Garbage First
    将整个内存分为一个个的方块,均分为2048块。
    在这里插入图片描述

    1. 初始标记;STW;
    2. 并发标记;
    3. 最终标记;STW;
    4. 筛选回收;STW;可以通过-XX:MaxGCPauseMillis来制定GC的STW停顿的时间,所以可能并不会回收掉所有垃圾对象,默认200ms;采用复制算法,不会产生碎片(会把某个region对象复制到另外空闲的region中)
      YoungGC:Eden区满了,就会触发G1的YoungGC,对Eden区进行GC
      MixedGC:老年代占用率达到了-XX:InitiatingHeapOccupancyPercent指定的百分比,回收所有的新生代以及部分老年代,以及大对象区
      FullGC:在进行MixedGC过程中,采用复制算法,如果复制过程内存不够,会触发FullGC,会STW,并采用单线程进行标记-整理算法进行GC,相当于一次Serial GC;

    堆区

    在这里插入图片描述
    所有的对象和数组都应该存放在堆区,在执行字节码指令时,会把创建的对象存入堆中,对象对应的引用地址存入虚拟机栈中的栈帧中,不过当方法执行完之后,刚刚被创建的对象并不会被回收,而是要等JVM后台执行GC之后,对象才会被回收。

    • Xms:设置堆的初始化内存大小,等价于-XX:InitialHeapSize;
    • Xmx:设置堆的最大内存,等价于-XX:MaxHeapSize;
      一般会把两个设置为一样,这样JVM就不需要再GC后去修改堆的内存大小了,提高了效率,默认,初始化内存大小=物理内存大小/64,最大内存大小=物理内存大小/4;
    新生代

    可以通过-XX:NewRatio参数来配置新生代和老年代的比例,默认是2,新生代占1,老年代占2,也就是新生代占堆区的1/3;
    一般不需要调整,只有明确知道存活时间比较长的对象偏多,那么就需要调大NewRatio,从而调整老年代的占比;

    • Eden:伊甸园区,新对象都存放在伊甸区(除非对象的大小超过了Eden区,那么就只能直接进入老年代)
    • S0、S1区:Survivor0、Survivor1区,也可以叫做from区,to区,用来存放MinorGC(YGC)后的对象。
    • 默认情况下(Eden区:S0区:S1区)的比例关系为(8:1:1),即Eden区占8/10;可以用-XX:SurvivorRatio调整。

    YGC,Young Garbage Collection,新生代垃圾回收,将Eden区对象放入S0区;S0区和S1区交替使用;

    老年代

    当达到某个条件之后,剩余对象就会被保存到老年代中。
    遇到第二个非常大的对象,Eden区原有一个大对象,内存不够用时,Eden区的大对象会被直接放到老年代(S0、S1区放不下)
    或者来了一个超大对象,可以直接放进老年代;

    GC分类
    • Young GC/Minor GC:负责对新生代进行垃圾回收
    • Old GC/Major GC:负责对老年代进行垃圾回收,目前只有CMS垃圾收集器会单独对老年代进行垃圾收集,其他垃圾收集器基本都是在整堆回收的时候对老年代进行垃圾收集
    • Full GC:整堆回收,也会堆方法区进行垃圾收集。

    程序计数器

    记录下一条待执行指令的地址;

    1. 是物理寄存器的抽象实现;
    2. 用来记录下一条待执行指令的地址;
    3. 它是程序控制流的指示器,循环、if else、异常处理、线程恢复等都依赖它来完成;
    4. 解释器工作时就是通过它来获取下一条需要执行的字节码指令的;
    5. 是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。
      在这里插入图片描述

    Java方法栈

    虚拟机栈是线程私有的,每个线程创建时都会创建一个虚拟机栈,栈内保存一个栈帧,一个栈帧就对应一个方法。

    1. 虚拟机栈是线程私有的
    2. 一个方法开始执行,栈帧入栈、方法执行完对应的栈帧就出栈,所以虚拟机栈不需要进行垃圾回收
    3. 虚拟机栈存在OutOfMemoryError,和StackOverFlowError;
    4. 线程太多,就会出现OutOfMemoryError,线程创建时没有足够的内存去创建虚拟机栈了;
    5. 方法调用层数过多,就会出现StackOverFlowError;
    6. 可以通过-Xss来设置虚拟机栈的大小。
      在这里插入图片描述
    栈帧:

    在这里插入图片描述
    操作数栈:也可以叫做操作栈,是栈帧的一部分,操作数栈是用来执行字节码指令过程中用来进行计算的。

    操作数栈

    比如加法过程,运算操作数在计算过程中的进栈出栈;
    在这里插入图片描述

    局部变量表

    保存局部变量,比如加法过程中,其实每个变量的值都是保存在局部变量表中的。

    方法返回地址
    动态链接

    本地方法栈

    C/C++语言写的一些代码
    存的是本地方法的栈帧,也是线程私有的,也会OOM和SOF。

    补充内容

    堆以及其中对象的创建过程

    堆是 JVM 内存管理的最大的一块区域,此内存区域的唯一目的就是存放对象的实例,所有对象实例与数组都要在堆上分配内存。它也是垃圾收集器的主要管理区域。java 堆可以处于物理上不连续的空间,只要逻辑上是连续的即可。线程共享的区域。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将抛出 OutOfMemoryError 异常。

    堆的“分区”

    为了支持垃圾收集,堆被分为三部分:

    1. 新生代(年轻代):经常被分为Eden区、Survivor区(From Survivor区和To Servivor区),空间分配比例为8:1:1;
    2. 老年代:
    3. 永久代 :JDK8已移除永久代;
    4. Java1.8的堆内存构成:
      在这里插入图片描述
      a) 堆是JVM中线程共享的,因此在其上创建对象都需要加锁;因此导致new对象的开销较大;
      b) Sun Hotpot JVM 为了提升对象内存分配效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行时情况而定,在TLAB上分配对象不需要加锁,此情况下,JVM分配对象的性能和C基本是一样的高效的;若对象过大,仍然直接使用对空间分配;
      c) TLAB只作用于新生代的Eden区,因此在Java程序编写时,多个小对象比大对象分配更加高效;
      d) 所有新创建的Object都将会存储在新时代Young Generation中;如果Young Generation的数据在GC后存活下来,就将被转移到老年代。新的Object总是创建在Eden区。
      需要注意我们所说的TLAB是线程独享的,但是只是在“分配”动作上是线程独占,在读取、垃圾回收等动作上都是现成共享的。而且在使用上没有什么区别。
    对象的创建过程

    在Java中可以使用new关键字和反射机制创建对象:

    User user = new User();
    
    • 1
    User user = User.class.newInstance();
    
    • 1

    或者使用Constructor类的newInstance():

    Constructor<User> constructor = User.getConstructor();
    User user = constructor.newInstance();
    
    • 1
    • 2
    Java对象创建过程

    在这里插入图片描述

    1. 类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
    2. 内存分配:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
    3. 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
    4. 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
    5. 执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
    6. Java虚拟机栈中的Reference指向我们刚刚创建的对象。

    内存的分配过程的?

    内存分配方式

    Integer对象占用空间的有三部分:

    1. 对象头,占16字节;
    2. int表示的具体值,4个字节;
    3. JVM对象内存必须是8字节的整数倍,需要补全4字节。
      因此总共24字节;
  • 相关阅读:
    代码随想录算法训练营第五十一天 |309.最佳买卖股票时机含冷冻期、714.买卖股票的最佳时机含手续费、总结
    时间复杂度吐血总结
    NLP 项目:维基百科文章爬虫和分类【01】 - 语料库阅读器
    基于matlab实现的弹簧振动系统模型程序(动态模型)
    exadata的xdwk进程
    Java中时间日期类、JDK8时间日期类和异常
    python+pytest接口自动化(6)-请求参数格式的确定
    惨,给Go提的代码被批麻了
    Python的高频写法
    不同平方的电线都能带动哪些家用电器
  • 原文地址:https://blog.csdn.net/qq_41119146/article/details/133436207