Java核心篇之JVM探秘:对象创建与内存分配机制

news2024/11/13 12:46:10

系列文章目录

第一章 Java核心篇之JVM探秘:内存模型与管理初探

第二章 Java核心篇之JVM探秘:对象创建与内存分配机制

第三章 Java核心篇之JVM探秘:垃圾回收算法与垃圾收集器

第四章 Java核心篇之JVM调优实战:Arthas工具使用及GC日志分析


目录

前言

一、对象创建过程

(1)类加载检查

(2)分配内存

1.对象创建与内存分配

2.对象填充

3.执行构造函数

4.并发问题处理

(3)初始化零值

(4)设置对象头

Mark Word

Type Pointer

Array Length (如果对象是数组)

(5)执行方法

二、对象内存分配

(1)对象在栈上分配

示例:

(2)对象在Eden上分配

Eden区分配的特点:

示例:

(3)大对象直接进入老年代

(4)长期存活的对象将进入老年代

(5)对象动态年龄判断

(6)老年代空间分配担保机制

三、对象内存回收

(1)引用计数法

示例:

(2)可达性分析算法

(3)常见引用类型

(4)finalize()方法最终判定对象是否存活

(5)如何判断一个类是无用的类

总结


前言

        Java虚拟机(JVM)是Java语言的核心组件之一,负责执行Java字节码。在JVM中,对象的创建和内存管理是一个复杂而精细的过程,涉及多个阶段和多种策略。本文将深入探讨JVM中的对象创建流程、内存分配机制以及它们如何影响程序性能。


一、对象创建过程

下边两张图分别是类加载机制和对象创建过程的流程图

(1)类加载检查

当程序请求创建一个新对象时,JVM首先会检查这个类是否已经被加载、解析和初始化过。如果尚未完成这些步骤,那么JVM会先执行类加载过程。

(2)分配内存

一旦类被确认可以使用,JVM会在堆内存中为新对象分配空间。对象的内存大小由其成员变量决定,包括实例变量和继承链上的变量。

在JVM中,内存分配主要发生在堆内存中,堆内存是所有线程共享的内存区域,用来存储所有Java对象实例和数组。内存分配的过程可以分为几个关键步骤:

1.对象创建与内存分配

当Java程序请求创建一个新对象时,JVM首先检查这个类是否已经被加载、解析和初始化过。如果类已经准备好,JVM会在堆中为新对象分配内存。内存分配的方式有两种主要策略:指针碰撞(Bump-the-Pointer)和空闲列表(Free List)。

  • 指针碰撞:如果堆内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间维护一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
  • 空闲列表:如果堆内存是不规整的,已使用的内存和空闲的内存相互交错,那就需要维护一个列表,记录上面那些大小不一的空闲内存区间。

2.对象填充

分配好内存之后,JVM会对新对象进行填充,包括初始化对象的成员变量为默认值(例如,int类型为0,引用类型为null)。

3.执行构造函数

最后,JVM会执行对象的构造函数,完成对象的初始化。

4.并发问题处理

在多线程环境下,内存分配和对象创建可能会引发竞态条件和内存一致性问题。JVM采用了多种机制来解决这些问题:

  • 线程同步 JVM使用锁机制来保证在多线程环境下内存分配的原子性。当多个线程试图同时创建对象时,JVM会使用锁来确保一次只有一个线程能够进行内存分配,从而避免竞态条件。

  • 内存屏障 为了保证内存访问的有序性,防止指令重排序,JVM使用内存屏障(Memory Barrier)技术。内存屏障是一种特殊的指令,它可以阻止编译器和处理器对内存操作进行重排序,确保内存操作的顺序符合程序的预期。

  • 缓存一致性 在现代多核处理器中,每个CPU都有自己的缓存,为了保持缓存之间的一致性,处理器使用了一种称为MESI(Modified, Exclusive, Shared, Invalid)协议的缓存一致性协议。JVM利用硬件提供的缓存一致性协议来维持多线程环境下的内存一致性。

  • TLAB (Thread Local Allocation Buffer) 为了减少锁的使用,提高对象创建的效率,JVM提供了一个叫做TLAB(线程本地分配缓冲区)的概念。每个线程都有一个独立的TLAB,对象优先在自己的TLAB中分配,这样可以避免在多线程环境下频繁地获取锁,从而提高了对象创建的速度。

  • CAS (Compare and Swap) CAS是一种无锁编程技术,用于原子更新变量。JVM利用CAS操作来实现一些轻量级的同步机制,比如原子变量类(如java.util.concurrent.atomic包中的类)。

