JVM 把内存分为若干个不同的区域,这些区域有些是线程私有的,有些则是线程共享的,Java 内存区域也叫做运行时数据区,它的具体划分如下:

虚拟机栈

  Java 虚拟机栈是线程私有的数据区Java 虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。方法在执行过程中,会在虚拟机栈中创建一个 栈帧(stack frame)。每个方法执行的过程就对应了一个入栈和出栈的过程。
在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。

一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。

栈帧是方法运行时的基础数据结构

在Java虚拟机规范中,对虚拟机栈规定了两种异常状况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  2. 如果虚拟机栈可以动态扩展( 当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈 ),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

局部变量表

局部变量表是变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在java编译成class文件的时候,就在方法的Code属性的max_locals数据项中确定该方法需要分配的最大局部变量表的容量。

局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放32位(4 字节)以内的数据类型( boolean、byte、char、short、int、float、reference和returnAddress八种)

对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。

reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。

Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。

系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。

操作数栈

操作数栈和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。

操作数栈的每一个元素可用是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型占用的栈容量为2。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。

在概念模型里,栈帧之间是应该是相互独立的,不过大多数虚拟机都会做一些优化处理,使局部变量表和操作数栈之间有部分重叠,这样在进行方法调用的时候可以直接共用参数,而不需要做额外的参数复制等工作。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中方法的符号引用为参数。

这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态方法,私有方法等),这种转化称为静态解析,另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接

静态解析
静态解析的四种情形:
1.静态方法
2.父类方法
3.构造方法
4.私有方法(无法被重写)
5.final修饰的方法

动态连接
有些符号引用则是每次运行期间转化为直接引用,这种转换叫做动态链接.这体现为Java的多态性.
相关字节码指令:
1.invokeinterface—调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的那个对象的特定方法
2.invokestatic—调用静态方法
3.invokespecial—调用自己的私有方法,构造方法(以及父类的方法)
4.invokevirtual—调用虚方法,存在运行期动态查找的过程
5.invokedynamic—动态调用方法

方法返回地址

一个方法开始执行后,只有两种方式可以退出这个方法:
1.执行引擎遇到任意一个方法返回的字节码指令
传递给上层的方法调用者,是否有返回值和返回值类型将根据遇到何种方法来返回指令决定,这种退出的方法称为正常完成出口

2.方法执行过程中遇到异常
无论是java虚拟机内部产生的异常还是代码中throw出的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出的方式称为异常完成出口,一个方法若使用该方式退出,是不会给上层调用者任何返回值的。

无论使用那种方式退出方法,都要返回到方法被调用的位置,程序才能继续执行。方法返回时可能会在栈帧中保存一些信息,用来恢复上层方法的执行状态。

一般方法正常退出的时候,调用者的pc计数器的值可以作为返回地址,帧栈中很有可能会保存这个计数器的值作为返回地址。

方法退出的过程就是栈帧在虚拟机栈上的出栈过程,因此退出时的操作可能有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者的操作数栈每条整pc计数器的值指向调用该方法的后一条指令。

本地方法栈

  本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是 Java 中使用 native 关键字修饰的方法所存储的区域。

与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常。

程序计数器

  程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线
程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能通过一些更高效的方式去实现 )

  程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都通过程序计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。
因此为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指合的地址:
如果正在执行的是Native方法这个计数器值则为空(Undefined )。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

方法区

  方法区是各个线程共享的内存区域,别名叫做Non-Heap( 非堆 )。

  对于HotSpot虚拟机方法区称为“永久代”( Permanent Generation ),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。

对于其他虚拟机( 如BEAJRockit、IBMJ9等)来说是不存在永久代的概念的。

《深入理解Java虚拟机》书中对方法区(Method Aera)存储内容描述如下:它用于存储已被虚拟机加载的类型信息常量静态变量即时编译器编译后的代码缓存域信息方法信息等。

类型信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

1.类型的完整有效名称(全名=包名.类名);
2.类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类);
3.类型的修饰符(public,abstract,final的某个子集);
4.类型直接接口的一个有序列表;

域(Field)(属性)信息

1.JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

2.域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

方法(Method)信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

1.方法名称;
2.方法的返回类型(或void);
3.方法参数的数量和类型(按顺序);
4.方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的某个子集);
5.方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外);
6.异常表(abstract和native方法除外)。
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。

