Java内存分配

运行时数据区域

在这里插入图片描述

程序计数器

  1. 程序计数器可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的的字节码指令,它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都依赖这个计数器完成。
  2. 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码的指令地址。如果正在执行的是本地(Native)方法,这个计数器的值应为空。
  3. 此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

JAVA虚拟机栈

  1. Java虚拟机栈是线程私有的,它的生命周期与线程相同。
  2. Java虚拟机栈里面存放很多栈帧。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储 局部变量表,操作数栈,动态链接,方法出口等信息。 每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
  3. 在编译Java源码的时候,栈帧需要多大的局部变量表,需要多深的操作数栈已经被分析计算了出来,并写入到方法表的Code属性之中。换而言之,一个栈桢需要分配多少内存,并不会收到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的占内存布局形式。
    因为:比如递归的时候,都知道栈的深度受传进来参数的影响。正确想法是:一直递归的话,会一直调用这个方法,就会产生很多栈帧,但每个栈帧的操作数栈的深度是只和变量和程序代码有关系的。(举个例子分析下gcd函数的调用) 这样的话,递归次数过多,栈帧过多,导致StackOverflowError。
  4. 一个线程的方法调用链可能会很长(我猜这儿说的是递归),以Java程序员的角度来看,同一时刻,同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”,与这个栈帧所关联的方法成为“当前方法”。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
    在这里插入图片描述

局部变量表

  1. 局部变量表是一组变量值的存储空间,用于存放方法参数方法内部定义的局部变量。其容量以变量槽为最小单位(《Java虚拟机规范》并没有明确指出一个变量槽应占用的内存空间的大小)。
  2. 当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被static修饰的方法),那局部变量表的第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽。
  3. 为了节省栈帧所耗用的内存空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体。如果当前字节码PC计数器的值已经超过了某个变量的作用域,那么这个变量就可以交给其他变量重用。
    副作用:某些情况下变量槽的复用会直接影响到系统的垃圾收集行为。(举例如下)
  4. 1.类加载的准备阶段,进行内存分配的仅包括类变量(而不包括实例变量,实例变量将会在对象实例化时跟随对象一起分配在Java堆中) 准备阶段过后,类变量的初始值分别赋值为0,null或false等等。不过 准备阶段会将字段属性表中存在ConstantValue属性的字段初始化为ConstantValue属性所指定的初始值。
    2.类加载的初始化阶段,就是执行类构造< clinit>()方法的过程。< clinit>()编译器自动收集所有类变量的赋值动作和静态语句块的语句合并产生的。(类加载具体见另一篇博客)
    3.字节码执行过程时,遇到一条字节码new指令时,首先去检查这个指令的参数是否能在常量池定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析,初始化。如果没有,那必须先执行相应的类加载过程。类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。(怎么分配对象的内存以及类的加载过程具体见另一篇博客) new指令后一般会跟随invokespecial指令(Java编译器遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),invokespecial指令用于调用实例构造器()方法、私有方法和父类中的方法。new指令之后会接着执行()方法,按照程序员的意愿对对象初始化。调用无参构造函数的话就是0、null、false等等。
    总而言之:局部变量表没有赋初始值的话,访问会报错。因为他会在字节码文件的方法表的Code属性中的code用来存储Java源程序编译后生成的字节码指令。里面指令iconst等指令(假如没有给局部变量赋值 就不会有这些指令 但也会为之分配相应的变量槽滴) 的意思是 直接将代码中的局部变量的值放入变量槽中。全程并没有对局部变量初始化的(调用方法时虚拟机分配栈帧,里面有局部变量表,但并没有初始化这部分内存成0、null、false等等)

操作数栈

  1. 在做算术运算的时候是通过将运算涉及的操作数压入栈顶后调用运算指令来进行的。
  2. 调用其他方法的时候是通过操作数栈顶来进行方法参数的传递。
  3. 方法有返回值的时候,把返回值放入操作数栈顶,并返回。
    在这里插入图片描述
    在这里插入图片描述

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。(我觉着这里是因为 重写 运行的时候才知道调用哪个方法。而静态解析时,调用这个方法本来指令参数都会有指向常量池的该方法的引用。动态连接时,生成的invokevirtual指令后面跟的 一个指向运行时常量池 被什么类修饰的这个类的所属的方法的引用 是无效的,所以栈帧需要一个位置存储 一个该指令运行时解析完成后返回的直接引用()这个才是正确的引用 )