(3)初始化零值

分配完内存后,JVM会将对象的成员变量初始化为默认值,如整型为0,浮点型为0.0,引用类型为null等。这一步是为了确保对象在构造函数执行前处于一致状态。

(4)设置对象头

当JVM为新对象分配完内存后,在初始化零值之前,它会先设置对象头。对象头通常包含以下信息:

Mark Word

Mark Word是对象头中的一部分,主要用于存储对象的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁等。Mark Word的布局会根据对象的锁定状态动态改变,以便支持轻量级锁、偏向锁、重量级锁等不同的锁级别。

Type Pointer

这是指向对象所属类的元数据的指针,JVM通过这个指针确定对象所属的类。在某些JVM实现中,类型指针可能不在对象头中,而是通过其他方式(如类指针压缩)来存储。

Array Length (如果对象是数组)

如果创建的对象是一个数组,对象头还会额外包含一个长度字段,用于存储数组的长度。

(5)执行<init>方法

执行<init>方法,也就是执行对象的构造函数,这是程序员定义的用于初始化对象状态的方法。构造函数可以调用其他方法或访问静态变量,但不能直接访问或修改非初始化的实例变量。

二、对象内存分配

(1)对象在栈上分配

通常情况下,对象是在堆内存中分配的,这是因为对象的生命周期不可预测,可能需要长期存在。然而,在某些特殊情况下,对象可以分配在栈上,这种情况被称为栈上分配(Stack Allocation)或标量替换(Scalar Replacement)。栈上分配主要应用于局部变量,尤其是那些在方法体内创建并很快就会被销毁的小型对象,这样可以显著减少垃圾收集的压力。 

示例:

假设有一个简单的Java方法,其中创建了一个局部变量对象,这个对象不会被方法之外的代码引用,而且它的生命周期仅限于该方法的执行期间。在这种情况下,如果JVM启用了栈上分配并且经过逃逸分析确定该对象不会逃逸,那么该对象就有可能在栈上分配。

public class StackAllocationExample {
    public void method() {
        User user = new User();
        // 使用user...
        // user仅在method方法的栈帧中存在
    }
}

class User {
    String name;
    int age;
}

在这个例子中,User对象只在method方法内部创建和使用,如果它满足栈上分配的条件,那么它将会在栈上分配,而不是在堆上。 

(2)对象在Eden上分配

JVM的堆内存被划分为新生代和老年代。新生代又进一步分为一个Eden区和两个Survivor区(S0和S1)。Eden区是对象首次创建的地方,大部分对象在Eden区分配。

Eden区分配的特点

  • 对象创建时,首先尝试在Eden区内分配。
  • 如果Eden区没有足够的空间,或者对象太大,可能直接进入老年代。
  • 经过若干次垃圾回收(Minor GC),存活下来的对象会从Eden区晋升到Survivor区,或者直接晋升到老年代。

示例:

当一个对象创建时,默认情况下它会在Eden区内分配。Eden区是新生代的一部分,专门用于存放新创建的对象。如果对象在一次或多次垃圾回收后仍然存活,它将被移动到Survivor区,或者直接晋升到老年代。

public class EdenAllocationExample {
    public static void main(String[] args) {
        User user = new User();
        // 使用user...
    }
}

