爆款云主机2核4G限时秒杀,88元/年起!
查看详情

活动

天翼云最新优惠活动,涵盖免费试用,产品折扣等,助您降本增效!
热门活动
  • 618智算钜惠季 爆款云主机2核4G限时秒杀,88元/年起!
  • 免费体验DeepSeek,上天翼云息壤 NEW 新老用户均可免费体验2500万Tokens,限时两周
  • 云上钜惠 HOT 爆款云主机全场特惠,更有万元锦鲤券等你来领!
  • 算力套餐 HOT 让算力触手可及
  • 天翼云脑AOne NEW 连接、保护、办公,All-in-One!
  • 中小企业应用上云专场 产品组合下单即享折上9折起,助力企业快速上云
  • 息壤高校钜惠活动 NEW 天翼云息壤杯高校AI大赛,数款产品享受线上订购超值特惠
  • 天翼云电脑专场 HOT 移动办公新选择,爆款4核8G畅享1年3.5折起,快来抢购!
  • 天翼云奖励推广计划 加入成为云推官,推荐新用户注册下单得现金奖励
免费活动
  • 免费试用中心 HOT 多款云产品免费试用,快来开启云上之旅
  • 天翼云用户体验官 NEW 您的洞察,重塑科技边界

智算服务

打造统一的产品能力,实现算网调度、训练推理、技术架构、资源管理一体化智算服务
智算云(DeepSeek专区)
科研助手
  • 算力商城
  • 应用商城
  • 开发机
  • 并行计算
算力互联调度平台
  • 应用市场
  • 算力市场
  • 算力调度推荐
一站式智算服务平台
  • 模型广场
  • 体验中心
  • 服务接入
智算一体机
  • 智算一体机
大模型
  • DeepSeek-R1-昇腾版(671B)
  • DeepSeek-R1-英伟达版(671B)
  • DeepSeek-V3-昇腾版(671B)
  • DeepSeek-R1-Distill-Llama-70B
  • DeepSeek-R1-Distill-Qwen-32B
  • Qwen2-72B-Instruct
  • StableDiffusion-V2.1
  • TeleChat-12B

应用商城

天翼云精选行业优秀合作伙伴及千余款商品,提供一站式云上应用服务
进入甄选商城进入云市场创新解决方案
办公协同
  • WPS云文档
  • 安全邮箱
  • EMM手机管家
  • 智能商业平台
财务管理
  • 工资条
  • 税务风控云
企业应用
  • 翼信息化运维服务
  • 翼视频云归档解决方案
工业能源
  • 智慧工厂_生产流程管理解决方案
  • 智慧工地
建站工具
  • SSL证书
  • 新域名服务
网络工具
  • 翼云加速
灾备迁移
  • 云管家2.0
  • 翼备份
资源管理
  • 全栈混合云敏捷版(软件)
  • 全栈混合云敏捷版(一体机)
行业应用
  • 翼电子教室
  • 翼智慧显示一体化解决方案

合作伙伴

天翼云携手合作伙伴,共创云上生态,合作共赢
天翼云生态合作中心
  • 天翼云生态合作中心
天翼云渠道合作伙伴
  • 天翼云代理渠道合作伙伴
天翼云服务合作伙伴
  • 天翼云集成商交付能力认证
天翼云应用合作伙伴
  • 天翼云云市场合作伙伴
  • 天翼云甄选商城合作伙伴
天翼云技术合作伙伴
  • 天翼云OpenAPI中心
  • 天翼云EasyCoding平台
天翼云培训认证
  • 天翼云学堂
  • 天翼云市场商学院
天翼云合作计划
  • 云汇计划
天翼云东升计划
  • 适配中心
  • 东升计划
  • 适配互认证

开发者

开发者相关功能入口汇聚
技术社区
  • 专栏文章
  • 互动问答
  • 技术视频
资源与工具
  • OpenAPI中心
开放能力
  • EasyCoding敏捷开发平台
培训与认证
  • 天翼云学堂
  • 天翼云认证
魔乐社区
  • 魔乐社区

支持与服务

为您提供全方位支持与服务,全流程技术保障,助您轻松上云,安全无忧
文档与工具
  • 文档中心
  • 新手上云
  • 自助服务
  • OpenAPI中心
定价
  • 价格计算器
  • 定价策略
基础服务
  • 售前咨询
  • 在线支持
  • 在线支持
  • 工单服务
  • 建议与反馈
  • 用户体验官
  • 服务保障
  • 客户公告
  • 会员中心
增值服务
  • 红心服务
  • 首保服务
  • 客户支持计划
  • 专家技术服务
  • 备案管家

了解天翼云

天翼云秉承央企使命,致力于成为数字经济主力军,投身科技强国伟大事业,为用户提供安全、普惠云服务
品牌介绍
  • 关于天翼云
  • 智算云
  • 天翼云4.0
  • 新闻资讯
  • 天翼云APP