静态变量(non-final的类变量)

1.静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。

2.类变量被类的所有实例共享,即使没有类实例时你也可以访问它。

静态变量(全局常量static+final)

被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

常量池

实际上分为两种形态:常量池(静态常量池 Constant Pool Table)和运行时常量池(Runtime Constant Pool)。

常量池(静态常量池 Constant Pool Table),每个class有一份,存在于字节码文件中。常量池中有字面量(数量值、字符串值)和符号引用(类符号引用、字段符号引用、方法符号引用),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

运行时常量池(Runtime Constant Pool),每个class一份,存在于方法区中(JDK8以后为元空间)。当类加载到内存中后,JVM就会将class常量池中的内容存放到运行时常量池中,经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是下面的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

字符串常量池(堆中字符串常量池 String Pool),每个JVM中只有一份,存在于堆。全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到String Pool中(String Pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。

在HotSpot VM里实现的String Pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(用双引号括起来的引用而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。

方法区和永久代

说到方法区就联想到永久代,他们之间什么关系呢?

《Java虚拟机规范》只是规定了有方法区这个概念和它的作用,并没有规定如何去实现它。

不同的 JVM 上方法区的实现有所区别。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区

永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。其他的虚拟机实现并没有永久带这一说法。

各版本JVM方法区变化

版本 说明
jdk1.6及以前 有永久代(permanent generation),静态变量存放在永久代上
jdk1.7 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中
jdk1.8 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆

配置参数

JVM 1.7即以前:

#方法区初始大小
#默认是物理内存的1/64
-XX:PermSize=32m#方法区最大大小,超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen
#默认是物理内存的1/4
-XX:MaxPermSize=64m

1.8以后使用了Metaspace,使用PermSize参数配置出现提示:
Java HotSpot™ Client VM warning: ignoring option PermSize=32m; support was removed in 8.0
Java HotSpot™ Client VM warning: ignoring option MaxPermSize=64m; support was removed in 8.0

Metaspace元空间

JDK1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了MetaSpace(元空间)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。

Java8为什么要将永久代替换成Metaspace?

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

参数配置:

# 初始Metaspace元空间的大小
-XX:MetaspaceSize=128m # 最大Metaspace元空间大小,默认是没有限制
-XX:MaxMetaspaceSize=128m

  堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,几乎所有的对象实例都会分配在堆上。JDK 1.7后,字符串常量池从永久代中剥离出来,存放在堆中。

随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

堆内存划分

Eden、S0、S1归为Young区(Young Gen),即新生代,执行new时大部分对象在此分配内存,经过一定GC次数(默认15次)后进入old区。

大部分对象在Eden区中生成,当Eden区满时,还存活的对象将被复制到Survivor区,当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代。

Eden和survivor(S0、S1)默认比例是8:1:1。

old区(Old Gen)即老年代。

堆内存GC


堆是垃圾回收机制的重点区域。垃圾回收机制有三种:minor gc,major gc 和full gc

年轻代中存在的对象是死亡非常快的。存在朝生夕死的情况。

为了提高年轻代的垃圾回收效率,又将年轻代划分为三个区域,一个eden和两个S0(sunrvivor) S1(from)。

堆内存配置参数

#初始总堆内存,推荐和最大堆内存一样大
-Xms512m
#最大总堆内存
-Xmx512m#新生代(Eden + 2*S)与老年代的比值
# 2:新生代占总堆大小1/3,老年代总堆大小2/3
# 3:新生代占总堆大小1/4,老年代总堆大小3/4
-XX:NewRatio=2#S0,S1和Eden比值,S0,S1的比例固定为1/X ,X=SurvivorRatio+1+1
#默认Eden:S0:S1比值8:1:1
#配置6,Eden:S0:S1比值6:1:1
-XX:SurvivorRatio=8

执行以下配置验证:

-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms512m -Xmx512m -XX:NewRatio=4 -XX:SurvivorRatio=6

执行结果如下:

Heap Configuration:MinHeapFreeRatio         = 40MaxHeapFreeRatio         = 70MaxHeapSize              = 536870912 (512.0MB)NewSize                  = 107347968 (102.375MB)MaxNewSize               = 107347968 (102.375MB)OldSize                  = 429522944 (409.625MB)NewRatio                 = 4SurvivorRatio            = 6MetaspaceSize            = 134217728 (128.0MB)CompressedClassSpaceSize = 1073741824 (1024.0MB)MaxMetaspaceSize         = 134217728 (128.0MB)G1HeapRegionSize         = 0 (0.0MB)Heap Usage:
New Generation (Eden + 1 Survivor Space):capacity = 93978624 (89.625MB)used     = 14530664 (13.857521057128906MB)free     = 79447960 (75.7674789428711MB)15.461669240869073% used
Eden Space:capacity = 80609280 (76.875MB)used     = 14530664 (13.857521057128906MB)free     = 66078616 (63.017478942871094MB)18.026043651549795% used
From Space:capacity = 13369344 (12.75MB)used     = 0 (0.0MB)free     = 13369344 (12.75MB)0.0% used
To Space:capacity = 13369344 (12.75MB)used     = 0 (0.0MB)free     = 13369344 (12.75MB)0.0% used
tenured generation:capacity = 429522944 (409.625MB)used     = 0 (0.0MB)free     = 429522944 (409.625MB)0.0% used2070 interned Strings occupying 158440 bytes.

NewRatio=4
老年代大小为总堆大小4/5,即512*4/5=409.6(结果输出:409.625MB);
年轻代大小为总堆大小1/5,即512*1/5=102.4(结果输出:102.375MB)。

SurvivorRatio=6
Eden占young区6/8,即102.375*6/8=76.78125(结果输出:76.875MB);
S0占young区1/8,即102.375*1/8=12.796875(结果输出:12.75MB)。

直接内存

直接内存( Direct Memory) 并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

例如在JDK 1.4中新加入了NIO ( New Input/Output )类,引入了一种基于通道( Channel) 与缓冲区( Bufer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectBvteBufer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

服务器直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存( 包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。

服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制( 包括物理的和操作系统级的限制)从而导致动态扩展时出现OutOfMemoryError异常。

内存分配方式

  在类加载完成后,虚拟机需要为新生对象分配内存,为对象分配内存相当于是把一块确定的区域从堆中划分出来,这就涉及到一个问题,要划分的堆区是否规整。

  假设 Java 堆中内存是规整的,所有使用过的内存放在一边,未使用的内存放在一边,中间放着一个指针,这个指针为分界指示器。那么为新对象分配内存空间就相当于是把指针向空闲的空间挪动对象大小相等的距离,这种内存分配方式叫做指针碰撞(Bump The Pointer)

  如果 Java 堆中的内存并不是规整的,已经被使用的内存和未被使用的内存相互交错在一起,这种情况下就没有办法使用指针碰撞,这里就要使用另外一种记录内存使用的方式:空闲列表(Free List),空闲列表维护了一个列表,这个列表记录了哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

  所以,上述两种分配方式选择哪个,取决于 Java 堆是否规整来决定。在一些垃圾收集器的实现中,Serial、ParNew 等带压缩整理过程的收集器,使用的是指针碰撞;而使用 CMS 这种基于清除算法的收集器时,使用的是空闲列表

JVM-内存区域 堆、方法区,虚拟机栈、程序计数器详解相关推荐

  1. Java必突-JVM知识专题(一): Java代码是如何跑起来的+类加载到使用的过程+类从加载到使用核心阶段(类初始化)+类加载的层级结构+什么是JVM的内存区域划分?Java虚拟机栈、Java堆内存

    前言: 该章节知识点梳理:本文主要是入门和了解jvm,不做深入 1.Java代码是如何运行起来的? 2.类加载到使用的过程? 3.验证准备和初始化的过程? 4.类从加载到使用核心阶段:初始化.类加载器 ...

  2. jvm中方法区和常量池详解_JVM——内存区域:运行时数据区域详解

    关注微信公众号:CodingTechWork,一起学习进步. 引言 我们经常会被问到一个问题是Java和C++有何区别?我们除了能回答一个是面向对象.一个是面向过程编程以外,我们还会从底层内存管理和垃 ...

  3. JVM 内存模型:方法区(Method Area)

    1. 前言 最近在研究Java语言底层基础知识,发现对于JVM 内存模型理解不够深入.笔者突发奇想深入理解JVM内存模型,特在此介绍详细介绍一下JVM内存模型中的方法区.看似名称叫做方法区,其实不然. ...

  4. jvm中方法区和常量池详解_Java常量池(静态常量池与运行时常量池)

    1.什么是常量 用final修饰的成员变量表示常量,值一旦给定就无法改变! final修饰的变量有三种:静态变量.实例变量和局部变量,分别表示三种类型的常量. Java中的常量池,实际上分为两种形态: ...

  5. java上帝模块常见的情况_JVM上帝视角看JVM内存模型,分而治之论各模块详情详解...

    1. 上帝视角 [树看JVM] [图看JVM] 2. 分而治之 2.1 堆区 构成:堆区由新生代和老年代组成,新生代中包含伊甸区(Eden).幸存者区(survivor from .survivor ...

  6. JVM学习笔记之-方法区,栈、堆、方法区的交互关系,方法区的理解,设置方法区大小与OOM,方法区的内部结构,方法区使用举例

    栈.堆.方法区的交互关系 运行时数据区结构图 从线程共享与否的角度来看 栈,堆,方法区的交互关系 方法区的理解 方法区在哪里? <Java虚拟机规范>中明确说明:"尽管所有的方法 ...

  7. Java虚拟机------JVM内存区域

    JVM内存区域运行时数据区域分为两种: JVM内存区域 运行时数据区域分为两种: 线程隔离的数据区: 程序计数器 Java虚拟机栈 本地方法栈 所有线程程共享的数据区: Java堆 方法区 JVM 内 ...

  8. 四.运行时数据区-本地方法栈(Native Method Stack)-堆-方法区

    1. 前言:本地方法接口 1.1 本地方法 简单来讲,一个Native Method就是一个java调用非java代码的接口,一个Native Method 是这样一个java方法:该方法的实现由非J ...

  9. JVM—堆栈 堆 方法区 静态区 final static 内存分配

    原文作者:一夜丶鱼龙舞 原文地址:JAVA 堆栈 堆 方法区 静态区 final static 内存分配 详解(转) 一.栈(stack)和堆(heap) (1)内存分配的策略 按照编译原理的观点,程 ...

最新文章

  1. 2008年六大创新Web技术(1)
  2. php-fpm进程利用CPU不均问题的优化过程
  3. hdu 1134 卡特兰数(大数模板)
  4. java oauth2搭建_Spring Security 实战干货:OAuth2授权请求是如何构建并执行的
  5. 织梦网站如何上传服务器还原,织梦系统DedeCMS网站通过数据库备份、还原实现网站整站搬家移植...
  6. 第4次作业类测试代码 021
  7. LINUX内核的进程调度策略
  8. 基于Dlib库构建人脸识别数据集
  9. 干货 | 外文文献哪里找?八大网站免费下载!
  10. 斐讯k2路由器刷固件一条龙——从小白到大神
  11. JS设计模式之工厂模式
  12. Jenkins build light on Windows, with Blinky
  13. PHP微信怎么计步数,微信运动怎么关注好友步数(微信运动计步功能使用方法介绍)...
  14. 手机端和pc端浏览器兼容性问题
  15. 数据结构实践(有的数据结构课后习题答案),红色是答案
  16. python实验日记
  17. MyBatis从入门到精通__刘增辉(著)_ 电子工业出版社PDF下载
  18. 网络编程全解(Java)
  19. python流程图怎么画-Python使用graphviz画流程图过程解析
  20. 汇川PLC单位换算及电子齿轮比

热门文章

  1. iphone开发中内存利用说明
  2. linux pptpd 不能上网,CentOS7.7使用pptpd搭建服务器(配置好了,但是不能使用)
  3. matlab怎么绘制零极点,matlab中画系统零极点的方法
  4. 互联网行业的裁员潮;程序员到35岁是个坎儿!
  5. 程序员年近三十,如何抉择,我打个样!
  6. 设计模式之禅【访问者模式】
  7. PCAN CANable CANDLELight USB转CAN CAN盒
  8. 微信内链接已禁止访问是什么情况?微信链接防封细节
  9. cvpr2020 人脸检测与识别_就CVPR2020的来看,目前人工智能的研究热点有哪些进展?未来的研究趋势会有什么变化?...
  10. python 矩阵元素查找位置函数_Python中二维数组中非零元素位置的快速查找方法...