class User {
    String name;
    int age;
}

在这个例子中,User对象创建在Eden区,如果它在接下来的垃圾回收过程中存活,那么它可能被晋升到Survivor区或老年代。 

(3)大对象直接进入老年代

大对象指的是需要大量连续内存空间的对象,例如大的数组或长字符串。JVM为了减少新生代的碎片化,避免频繁的Minor GC,采取了以下策略:

  • 如果对象的大小超过了一定的阈值(通常是JVM的一个参数设置),这个对象会被直接分配到老年代中。
  • 直接进入老年代可以避免新生代的频繁垃圾回收,因为大对象通常生命周期较长,不易被回收。
  • 这种策略有助于提高大对象密集型应用的性能,减少垃圾收集的次数。

(4)长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数–XX:MaxTenuringThreshold来设置。

(5)对象动态年龄判断

对象的年龄是指对象从创建开始到被垃圾收集器回收之间所经历的Minor GC次数。JVM给每个对象分配一个年龄计数器(Age Counter)。对象在Eden区创建后,如果在第一次Minor GC后依然存活,它会被移动到一个Survivor区,并且年龄计数器会加1。此后,每次经历Minor GC并且没有被回收,年龄计数器都会递增,直到达到一定的阈值(默认为15)。当对象的年龄达到这个阈值时,它将被提升到老年代。

对象年龄的判断机制有助于将长期存活的对象及时移动到老年代,避免新生代的频繁垃圾回收,同时也减少了老年代的垃圾回收压力,因为老年代的垃圾回收(Full GC或Major GC)成本更高。

(6)老年代空间分配担保机制

在进行Minor GC之前,JVM会检查老年代是否有足够的空间来接收可能从新生代提升过来的对象。如果老年代的空间不足以担保这次Minor GC后所有存活对象的迁移,JVM将触发一次Full GC,以腾出足够的空间。这种机制被称为老年代空间分配担保(Space Allocation Guarantee),其目的是避免在新生代进行垃圾回收时出现内存不足的情况,确保Minor GC的顺利进行。

如果担保机制检测到老年代空间不足,JVM会进行如下操作:

  1. 尝试压缩老年代,回收部分空间。
  2. 如果压缩后仍然不足,将触发Full GC,清理整个堆内存,包括老年代和永久代(在JDK 8中是方法区)。
  3. 如果Full GC后仍然不足,将抛出OutOfMemoryError异常。

三、对象内存回收

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象是没有被任何地方使用的。

(1)引用计数法

引用计数法是一种简单的内存管理策略,它通过跟踪指向一个对象的引用数量来确定对象是否可被回收。每当有一个地方引用一个对象时,它的引用计数器就会加1;当引用失效时,计数器减1。当一个对象的引用计数变为0时,表明没有任何引用指向它,此时它就可以被回收了。

优点:实现简单,运行时不需要进行额外的计算或全局性的搜索,即时性好。

缺点:无法处理循环引用的问题。如果有两个对象相互引用对方,即使它们不再被外部引用,引用计数法也无法正确识别它们为垃圾,从而导致内存泄露。

示例:

尽管Java的垃圾收集器不采用引用计数法,但我们可以通过一个类似场景的示例来说明,如果在引用计数法下,两个对象互相引用时可能导致的内存泄漏情况。下面是一个简化版的伪代码示例,用于说明这个问题:

// 注意:以下代码仅用于演示,实际上Java的垃圾收集器不使用引用计数法
class Node {
    private Node reference;

    public Node(Node ref) {
        this.reference = ref;
    }

    public void setReference(Node ref) {
        this.reference = ref;
    }

    public Node getReference() {
        return reference;
    }
}

public class ReferenceCountingDemo {
    public static void main(String[] args) {
        Node nodeA = new Node(null);
        Node nodeB = new Node(nodeA);
        nodeA.setReference(nodeB);

        // 现在nodeA和nodeB互相引用,如果没有垃圾收集器,将导致它们都无法被回收
    }
}