基础设施
  • 全球基础设施
  • 信任中心
最佳实践
  • 精选案例
  • 超级探访
  • 云杂志
  • 分析师和白皮书
  • 天翼云·创新直播间
市场活动
  • 2025智能云生态大会
  • 2024智算云生态大会
  • 2023云生态大会
  • 2022云生态大会
  • 天翼云中国行
天翼云
  • 活动
  • 智算服务
  • 产品
  • 解决方案
  • 应用商城
  • 合作伙伴
  • 开发者
  • 支持与服务
  • 了解天翼云
      • 文档
      • 控制中心
      • 备案
      • 管理中心

      jvm专题(2) - 内存结构

      首页 知识中心 存储 文章详情页

      jvm专题(2) - 内存结构

      2023-05-04 09:50:42 阅读次数:111

      jvm

      本章笔者会重新组织下语言,着重讲下JVM的内存结构。此章会贯穿JDK1.6到JDK1.8的内容,最后会阐述下类初始化的过程,从原理上了解JVM的内存分配机制,本章内容比较基础但非常重要,它是优化代码和JVM调优的基本一定要牢记。后续会专题讲解JVM调优的实操,本章相当于授渔,后续章节相当于授鱼吧。

           JVM通俗来讲有三种不同的解释:1、一套抽象的规范;2、一个具体的规范实现,分为硬件和软件实现;3、一个运行中的java实例。正常我们指的是第三种:运行在一个jvm实现上的java程序。在同一计算机上同时运行三个java程序,将得到三个java虚拟机实例,每个java程序都运行于它自己的java VM中。

       

      一、概述

      1.1、体系结构

      jvm专题(2) - 内存结构

      1.2、内存模型

      jvm专题(2) - 内存结构

      • 不同的虚拟机有不同的内存实现机制,实现逻辑大概和上图一样,也可以认为上图就是事实上的标准。每个JVM实例都有一个方法区和一个堆区,它们是可以被此JVM实例中所有线程共享的,当JVM装载一个.class文件时,会把从.class文件中解析的二进制数据放在方法区中,把程序运行时把所有在运行时创建的对象放在堆中。
      • 当一个新线程创建时,都将得到一个私有的PC寄存器(程序计数器)和一个栈,PC寄存器的值总是指示下一条将被执行的指令,而栈总是存储此线程中java方法调用的状态,包括局部变量、参数、返回值及中间结果等。每个线程的栈区是私有的,任何线程都不能访问另一个线程的PC寄存器和栈。
      • java栈是由很多帧组成的,java VM没有寄存器,其指令集使用java栈来存储中间数据,目的是为了保持java VM的指令集紧凑,也有助于VM实现动态编译器和即时编辑器的代码优化。
      • 本地方法的调用的状态,是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是其它相关的内存区,因为是依赖java VM的具体实现的。

      二、内存模型详解

      2.1、共享内存区

      2.1.1、方法区

      即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静 态变量以及编译器编译后的代码等数据. HotSpot VM 把GC分代收集扩展至方法区, 使用 Java 堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理java堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。一般主要回收:

      • 常量:只在常量池中的常量没有被任何地方引用,就可以回收,比如String,所以有时对String的大量操作要谨慎;
      • 元数据:一般是应对的动态生成场景而设计的,没有被任何地方引用就会回收;

      运行时常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。java 虚拟机对.class文件的每一部分(自然也包括常量 池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

      方法区是所有线程共享的,需要注意线程安全的问题。有几点需要说明:1、方法区可以不是连续的内存空间;2、类变量也就是static变量是由所有实例共享的,访问它不需要实例化一个类是直接从方法区中取得的;3、用final修饰的类变量和普通的类变量不同,当访问final类型的类变量时java VM会把final复制一份到自己的类型常量池中。

      在1.6和1.7这块区域称为永久区由-XX:PerSize和-XX:MaxPerSize(默认64M)来指定,在1.8中去掉了永久区用-XX:MaxMetaspaceSize来代替(一个称为“元数据区”(元空间)的区域),如果不指定由会直接耗掉物理内存大小,在1.8中不建议指定其大小(除非存在应用混部的情况)。元空间的本质和永久代类似,它们之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。

      2.1.2、Heap堆区

           java程序在运行时创建的所有实例和数组(在java VM中数组是当做一个对象)都存放在堆中。一个java VM只有一个堆空间,它被所有线程共享的,同样需要注意线程安全的问题。在堆中有创建新对象的指令、但没有释放对象的指令码,在程序运行时堆和方法区都是可以动态扩展的。堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代,新生代分三个区是为了减少碎片,GC一次只会回收eden区和其中一个survivor区。默认的堆区结构和内存分配如下图所示:

      jvm专题(2) - 内存结构

      Young区内存分配
      • 对象在Eden Space创建(正常情况下如此),当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放掉;
      • 当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时JVM GC停止(此处的停止可参考STW机制是一个相对的停止)所有在堆中运行的线程并执行清除动作。
      Old区内存分配
      • 初创对象全在eden区,只有GC才会离开此区域;但当对象体积超过PretenureSizeThreashold参数设置的字节数,会绕过eden, from, to区直接在老年代中创建 ;
      • eden中对象是根据年龄来算的,一次GC年龄+1,由MaxTenuringThreshold,默认值为15,当超过15次时就会移到老年代,另一个控制参数是TargetSurvivorRatio,指新生区的使用率,默认为50%,如果GC后超过50%使用率,也会有一部分对象直接放到老年代;注意上述提到的age=15是一个上限值,jvm会根据实际情况动态调整此值。
      Tlab内存分配

      全称,线程本地分配缓存。由于堆是共享的,多个线程同时分配空间时会存在竞争,为了加快内存分配。每个线程都有一个tlab区,它直接占用的是eden空间,默认开启,也可以被禁用,下图是内存分配的全过程:

      jvm专题(2) - 内存结构

      LABRefillWasteFraction默认值为tlab区的64分之1大小;JVM默认会自动调整TLAB和LABRefillWasteFraction的大小,可通过ResizeTLAB来禁用此功能并可用TLABSize来手动指定大小,实际不建议调整。

      2.1.3、示例:堆区的典型设置

      java -Xmx3550m -Xms3550m -Xmn2g -Xss128k 
      -XX:NewRatio=4 -XX:SurvivorRatio=4
      -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
      • -Xmx3550m:设置JVM最大可用内存为3550M;
      • -Xms3550m:此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存;
      • -Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代默认大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8;
      • -Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。可根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右;
      • -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5;
      • -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6;
      • -XX:MaxPermSize=16m:设置持久代大小为16m。JDK1.8取消了PermGen,取而代之的是Metaspace,所以PermSize和MaxPermSize参数失效,取而代之的是-XX:MetaspaceSize -XX:MaxMetaspaceSize;
      • -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

      2.2、私有内存区

      2.2.1、PC寄存器

      对于一个运行中的java程序,每一个线程都有它自己的PC寄存器,它是在此线程创建时创建的,大小为一个字长,它存储的是下一条将被执行指令的“地址”,也就是字节码的偏移地址。如果线程在执行一个本地方法,它的内存永远是“undefined”。这是内存区唯一不会报outofMemoryError的区域。

      2.2.2、stack栈

      运行时内存区,先进后出模式用于方法的压和出,当有死循环时,有可能出现StackOverflowError错误,方法区需要线程安全的设计。每当启动一个线程时,java VM都会为它分配一个java栈。以帧为单位保存线程的运行状态,VM只会直接对帧进行进栈和出栈两种操作,栈上的所有数据都是私有的。某个线程正在执行的方法称为该线程的当前方法,当前方法使用的帧称为当前帧。当前方法正常返回或抛出异常,都会弹出。上一个方法的帧变成当前帧。在线程上执行一个方法时,JVM都会在java栈中压入一个新的帧。

      私有变量分配在了栈上,好处是栈会自动销毁,不需要垃圾回收器介入,可提高性能;但是大的对象不建议在栈上分配,因为其空间小,栈的大小是在编译时指定的。用-Xss来指定。

      栈帧

      栈帧的结构如下图所示:

      ​

      jvm专题(2) - 内存结构

      类型在方法区中的结构:全限定名,直接超类的全限定名+类型(类||接口)包括它的结构信息、修饰符、超类的有序列表、一个到类ClassLoader的引用、一个到Class类的引用(即创建一个java.lang.Class类的实例)。每个帧都有自己的局部变量表和指向常量池的引用;它随着方法创建或结束而创建或销毁,如果stackoutException一般是由于死循环引起的。一个帧栈由三部分组成:

      • 局部变量区:是一个数组,存储方法的参数和临时变量。需要注意的是对于比int短的数据类型在java栈中都会转变为Int再进行运算,存回方法区时再转换为原来的类型;java栈中不会拷贝对象,只是存储到堆区的引用;
      • 操作数栈:是一个数组结构,存储中间计算结果,它不是通过索引来访问,而是标准的栈操作来访问的,A指令可能把数据压入栈中,稍后B指令可能执行出栈操作再取来;
      • 帧数据区:除了以上两个数据区后,java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常机制派发,当需要用到常量池中的数据时,就会通过存放在此帧数据区的指针来访问(如果是普通数据则直接取出压入栈中);

      2.2.3、本地方法栈

      本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为 Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个 C 栈。运行中的每一个java线程都有一个私有的执行引擎实例,这个执行引擎会执行两种操作:字节码或是本地方法(JNI,本地方法视为一种扩展),它主要操作的是操作数栈。HostSpot-JVM把本地方法堆栈和JVM堆栈合二为一了。

      当java程序直接调用本地方法(一般用C语言实现),当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受VM限制的世界,本地方法可以通过本地方法接口来访问虚拟机的运行时数据区。java调用本地方法时不会操作栈,而是一个动态链接。本地方法执行后会有一个返回值或异常,它也可以回调java方法。

      2.3、直接内存区

      这是类似堆的一块内存区,不是 JVM 运行时数据区的一部分。java的NIO库就是直接使用这块的内存区域,性能会更好。这块区域大小不受xmx限制。它的速度要快于堆,默认为Xmx的大小 ,也可以用MaxDirectMemorySize来指定。也会被GC。它适合多读的场景,比如把文件读到内存,然后多次被访问。原因是它写内存的速度要远低于堆,但它读内存的速度的要远高于堆。

      在NIO的设计中有体现,比如 在 JDK 1.4 引入的 NIO 提 供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java I/O 扩展), 这样就避免了在 Java 堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。

      三、生命周期

      3.1、基础

      数据类型

      javaVM的数据分为两种类型:基本类型和引用类型。其中基本类型中的boolean有点特别,当编译为字节码时,会用int或byte来表示boolean。涉及到boolean的值操作时则会使用int,boolean数组是当做byte数组来使用的。在基本类型中还有一个returnAddress的基本类型,程序用不了,它用来实现finally语句。

      jvm专题(2) - 内存结构

      在java VM中最基本的数据单元就是字(word),它的大小由每个VM实现的设计者来决定,但一般至少是一个byte或byte的整数倍。字长大小程序不会侦测到,但也不会影响程序的运行。

      类型解析

      jvm专题(2) - 内存结构

      详细的可看上图中的描述,有几点需要注意的如下:

      1. 在引用和被引用对象由不同的类加载器加载时,为了保证安全,这两边必须保证全路径名在方法区中一致,这样即保证了安全性也保证了一致性;
      2. 解析final常量:常量类解析为一个本地的拷贝,所以这保证了在使用switch和if时要注意线程的安全性。final是在编译时解析的,所以如果if中引用了final变量,在改变final时也要重新编译if所在的语句;
      3. 接口引用调用方法要比类引用计用方法慢很多,因为在JVM中method在方法区中会维护一个列表,外部通过引用方法偏移表来直接引用,但接口就不一定,因为外部可以持有同一个接口的不同引用,每次调用都需要在接口的实现树上找到一个适合的实现。
      4. 其它基本类型的解析就比较简单一般就是直接引用即可;

      类装载子系统

      java VM有两种类加载器:启动和用户自定义加载器,用户自定义加载器必须派生自java.lang.ClassLoader,这个类提供了访问类加载器机制的接口,用户自定义的类加载器以及Class类的实例也同样存在于堆区中,而装载的类型信息则都位于方法区。其它他们都是系统的一部分。类加载器的工作过程主要包括:1、装载,查找并装载类型的二进制数据;2、连接,执行验证,准备,以及解析(可选);3、初始化;4、使用;5、卸载;

      ClassLoader有四个方法:defineClass两个、findSystemClass、resoloveClass。defineClass方法负责把新类型导入到方法区中, resoloveClass接受defineClass返回值做为参数,对此Class执行连接动作,defineClass执行后此类就位于方法区了,因为方法区中存的是class实例。

      java VM的命名空间,其实是解析过程的结果。对于每一个被装载的类型,java VM都会记录装载它的class loader。这种机制也防止了类型混淆,即JVM除了限定全路径名以后还会限定类加载器。

      3.2、线程的生命周期

      在java VM中有两种线程:守护线程和非守护线程。守护线程通常是由虚拟机自己使用的,比如GC线程,但是java程序也允许把自定义的任何线程标记为守护线程。java程序中的初始线程(main())为非守护线程。只要还有非守护线程在运行,java VM就处于存活状态,一旦所有的非守护线程都停止了,虚拟机实例将自动退出。

      这里所说的线程是指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程,操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上,当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。Java 线程结束原生线程随之被回收。当线程结束时会释放原生线程和 Java 线程的所有资源。JVM 后台运行的系统线程主要有下面几个:

      线程分类

      描述

      虚拟机线程

      负责等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当 堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the- world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。

      周期性任务线程

      负责定时器事件(也就是中断),用来调度周期性操作的执行。

      GC线程

      支持 JVM 中不同的垃圾回收活动。

      编译器线程

      在运行时将字节码动态编译成本地平台相关的机器码。

      信号分发线程

      接收发送到 JVM 的信号并调用适当的 JVM 方法处理。

      3.3、对象的生命周期

      类一旦被加载、连接、初始化后。程序就可以访问它的静态方法、变量等。这是在堆上进行的操作。GC主要是对这个内存区进行垃圾回收,下一章节笔者会详细描述GC的内容,此小节会详细说明下类创建的过程,也把上一章节埋的一个坑填上。

      类实例化

      类实例化一般有4种途径:new()、clone()、newInstance()、java.io.ObjectInputStream().getObject()(序列化);另外还有隐式的创建,经如String运算过程中的创建。java编译器会为每个类创建一个实例化的init构造方法,如果这个构造函数中没有显示的this或super方法,则默认从超类的init方法开始调用。

      垃圾回收-对象的终结

      jvm必须实现自动的堆管理策略。程序员也可以重载finalize()方法:这个方法是由FinalizerThread线程来处理的,执行前要加入一个引用对列,对列内部是个链表结构,再执行。这也就是GC时时间不确定的原因之一;

      • gc时只会主动调用一次此类,并且此类中的任何异常都会被忽略;
      • 此类被程序员显式调用并不会影响gc过程;
      • 在这个类里也可以处理复杂的清理和对象复活任务,但不建议复活对象,原因是这个方法是由gc来调用的,无法确定其调用的时间,可以做一些清理工作;
      • 因为复活的原因,gc一般会进行二次检查来确认
      class Finale {
      protected void finalize() {
      System.out.println("A Finale object was finalized.");
      //...
      }
      //...
      }

      不建议重写finalize()方法的原因是,GC时JVM会把所有可回收的对象包装成一个java.lang.ref.Finalizer,然后放在ReferenceQueue队列中,内部为一个双向链表结构。然后 FinalizerThread线程依次执行队列中引用对象的finalize()方法,如果重写的finalize方法中存在sleep这样比较耗时的操作,就会导致很多对象来不及GC,导致oom异常。

      但有时可做为双保险使用,比如DB连接,除了程序中手动关闭后,也可以在此处在关闭一次。因为之前关了一次,所以重载的finalize中基本是什么都不做的。还有一种情况就是在分配了大量的空间后也最好进行一次显示的System.gc()调用;

      3.4、类型的生命周期

      jvm通过以下三步初始化一个java类型,使其可被当前程序可用。其中连接中的解析可以初始化后运行;一般的JVM有个规则是首次使用时才加载和初始化,有的也会提前感知预先装载。这个过程一般是在方法区上进行的;

      jvm专题(2) - 内存结构

      装载

      分三阶段完成:1、通过类型的全限定名,产生一个代表此类型的二进制数据流;2、解析内部结构;3、创建一个java.lang.Class实例;可能会占用方法区存放部分元数据;

      连接

      驱动java连接模型的引擎是解析过程,可以允许用户自定义类装载器,在运行时动态地扩展用户程序。 这个连接过程包括常量池、方法表。class文件把它所有的引用符号都保存在常量池中,这个池是独享的。解析过程中根据符号引用查找实体,再把符号引用替换成一个直接引用,主要过程如下所述:

      1. 验证:初步验证.class文件的合理性;
      2. 准备:除了分配内存外,jvm会给变量分配默认的初始值;初始化阶段再赋予程序员期望的值;
      3. 解析:在常量池中寻找类、接口、方法、字段的符号引用,再把这些符号替换成直接引用;

      理解连接模型,需要知道类的装载、检验、连接、解析、初始化这样的一个过程。

      动态连接

      jvm装载程序类和接口,并在动态连接的过程中把他们连接起来。这个连接是通过符号连接起来的,class文件被装载后都有一个内部版本的常量池(连接符号保存的地方),它和class文件的结构相对应。当引用于jvm会解析常量池中数据的入口并且只会解析一次,连接的过程主要是把符号替换成直接引用(指针),同时还要检查正确性和权限。类找不到的错误就是这个过程发现的。

      动态扩展

      在jvm中可以使用java.lang.Class.forName()和java.lang.ClassLoader.loadClass()来动态扩展。这两种的区别在于装载的命名空间不同:

      • forName():可以用参数指定是否在返回前被初始化,比如JDBC需要一个注册过程,返回前必须被初始化;它试图把类装载到当前的命名空间(系统类装载器,即classpath的路径)
      • loadClass():没有初始化操作。这种方法的扩展会提供一个命名空间,提供安全保护功能,forName()的第三个参数也可以指定一个classLoader实现安全的功能;它试图把类装载到用户自定义的装载器的命名空间里

      classLoader

      存在1.1和1.2两种版本的实现,其区别就在findClass和loadClass。前者是后者的子集,其中findClass是1.2的更容易扩展,它把loadClass的工作分离开,只负责按路径查找要加载的类,并转换成数组给defineClass方法。示例代码如下:

      public interface Greeter {
      void greet();
      }
      public class Hello implements Greeter {
      public void greet() {
      System.out.println("Hello, world!");
      }
      }
      //这个实现是java1.1的实现,是源码也是一个自定义的实现
      public class GreeterClassLoader extends ClassLoader {

      private String basePath;//用来保存目录路径

      public GreeterClassLoader(String basePath) {
      this.basePath = basePath;
      }

      public synchronized Class loadClass(String className, boolean resolveIt) throws ClassNotFoundException {
      Class result;
      byte classData[];

      // 检查要调用的类型是否被加载过
      result = findLoadedClass(className);
      if (result != null) {
      return result;
      }
      // 双亲加载
      try {
      result = super.findSystemClass(className);
      return result;
      }
      catch (ClassNotFoundException e) {
      }

      // Don't attempt to load a system file except through
      // the primordial class loader
      if (className.startsWith("java.")) {
      throw new ClassNotFoundException();
      }

      // Try to load it from the basePath directory.
      classData = getTypeFromBasePath(className);
      if (classData == null) {
      System.out.println("GCL - Can't load class: "
      + className);
      throw new ClassNotFoundException();
      }

      // 1、装载class,但不负责连接和初始化
      result = defineClass(className, classData, 0,
      classData.length);
      if (result == null) {
      System.out.println("GCL - Class format error: "
      + className);
      throw new ClassFormatError();
      }
      // 连接
      if (resolveIt) {
      resolveClass(result);
      }
      return result;
      }

      private byte[] getTypeFromBasePath(String typeName) {

      FileInputStream fis;
      String fileName = basePath + File.separatorChar
      + typeName.replace('.', File.separatorChar)
      + ".class";

      try {
      fis = new FileInputStream(fileName);
      }
      catch (FileNotFoundException e) {
      return null;
      }

      BufferedInputStream bis = new BufferedInputStream(fis);

      ByteArrayOutputStream out = new ByteArrayOutputStream();

      try {
      int c = bis.read();
      while (c != -1) {
      out.write(c);
      c = bis.read();
      }
      }
      catch (IOException e) {
      return null;
      }

      return out.toByteArray();
      }
      }
      //测试类
      public static void main(String[] args) {

      if (args.length <= 1) {
      System.out.println(
      "Enter base path and greeter class names as args.");
      return;
      }

      GreeterClassLoader gcl = new GreeterClassLoader(args[0]);

      for (int i = 1; i < args.length; ++i) {
      try {

      // Load the greeter specified on the command line
      Class c = gcl.loadClass(args[i], true);

      // Instantiate it into a greeter object
      Object o = c.newInstance();

      // Cast the Object ref to the Greeter interface type
      // so greet() can be invoked on it
      Greeter greeter = (Greeter) o;

      // Greet the world in this greeter's special way
      greeter.greet();
      }
      catch (Exception e) {
      e.printStackTrace();
      }
      }
      }
      //java 1.2的实现
      public class GreeterClassLoader extends ClassLoader {

      // basePath gives the path to which this class
      // loader appends "/<typename>.class" to get the
      // full path name of the class file to load
      private String basePath;

      public GreeterClassLoader(String basePath) {

      this.basePath = basePath;
      }

      public GreeterClassLoader(ClassLoader parent, String basePath) {

      super(parent);
      this.basePath = basePath;
      }

      protected Class findClass(String className)
      throws ClassNotFoundException {

      byte classData[];

      // Try to load it from the basePath directory.
      classData = getTypeFromBasePath(className);
      if (classData == null) {
      throw new ClassNotFoundException();
      }

      // Parse it
      return defineClass(className, classData, 0,
      classData.length);
      }

      private byte[] getTypeFromBasePath(String typeName) {

      FileInputStream fis;
      String fileName = basePath + File.separatorChar
      + typeName.replace('.', File.separatorChar)
      + ".class";

      try {
      fis = new FileInputStream(fileName);
      }
      catch (FileNotFoundException e) {
      return null;
      }

      BufferedInputStream bis = new BufferedInputStream(fis);

      ByteArrayOutputStream out = new ByteArrayOutputStream();

      try {
      int c = bis.read();
      while (c != -1) {
      out.write(c);
      c = bis.read();
      }
      }
      catch (IOException e) {
      return null;
      }

      return out.toByteArray();
      }
      }

      forName

      static public void main(String[] args) {

      if (args.length == 0) {
      System.out.println(
      "Enter greeter class names as args.");
      return;
      }

      for (int i = 0; i < args.length; ++i) {
      try {

      // Load the greeter specified on the command line
      Class c = Class.forName(args[i]);

      // Instantiate it into a greeter object
      Object o = c.newInstance();

      // Cast the Object ref to the Greeter interface type
      // so greet() can be invoked on it
      Greeter greeter = (Greeter) o;

      // Greet the world in this greeter's special way
      greeter.greet();
      }
      catch (Exception e) {
      e.printStackTrace();
      }
      }
      }

      初始化

      上述三个阶段中,只有初始化阶段是严格要求的,所有的JVM实现必须在每个类或接口首次主动使用时初始化,共有6种主动时机。其它的全是被动时机,都不会导致类被初始化。在子类初始化时,其父类必须先准备好。

      6种主动时机为:

      1. 当创建某个类的新实例时,包括new、反射、clone、反序列化;
      2. 当调用某个类的静态方法时;
      3. 当使用某个类或接口的静态字段,或对此字段赋值时,用final修饰的常量除外,这是在解析阶段就初始化好了,所以对final的引用并不会初始化类;
      4. 当调用JAVA API中的 某些反射方法时;
      5. 当初始化某个类的子类时;
      6. 当启动main时;

      不会发生初始化的情况:

      1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
      2. 定义对象数组,不会触发该类的初始化。
      3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
      4. 通过类名获取 Class 对象,不会触发类的初始化。
      5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
      6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。

      以上过程有几点需要注意的是:

      1. 正常的类变量和static修饰的代码块会被JVM聚合在一起放在一个叫clinit方法中,这个方法只能被JVM调用;
      2. 初始化类时,要先初始化Object类,然后再按继承树来初始化,最后是子类,基于这个原因子类不必显性的调用父类的构造函数;
      3. 初化化接口时例外,不需要初始化父类,直接调用整合后的clinit;
      4. 多个线程初始化同一个类时,是一个加锁执行方式;

      可运行以下两个测试程序,来加深一个类初始化的过程:

      class NewParent {
      static int hoursOfSleep = (int) (Math.random() * 3.0);
      static {
      System.out.println("NewParent was initialized.");
      }
      }

      class NewbornBaby extends NewParent {
      static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);
      static {
      System.out.println("NewbornBaby was initialized.");
      }
      }

      class Example2 {
      public static void main(String[] args) {
      int hours = NewbornBaby.hoursOfSleep;
      System.out.println(hours);
      }

      static {
      System.out.println("Example2 was initialized.");
      }
      }
      //Example2 was initialized.
      //NewParent was initialized.
      interface Angry {
      String greeting = "Grrrr!";
      int angerLevel = Dog.getAngerLevel();
      }

      class Dog {
      static final String greeting = "Woof, woof, world!";
      static {
      System.out.println("Dog was initialized.");
      }
      static int getAngerLevel() {
      System.out.println("Angry was initialized");
      return 1;
      }
      }

      class Example3 {
      public static void main(String[] args) {
      System.out.println(Angry.greeting);
      System.out.println(Dog.greeting);
      }
      static {
      System.out.println("Example3 was initialized.");
      }
      }
      //Example3 was initialized.
      //Grrrr!
      //Woof, woof, world!

      卸载

      只有用户自定义的类加载器的对象才能被卸载,启动类装载器的对象不能被回收。jvm分辨是否可GC的标准是判断此对象是否还有引用。

      从一定程度上来说,堆和方法区中的数据是一对多的对应关系的。一个简单的示例了解下此过程:

      //这个引用是以线程为根,依次向下查找的。所以在卸载时依次置空即可。
      GreeterClassLoader gcl = new GreeterClassLoader(args[0]);
      Class c = gcl.loadClass(args[i]);
      Object o = c.newInstance();
      Greeter greeter = (Greeter) o;

      greeter.greet();

      gcl = null;
      c = null;
      o = null;
      greeter = null;
      版权声明:本文内容来自第三方投稿或授权转载,原文地址:https://blog.51cto.com/arch/5275012,作者:生而为人我很遗憾,版权归原作者所有。本网站转在其作品的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如因作品内容、版权等问题需要同本网站联系,请发邮件至ctyunbbs@chinatelecom.cn沟通。

      上一篇:sql server 分页存储过程

      下一篇:大数据Spark “蘑菇云”行动第91课:Hive中Index和Bucket案例实战及存储类型rcfile实战详解

      相关文章

      2025-04-14 08:45:56

      java学习第十一天笔记-字符串222-学生管理系统4查询

      java学习第十一天笔记-字符串222-学生管理系统4查询

      2025-04-14 08:45:56
      i++ , java , jvm , System , 学习
      2025-04-01 10:28:48

      java中satb和tlab有什么区别?

      java中satb和tlab有什么区别?

      2025-04-01 10:28:48
      Java , jvm , JVM , 内存空间
      2025-03-17 07:49:59

      jvm内存堆栈监控之jmap篇

      jvm内存堆栈监控之jmap篇

      2025-03-17 07:49:59
      heap , jvm , 内存
      2025-03-04 09:11:34

      查看rancher中debug端口信息,并做IDEA Remote Jvm Debug

      查看rancher中debug端口信息,并做IDEA Remote Jvm Debug

      2025-03-04 09:11:34
      debug , Debug , IDEA , jvm , 断点
      2025-01-16 09:30:08

      java基础(1)

      java基础(1)

      2025-01-16 09:30:08
      java , JAVA , jsp , jvm , php , 动态 , 客户端
      2024-09-25 10:15:15

      jvm专题(4) - 【1/3】多线程-基础知识

      jvm专题(4) - 【1/3】多线程-基础知识

      2024-09-25 10:15:15
      jvm , 多线程
      2024-09-25 10:14:48

      JOL学习

      JOL(Java Object Layout)是一个开源的 Java 库,用于深入了解和分析 Java 对象的内存布局。它提供了一组工具和API,可以用于查看对象的内部结构、字段排列顺序、内存对齐等信息,以及计算对象的大小和对齐方式。

      2024-09-25 10:14:48
      java , jvm , 学习
      2024-09-25 10:13:46

      第二季:10.死锁编码及定位分析【Java面试题】

      第二季:10.死锁编码及定位分析【Java面试题】

      2024-09-25 10:13:46
      java , jvm
      2024-09-25 10:13:34

      java swing中的卡片布局

      java swing中的卡片布局

      2024-09-25 10:13:34
      java , jvm , servlet
      2024-09-24 06:30:37

      【java基础】泛型的通配符(extends,super,?)

      如果不使用通配符,那么我们在使用了泛型之后就不允许类型参数发生改变了,但是有了通配符就可以更加灵活的控制类型参数,类型参数可以发生改变。

      2024-09-24 06:30:37
      java , jvm , servlet
      查看更多
      推荐标签

      作者介绍

      天翼云小翼
      天翼云用户

      文章

      33561

      阅读量

      5224083

      查看更多

      最新文章

      jvm内存堆栈监控之jmap篇

      2025-03-17 07:49:59

      jvm(三)对象结构以及内存分配

      2024-05-29 09:01:43

      JVM 内存溢出排查

      2024-04-25 08:26:29

      面试害怕考到JVM? 看这一篇就够了~

      2024-03-29 09:48:26

      <JVM上篇:内存与垃圾回收篇>08-对象实例化及直接内存

      2023-07-04 07:11:09

      jvm 调试工具使用

      2023-06-30 08:11:40

      查看更多

      热门文章

      <JVM上篇:内存与垃圾回收篇>08-对象实例化及直接内存

      2023-07-04 07:11:09

      JVM 内存模型概述

      2023-05-05 10:12:40

      jvm 调试工具使用

      2023-06-30 08:11:40

      面试害怕考到JVM? 看这一篇就够了~

      2024-03-29 09:48:26

      JVM 内存溢出排查

      2024-04-25 08:26:29

      jvm(三)对象结构以及内存分配

      2024-05-29 09:01:43

      查看更多

      热门标签

      存储 缓存 内存 数据库 数据 redis mysql 服务器 数据恢复 Redis linux java 链表 MySQL sql
      查看更多

      相关产品

      弹性云主机

      随时自助获取、弹性伸缩的云服务器资源

      天翼云电脑(公众版)

      便捷、安全、高效的云电脑服务

      对象存储

      高品质、低成本的云上存储服务

      云硬盘

      为云上计算资源提供持久性块存储

      查看更多

      随机文章

      面试害怕考到JVM? 看这一篇就够了~

      jvm(三)对象结构以及内存分配

      JVM 内存溢出排查

      <JVM上篇:内存与垃圾回收篇>08-对象实例化及直接内存

      jvm 调试工具使用

      JVM 内存模型概述

      • 7*24小时售后
      • 无忧退款
      • 免费备案
      • 专家服务
      售前咨询热线
      400-810-9889转1
      关注天翼云
      • 旗舰店
      • 天翼云APP
      • 天翼云微信公众号
      服务与支持
      • 备案中心
      • 售前咨询
      • 智能客服
      • 自助服务
      • 工单管理
      • 客户公告
      • 涉诈举报
      账户管理
      • 管理中心
      • 订单管理
      • 余额管理
      • 发票管理
      • 充值汇款
      • 续费管理
      快速入口
      • 天翼云旗舰店
      • 文档中心
      • 最新活动
      • 免费试用
      • 信任中心
      • 天翼云学堂
      云网生态
      • 甄选商城
      • 渠道合作
      • 云市场合作
      了解天翼云
      • 关于天翼云
      • 天翼云APP
      • 服务案例
      • 新闻资讯
      • 联系我们
      热门产品
      • 云电脑
      • 弹性云主机
      • 云电脑政企版
      • 天翼云手机
      • 云数据库
      • 对象存储
      • 云硬盘
      • Web应用防火墙
      • 服务器安全卫士
      • CDN加速
      热门推荐
      • 云服务备份
      • 边缘安全加速平台
      • 全站加速
      • 安全加速
      • 云服务器
      • 云主机
      • 智能边缘云
      • 应用编排服务
      • 微服务引擎
      • 共享流量包
      更多推荐
      • web应用防火墙
      • 密钥管理
      • 等保咨询
      • 安全专区
      • 应用运维管理
      • 云日志服务
      • 文档数据库服务
      • 云搜索服务
      • 数据湖探索
      • 数据仓库服务
      友情链接
      • 中国电信集团
      • 189邮箱
      • 天翼企业云盘
      • 天翼云盘
      ©2025 天翼云科技有限公司版权所有 增值电信业务经营许可证A2.B1.B2-20090001
      公司地址:北京市东城区青龙胡同甲1号、3号2幢2层205-32室
      • 用户协议
      • 隐私政策
      • 个人信息保护
      • 法律声明
      备案 京公网安备11010802043424号 京ICP备 2021034386号