• JVM虚拟机理解


    JVM虚拟机

    1.jvm得内存布局

    程序计数器:用于在程度堵塞时记录程序停留的位置,以便再次运行时从记录位置开始,每个线程都有自己独立的程序计数器,但是native方法除外,native方法是用来访问其他语言写的dll文件接口,由其他系统进行管理

    虚拟机栈:用于描述java方法执行的内存模型,由于每个方法执行的时候都会创建一个栈帧用于储存局部变量表,操作数栈,动态链接,方法的出口等信息,每个方法从调用直到执行完成的过程,都对应的一个线帧在虚拟机栈中入栈和出栈的过程,所以虚拟机栈存储的是java方法执行的内存模型,但是如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 StackOverflowError 异常并且如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 OutOfMemoryError 异常,同事虚拟机栈是线程私有的,生命周期与线程一致(新生,就绪,运行,阻塞,死亡)

    本地方法栈:同虚拟机栈功能一样,但是本地方法栈是为虚拟机调用native方法服务的

    java堆:也称GC垃圾回收器,被所有线程所共享,在虚拟机启动时被创建,是jvm内存最大的一块区域,并且堆的内存可以动态调整,调整也是会损耗性能的,所以我们可以通过-Xms -Xmx来设定对应的最大内存和最小内存一样来规避动态调整;

    方法区:同堆一样,被所有线程所共享,它储存类的结构信息,如运行时的常量池,字段,方法数据,构造函数和普通方法的字节码等,常量池并非独立的区域,它时方法区的一部分

    2.java引用的强软弱虚

    Java 的引用的类型有四种:强引用、软引用、弱引用和虚引用。它们背后关系到 JVM 的垃圾回收机制回收内存的时机。

    1.首先说介绍下苹果公司的objective-c语言,此语言没有垃圾回收机制,但是引入了一个引用计数的功能,主要作用就是当在编译源码的时候去统计和记录对象的引用数量,当这个对象没有任何引用的时候,这个对象的引用计数就会变为0,这个时候编译器会在变为0的那个语句后加上释放空间的语句.

    2.并不是所有的引用都会使计数+1,只有强引用才会,弱引用是不会+1的

    3.在java中,我们日常编码所涉及的引用全是强引用

    4.java中的软引用:

    Student tom = new Student();
    SoftReference<Student> jerry = new SoftReference<>(tom);
    
    tom = null;
    
    • 1
    • 2
    • 3
    • 4

    当tom=null的时候,垃圾回收不会马上进行清理,首先垃圾回收器不是时时刻刻运行的,并且优先级很低,所以在没发现之前是不会回收的,再次如果垃圾回收器发现了软引用但是垃圾回收器也不会回收软引用的

    java中的弱引用:

    弱引用比软引用更低一级,如果对象中没有强引用则不管内存够不够都会直接被回收,但是考虑到垃圾回收机制不是时刻在干活,在应该回收之前也会在内存中停留一段时间

    java中的虚引用:

    虚引用是 Java 中最弱的引用,那么它弱到什么程度呢?通过虚引用无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被回收。

    5.为什么oc会提出弱引用的概念

    因为引用计数无法通过强引用解决循环引用的问题,java的回收机制是可达性分析,没有此问题

    3.垃圾的回收机制

    3.1 那些内存需要被回收?

    目前有两种方案进行判断

    1.引用计数

    通过编译源码时,通过统计对象的引用数量来判断是否需要被回收,当对象的引用数量为0时将被回收,当出现循环引用时无法使对象的引用数量为0,这个时候就需要使用弱引用来规避这个问题

    2.可达性分析

    主流具有垃圾回收机制的语言都使用的该方案,判断的依据如下:

    通过GC Roots的根对象作为起点向下搜索,搜索的路程为引用链,如果GC Roots与对象间没有引用链则证明该对象不可能再被使用

    3.2 并发可达性分析的问题

    为了降低 JVM 的垃圾回收器的造成的停顿,JVM 的在分析堆中对象的可达性时,是并发执行的,不会造成用户线程冻结。当出现并发的情况下,对象可能根据用户的选择不同从而触发不同的引用,这个时候就会造成有的引用对象在垃圾回收机制扫描前还没有引用,但是扫描过后出现了引用的情况,就会造成明明引用了对象但却被垃圾回收机制回收的情况,为了解决这个问题,出现了两个方案解决这个问题:

    3.2.1 增量更新

    思路为:新增一条被引用对象到没被引用对象的引用链,等并发结束之后再扫描一遍为被引用对象到没被引用对象的引用链,这个时候并发没被扫描到的引用链会被扫描到;

    CMS垃圾回收器采用的就是这个方案

    3.2.2 原始快照

    思路为:破环造成对象消失的条件,删除未被全部扫描到的对象指向未引用的对象,等并发结束后,扫描从未被全部扫描的对象到未被扫描对象;

    G1垃圾回收器采用的方案

    解决对象消息现象的方案有两种,JDK 1.5 开始的默认垃圾回收器 CMS 和 JDK 9 开始的默认垃圾回收器 G1 采用了不同的方案。

    3.3 什么时候回收

    首先是全局扫描回收(FULL GC),全局扫描会极大的影响程序的性能,对堆空间的所有对象进行扫描、分析、销毁的垃圾回收行为,被称为『全扫描』,基于这种行为的垃圾回收器,就被称为 Full GC 。这种方法长期使用是不可取的,后面就衍生出分代收集的方法;

    3.3.1 分代收集

    分代收集是jvm虚拟机将扫描对象分为 新生代老年代 两种区域,当一个引用对象被创建后会被放置到新生代区域,在经过15轮的扫描后还存活则将该引用对象放置到老年代,其中老年代的被扫描的频率远低于新生代被扫描的频率,这样就节省了扫描的性能,但同时也衍生了跨代引用的问题;

    3.3.2 跨代引用

    当新生代区域的对象引用了老年代的对象或者老年代的对象引用了新生代的对象时就会出现跨代引用

    问题:当老年代区域的对象引用这个新生代对象时,垃圾回收机制扫描了新生代区域的对象,发现新生代的其他对象没有引用该对象,那么该对象可能就会被回收掉,造成使用的对象会莫名的消失;

    解决办法:记忆集方案

    原理:将老年代引用新生代的对象存入到统一的记忆集中,在删除没被引用对象之前会去记忆集查找一边有没有对应对象,有就不删,没有就删除;

    3.4 如何回收?

    垃圾收集器线程被唤醒干活时,并不是简单地『判断对象的可达性,对于不可达的已死的对象回收其内存空间』。在回收内存过程中,实际上它是要通过 『垃圾回收算法』分若干步操作才能实现回收内存。有如下几个收集算法

    3.4.1 标记-清除算法

    最早出现的清除算法,原理就是将内存区域中的对象做标记,标记为存活对象,可回收,未使用几种状态,扫描完成后将所有的可回收对象进行回收;

    缺点: 1.出现大量的间断内存;

    ​ 2.执行的效率不稳定,随着内存中对象的增多导致扫描的时间过长;

    从 JDK 1.5 开始的默认垃圾收集器 CMS 就是基于标记-清除算法的,不过它并非原始的标记-清除算法,它是 Concurrent Mark-Sweep(并发式标记清除)算法

    3.4.2 标记-复制算法

    此算法是对清除算法的一种优化,使用的策略是用空间换时间;

    原理: 首先是将内存区域平均分为两个部分,将扫描其中一部分区域,将扫描的区域的存活对象连续的复制到另一个区域中,然后清除掉被复制的区域达到清除的目的;

    解释:虽然看起来还需进行复制对象并且移动对象,但是从根本来说需要复制的对象是远远小于内存的,这个时候清除一块区域全部空间的内存比一个个对象的清除可快的多,所以性能和速度反而提升了;

    缺点:总有一半的内存没有被使用

    3.4.3 标记-复制算法优化

    此算法是对标记复制算法的空间进行了优化,将空间分别划分为8:1:1,其对应的区域为eden,survivor1,survivor,其中eden,survivor1为扫描区域,survivor2为存放区域,当eden和survivor1中有可存活的对象将都移动到survivor2中,这样就只浪费了10%的空间

    3.4.4 标记-整理算法

    此算法是一个特殊的清除算法,当老年代的区域对象很少时可以使用此算法;

    原理:首先将整个内存都将存储所有对象,然后存储的对象按照内存顺序进行存储,然后删除后面不可存活的对象

    JVM 虚拟机里面的可选垃圾回收器:Parallel Scavenge 就是基于标记-整理算法的。另外,基于标记-清除算法的 CMS 在临空间碎片过多时会『临时性』地采用的就是这种处理办法来回收内存。

    4.Java 中对象占多大内存

    一个对象在堆内存中占多少内存空间,因 JDK 是 64 位还是 32 位有所区别,但是总体规则是相似的:

    • 32 位 JDK
      • 8 字节的头部信息;
      • byte、boolean 占 1 字节;
      • char、short 占 2 字节;
      • int、float 占 4 字节;
      • long、double 占 8 字节;
      • 引用占 4 字节;
      • 整体对齐到 4 字节的倍数。
    • 64 位 JDK
      • 12 字节的头部信息;
      • 基本数据类型占字节数与 32 位一样;
      • 引用在占 8 字节(开启『引用压缩』功能后,占 4 字节);
      • 整体对齐到 8 字节的倍数。

    5.虚拟机中的泛型类型信息

    Type 是 Java 语言中所有类型的公共父接口。这就是最官方的解释。

    Class 就是 Type 的一个直接实现类。Type 和 Class,以及 Type 的其它子接口( 和实现类 )组成了 Java 的类型体系。

    在Java5.0之前是没有type类型的数据,所有的类都有一个class对象,由于在5.0版本的时候加入了泛型的概念,这个概念就不成立了,所以就加了type类型,为了使type类型更加完善,就补全了对应的其他类型:

    parameterizedType

    TypeVariable

    WildcardType

    GenericArrayType

    Type 和它的子接口、实现类( Class、ParameterizedType、TypeVariable、WildcardType、GenericArrayType )共同组成了 Java 的类型体系。

    如hashMap中的k和v的类型就是typeVariable,而hashMap这个整体的类型就是ParamterizedType;

    而 private T[] array 的T[] 类型就是GenerticArrayType

    我们常用的反射class中的?类型就是wildCardType

    反射和type

    当我们使用getParameterTypes方法获取方法的参数时,如果该参数是泛型的话就会被泛型擦除,最后获得的是object对象,如果我们需要获得其参数就得使用getGenericParameterTypes来进行获取,其中获取的信息是保留泛型信息的,其他的原来的获取方法加上generic都可以获取其保留的泛型信息

    6.Java ClassLoader

    1.类加载机制

    将Java中的源码.java文件会在运行前被编译成.class文件,文件内的字节码的本质就是一个字节数组,有特定的格式,java类初始化的时候会调用java.lang.ClassLoader加载字节码,当需要使用这个类时,虚拟机会加载.class文件,并创建对应的对象,并将class文件加载到虚拟机内存中,而jvm中类的查找与装载就是classLoader完成的,并且在程序启动的过程中,并不是一次性加载程序所要用的.class文件,而是根据程序的需要,动态加载这个class文件到内存中,从而只有class文件被载入到内存中,才能被其他.class所引用.所以ClassLoader是动态加载class文件到内存中的

    2.类加载方式

    加载机制一般分为两种:

    显式:利用反射加载一个类

    隐式:通过ClassLoader来动态加载,new 一个类或者类名.方法名

    3. ClassLoader

    JVM中有3个内置ClassLoader加载器

    1.bootstrapClassLoader启动类加载器:负责加载JVM运行时核心类,如java.util,java.io

    2.ExtensionClassLoader扩展类加载器:负责加载JVM扩展类,如内置的js引擎,xml解析

    3.AppClassLoader 系统类加载器:负责加载用户的加载器,我们编写的代码和第三方jar包通常由它加载

    4. 类加载流程

    类的加载分为三个步骤:加载,连接,初始化

    a. 加载

    类加载指的是将 .class 文件读入内存,并为之创建一个 java.lang.Class 对象,即程序中使用任何类时,也就是任何类在加载进内存时,系统都会为之建立一个 java.lang.Class 对象,这个 Class 对象包含了该类的所有信息,如 Filed,Method 等,系统中所有的类都是 java.lang.Class 的实例。

    类的加载由类加载器完成,JVM 提供的类加载器叫做系统类加载器(上述三个),此外还可以通过自定义类加载器加载。

    通常可以用如下几种方式加载类的二进制数据:

    • 从本地文件系统加载 .class 文件。
    • 从JAR包中加载 .class 文件,如 JAR 包的数据库启驱动类。
    • 通过网络加载 .class 文件。
    • 把一个 Java 源文件动态编译并执行加载。
    b. 链接

    链接阶段负责把类的二进制数据合并到 JRE 中,其又可分为如下三个阶段:

    1. 验证:确保加载的类信息符合 JVM 规范,无安全方面的问题。
    2. 准备:为类的静态 Field 分配内存,并设置初始值。
    3. 解析:将类的二进制数据中的符号引用替换成直接引用。
    c. 初始化

    类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

    5.双亲委派机制基本概念

    双亲委派简单理解:向上委派,向下加载。这个过程非常类似于递归,分为『递』和『归』两个环节。

    classloader-01.png

    当一个 .class 文件要被加载时:不考虑我们自定义类加载器,

    • 首先会在 AppClassLoader 中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的 loadClass 方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。直到到达 Bootstrap classLoader 之前,都是在检查是否加载过,并不会选择自己去加载。
    • 直到 BootstrapClassLoader,已经没有父加载器了(无法再继续向上委派了),这时候开始考虑自己是否能加载了; 如果自己无法加载,会下沉到子加载器去加载,一直到最底层(向下加载)。如果没有任何加载器能加载,就会抛出 ClassNotFoundException 异常。

    那么为什么加载类的时候需要双亲委派机制呢?

    采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次。

    其次是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被 Bootstrap ClassLoader 加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是 BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

  • 相关阅读:
    深度详解 Android 屏幕刷新机制之 Choreographer
    前几天面了个30岁的测试员,年薪50w问题基本都能回答上,应该刷了不少八股文···
    paddledet 训练旋转目标检测 ppyoloe-r 训练自己的数据集
    uni-app 之 图片
    分享好用无广告的手机浏览器,亲测值得下载
    ui_cb.c
    【C++系列P5】‘类与对象‘-三部曲——[对象&特殊成员](3/3)
    【POJ】1050.To the Max (最大子矩阵和)
    使用 OpenTelemetry 构建 .NET 应用可观测性(2):OpenTelemetry 项目简介
    Hadoop基础学习总计
  • 原文地址:https://blog.csdn.net/qq_42652006/article/details/127412010