在引用计数法中,nodeAnodeB互相引用对方,即使它们不再被任何外部引用持有,但由于它们相互之间的引用,它们的引用计数都不会降到0,因此按照引用计数法,这两个对象将永远不会被回收,导致内存泄漏。 

(2)可达性分析算法

这是JVM中常用的垃圾回收算法。它基于一个基本思想:通过一系列称为“GC Roots”的根对象作为起始点,从这些根对象向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,即可判定此对象是不可达的,即不可能再被使用。

GC Roots通常包括:

  • 正在执行的方法中声明的局部变量对象。
  • 方法调用栈中引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。
  • Java虚拟机内部引用的对象,如基本类型的Class对象,或者常量池中的引用。

(3)常见引用类型

Java中定义了四种强度不同的引用类型,它们分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。

  • 强引用是最常用的引用类型,只要强引用存在,垃圾回收器就不会回收掉对象。
  • 软引用用于描述还有用但非必需的对象。当系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用比软引用的强度更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。
  • 虚引用也称为幽灵引用或幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

(4)finalize()方法最终判定对象是否存活

在Java中,finalize()方法是Object类的一个保护方法,允许在对象被垃圾回收前做一些必要的清理工作。当垃圾回收器准备回收一个对象时,如果发现这个对象中定义了finalize()方法,就会自动调用这个方法。但是,finalize()方法的调用并不是强制的,也不保证一定会被调用,且其执行时机不确定,不应依赖它进行资源释放,应使用try-finally或try-with-resources语句进行资源的显式释放。

示例:

在Java中 finalize()方法可以用来执行一些清理工作,如关闭文件、网络连接等资源。然而,finalize()方法的调用不是强制的,且其实现细节和调用时机由JVM决定,因此不应该依赖它来确保资源的释放。下面是一个使用finalize()方法的示例:

class ResourceManagedObject {
    private boolean isClosed = false;

    protected void finalize() throws Throwable {
        if (!isClosed) {
            // 执行清理工作,例如关闭文件或网络连接
            System.out.println("Resource cleaned up by finalize()");
            isClosed = true;
        }
        super.finalize();
    }

    public void close() {
        // 显式关闭资源
        System.out.println("Resource closed explicitly");
        isClosed = true;
    }
}

public class FinalizeDemo {
    public static void main(String[] args) throws Exception {
        ResourceManagedObject obj = new ResourceManagedObject();
        // 使用obj...

        // 显式关闭资源
        obj.close();

        // 删除引用,使obj成为垃圾收集的目标
        obj = null;
        System.gc(); // 请求垃圾收集

        // 等待垃圾收集器运行
        Thread.sleep(1000); // 假设垃圾收集器会在1秒内运行
    }
}

在这个示例中,ResourceManagedObject类实现了finalize()方法,用于在对象被垃圾收集前执行资源清理工作。然而,最佳实践是不要依赖finalize()方法,而应该在不再需要对象时显式地调用close()方法来释放资源,这是因为finalize()方法的执行是不确定的,且其执行可能带来性能上的开销。此外,从Java 9开始,finalize()方法的使用已被弃用,建议使用其他资源管理技术,如try-with-resources语句或显式的资源关闭逻辑。 

(5)如何判断一个类是无用的类

类的卸载(Class Unloading)在Java中并不常见,但在某些特定情况下,如Web容器中,可能需要卸载不再使用的类。判断一个类是否无用,通常考虑以下几点:

  • 类的所有实例都已经回收。
  • 没有任何引用指向该类的Class对象。
  • 类的加载器已经回收或可被回收。