方法返回地址

  1. 一个方法使用异常出口的方式退出,是不会给它的上层调用者提供任何返回值。
  2. 方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器的值,而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。

本地方法栈

本地方法栈与虚拟机栈的区别只是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)服务。

Java堆

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。Java世界里“几乎”所有的对象实例都在这里分配内存。

方法区

方法区用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此,我们得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现。

  1. 在JDK1.7之前,hotspot虚拟机对方法区的实现为永久代。
  2. 在JDK1.7 字符串常量池、静态变量 被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代。
  3. 在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池、静态变量还在堆中, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
  4. ----- 运行时常量池也是方法区的一部分。Class文件的其中一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容在类加载后存放到方法区的 运行时常量池。
    -----运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生。运行期间也可以将新的常量放入池中。如 String类的intern()方法。(挖坑)
  5. 比较常见的是:方法区中有类的虚方法表。
    原因:动态分派的方法的盘本选择过程需要运行时在接受这类型的方法的元数据中搜索合适的目标方法,因此,Java虚拟机实现基于执行性能的考虑,真正运行时一般不会如此频繁的去反复搜索类型元数据。面对这种情况,一种基础而常见的优化手段是为类型在方法区中建立一个虚方法表,与此对应的,在invokeinterface执行时也会用到接口方法表,使用虚方法表索引来代替元数据查找以提高性能。

    为何使用元空间替代永久代

    个人觉得有以下几点原因:
    1. 持久代很难进行调优:JVM加载的class的总数,方法的多少(虚方法表不是大多也放在方法区嘛) 等都很难确定,因此对永久代的大小的制定难以确定。太小的永久代导致永久代内存溢出,太大的永久代则容易导致虚拟机内存紧张。
    2. 降低GC难度:方法区的垃圾收集主要回收两部分的内容:废弃的常量和不再使用的类型。而判定一个类型是否属于“不再被使用的类”的条件比较苛刻。放在永久代加大了GC的难度。而用元空间实现的话:每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。当一个类加载器被垃圾收集器标记为不再存活,其对应的元空间会被回收,在元空间的回收过程中没有重定位和压缩等操作。但是元空间内的元数据会进行扫描来确定Java的引用。

    为什么字符串常量池要放到堆区

    原因:开发中会有大量的字符串被创建,永久代满了就会发生full gc。导 致gc停顿时间太长,将它放到堆里,可及时回收内存。

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目标检测(Object Detection)是计算机视觉领域的一个核心问题,其主要任务是找出图像中所有感兴趣的目标(物体),并确定它们的类别和位置。以下是对目标检测的详细阐述: 一、基本概念 目标检测的任务是解决“在哪里?是什么?”的问题,即定位出图像中目标的位置并识别出目标的类别。由于各类物体具有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具挑战性的任务之一。 二、核心问题 目标检测涉及以下几个核心问题: 分类问题:判断图像中的目标属于哪个类别。 定位问题:确定目标在图像中的具体位置。 大小问题:目标可能具有不同的大小。 形状问题:目标可能具有不同的形状。 三、算法分类 基于深度学习的目标检测算法主要分为两大类: Two-stage算法:先进行区域生成(Region Proposal),生成有可能包含待检物体的预选框(Region Proposal),再通过卷积神经网络进行样本分类。常见的Two-stage算法包括R-CNN、Fast R-CNN、Faster R-CNN等。 One-stage算法:不用生成区域提议,直接在网络中提取特征来预测物体分类和位置。常见的One-stage算法包括YOLO系列(YOLOv1、YOLOv2、YOLOv3、YOLOv4、YOLOv5等)、SSD和RetinaNet等。 四、算法原理 以YOLO系列为例,YOLO将目标检测视为回归问题,将输入图像一次性划分为多个区域,直接在输出层预测边界框和类别概率。YOLO采用卷积网络来提取特征,使用全连接层来得到预测值。其网络结构通常包含多个卷积层和全连接层,通过卷积层提取图像特征,通过全连接层输出预测结果。 五、应用领域 目标检测技术已经广泛应用于各个领域,为人们的生活带来了极大的便利。以下是一些主要的应用领域: 安全监控:在商场、银行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值