总结

        JVM中对象的创建和内存分配是一个多步骤、多策略的过程,涉及类加载、内存布局、垃圾回收等多个层面。理解和优化这一机制对于提升Java应用程序的性能至关重要。通过合理的设计和编码实践,我们可以最大限度地发挥JVM的优势,构建高效稳定的应用系统。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1925773.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows11终端winget配置

一、工具安装 Windows11是自带该工具的&#xff0c;如果wind10&#xff0c;可以找应用商店和GitHub上进行下载。 安装地址使用 winget 工具安装和管理应用程序 | Microsoft Learn 发布地址 Releases microsoft/terminal GitHub 二、无法使用问题排错 在命令行界面出现以…

鸿蒙读取本地文件同步,异步的优化问题

一、问题引入 有这样一个业务场景&#xff0c; 在进入一个新页面前&#xff0c;需要读取本地文件 。 当这个文件比较大时 &#xff0c;会造成加载页面时间过长。 二、 问题讲解 一般在页面的aboutToAppear里&#xff0c;我们会同步读取文件 &#xff0c; 因为页面UI 依赖文件…

sql monitoring 长SQL ASH AWR 都没有 未Commit or export to csv

Duration 4小时&#xff0c; Database Time 22.5&#xff0c; Session Inactive&#xff0c; 1.未Commit原因, 2.慢慢导出成csv文件&#xff1f; How is v$session status INACTIVE and v$sql_monitor status EXECUTING concurrently 2641811 Posts: 8 Jan 11, 2016 6:47P…

【Linux】重定向 | 为什么说”一切皆文件?“

目录 前言 1.文件描述符分配规则 2.dup2 重定向接口 3.重定向 3.1>输出重定向 3.2>>追加重定向 3.3<输入重定向 3.4 shell 模拟实现< > 3.5 理解> 4. 理解“Linux 下一切皆文件” 前言 问&#xff1a;fd 为什么默认从 3 开始&#xff0c;而不是…

手机怎么用代理ip上网

在数字化时代&#xff0c;网络已经成为我们生活中不可或缺的一部分。然而&#xff0c;有时候出于安全、隐私或访问特定网络资源的需要&#xff0c;我们可能需要使用代理IP来上网。那么&#xff0c;什么是代理IP&#xff1f;如何在手机上设置并使用它呢&#xff1f;本文将为您详…

聚观早报 | 网宿科技推出边缘AI网关;AMD再收购AI公司

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 7月12日消息 网宿科技推出边缘AI网关 AMD再收购AI公司 谷歌Pixel 9系列将配超声波指纹 三星Galaxy Z Fold6亮相 …

如何写论文的讨论和结论部分,提升审稿通过率300%?(附例句模版)

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 关于论文讨论Discussion部分的撰写&#xff0c;娜姐之前写过几篇文章&#xff1a; 1 Discussion讨论部分被3个审稿人说没深度没逻辑&#xff0c;用这个AI工具三步拯救了我&am…

前端插槽简易版详解【适合有点基础但是有点迷糊的同学查阅】

简易版默认插槽 默认插槽也有自己的名字 效果&#xff1a; 具名插槽 简写 萝卜坑可以种组件哦 插槽传值&#xff1a;父组件用scope接收---变成一个对象

udp协议模拟远程输入指令控制xshell

不了解udp协议的可以先看一下udp协议下的socket函数_udp socket函数-CSDN博客 我之前还写过模拟实现xshell的模拟实现简单的shell-CSDN博客 如今我们要模拟的是让别人连网络连到我们主机&#xff0c;他可以执行命令&#xff1a; 1.接口 我们之前是用execl系列的函数来实现的…

【Android】Service介绍和生命周期

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 介绍 Service是Android程序中四大基础组件之一&#xff0c;它和Activity一样都是Context的子类&#xff0c;只不…

强化学习的数学原理(1)

基本概念 State:在下面的例子中就是指每个位置&#xff08;location&#xff09;&#xff0c;例如s1、s2、s3…就是一个State&#xff0c;所有State的集合就是State Space(集合空间 S { s1,s2,s3,s4,s5…,s9 }) Action:每个状态可采取的行动&#xff0c;就如下面图片的例子&a…

openEuler 安装 podman 和 podman compose

在 openEuler 22.03 LTS SP4 中&#xff0c;你可以使用 dnf 包管理器来安装 Podman 和 Podman Compose。openEuler 默认使用 dnf 作为包管理器&#xff0c;所以这是安装软件的首选方式。 关于 openEuler 22.03 LTS SP4 下载地址&#xff1a; https://www.openeuler.org/zh/dow…

昇思MindSpore 25天学习打卡营|day20

GAN图像生成 模型简介 生成式对抗网络(Generative Adversarial Networks&#xff0c;GAN)是一种生成式机器学习模型&#xff0c;是近年来复杂分布上无监督学习最具前景的方法之一。 最初&#xff0c;GAN由Ian J. Goodfellow于2014年发明&#xff0c;并在论文Generative Adve…

Java中HashMap详解:hash原理、扩容机制、线程不安全及源码分析

前言 HashMap 是 Java 中常用的数据结构之一&#xff0c;用于存储键值对。在 HashMap 中&#xff0c;每个键都映射到一个唯一的值&#xff0c;可以通过键来快速访问对应的值&#xff0c;算法时间复杂度可以达到 O(1)。 HashMap 的实现原理是基于哈希表的&#xff0c;它的底层是…

Java语言程序设计——篇四(1)

类和对象 面向对象概述面向过程与面向对象面向对象基本概念面向对象的基本特征面向对象的优势及应用 为对象定义类类的修饰符成员变量成员变量-修饰符 构造方法⭐️成员方法成员方法-修饰符例题讲解 ⚠️理解栈和堆 面向对象概述 两种程序设计方法 结构化程序设计&#xff0c…

方波的傅里叶变换及方波的MATLAB实现

一、傅里叶变换简介 傅里叶变换&#xff0c;表示能将满足一定条件的某个函数表示成三角函数&#xff08;正弦和/或余弦函数&#xff09;或者它们的积分的线性组合。傅里叶变换是一种线性的积分变换。它的理论依据是&#xff1a;任何连续周期信号都可以由一组适当的正弦曲线组合…

Mac安装stable diffusion 工具

文章目录 1.安装 Homebrew2.安装 stable diffusion webui 的依赖3.下载 stable diffusion webui 代码4.启动 stable diffusion webui 本体5.下载模型6.这里可能会遇到一个clip-vit-large-patch14报错 参考&#xff1a;https://brew.idayer.com/install/stable-diffusion-webui/…

Unity 优化合集

1️⃣ 贴图优化 1. Read/Write Enable 这个属性勾选后允许你在运行时读取和写入纹理数据&#xff0c;这对于需要实时生成内容或者需要动态修改纹理的场合非常有用但在大部分情况下这是不必要的。如果打开这个属性&#xff0c;会使运行时贴图大小翻倍&#xff0c;内存中会额外…

浅谈C嘎嘎入门基础

看到这篇文章的童鞋或许会有疑惑&#xff0c;这不是之前 已经出过了吗&#xff0c;是的但是之前那篇文章可能不太好理解&#xff0c;因此我再写一篇便于大家理解的文章 那么上一篇文章已经帮大家过渡到C嘎嘎了&#xff0c;那么这篇文章我们继续讲解C嘎嘎的知识点。 C嘎嘎中的引…

牛客:DP25 删除相邻数字的最大分数(动态规划)

文章目录 1. 题目描述2. 解题思路3. 代码实现 1. 题目描述 2. 解题思路 题目的意思是选择某一个数&#xff0c;就会得到这个数 * 它出现的次数的分数&#xff0c;因此为了能快速统计出选择某一个数所能得到的分数&#xff0c;我们可以在输入数据时&#xff0c;使用一个数据来计…