深入理解java虚拟机(第二版)读书笔记(更新ing)

第二章 java内存区域与内存溢出异常

2.2 运行时数据区域

java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
这里写图片描述

2.2.1 程序计数器

是一块较小的内存,可以看作是当前线程所执行的字节码的行号指示器。分支,循环,跳转,异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。所以每个线程都需要有一个独立的程序计数器嘛,这个区域也是java虚拟机中唯一一个不会内存溢出的区域。

2.2.2 java虚拟机栈

也是线程私有的。每个方法执行前都会创建一个栈帧,用来存储局部变量表、操作数帧、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这里规定了两种异常:第一种是线程请求的栈的深度大于虚拟机所运行的深度,将会抛出StackOverflowError异常;第二种是如果虚拟机栈可以动态扩展,但是扩展的时候无法申请到足够的内存,就会抛出内存溢出的异常。

2.2.3 本地方法栈

与虚拟机栈类似,前者为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机用到的native方法服务。

2.2.4 java堆

被所有线程共享,用来存放对象实例。在虚拟机启动的时候创建。随着JIT编译器的发展等技术逐渐成熟,所有的对象都分配在堆上也渐渐的变得不那么“绝对”了。JAVA堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC”堆。

2.2.5 方法区

各个线程共享。用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。垃圾收集行为在这个区域是比较少出现的。但是这部分区域的回收确实是必要的。

2.2.6 运行时常量池

方法区的一部分,用于存放编译期生成的各种字面量和符号引用。具有动态性,并不一定要求常量一定只有编译期才产生。运行期间也可能将新的常量放入。(String类的intern()方法)

2.2.7 直接内存

不是虚拟机运行时数据区的一部分,也不是虚拟机规范种定义的内存区域,但是也被频繁使用,而且也可能导致内存溢出。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在JAVA堆种的对象对这块内存的引用进行操作。能在一些场景提高性能,因为避免了在java堆和Native堆种来回复制数据。

2.3HotSpot虚拟机对象探秘

HotSpot虚拟机在Java堆种对象分配、布局、和访问的全过程。

2.3.1 对象的创建

类加载检查:虚拟机碰到一个New指令时,首先检查这个指令的参数是否能在常量池种定位到一个类的符号引用,并检查这个类是否已经被加载,解析和初始化过。如果没有,就必须先执行相应的类加载过程。

为新生对象分配内存:把一块确定大小的内存从Java堆种划分出来。有两种方法,一种是指针碰撞。假设堆中内存是绝对规整的,将用过的内存放一边,没用的放一边,中间放着一个指针,分配内存的时候只要把指针像没用的那边移动。第二种是空闲列表,此时堆中的内存是相互交错的。维护一个列表,记录哪些内内存块是可用的,分配的时候从列表中找出一块足够大的区域然后更新列表。

线程安全处理:一种是对分配内存空间的动作进行同步处理(采用CAS保证更新操作的原子性)一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存。那个线程需要分配内存就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

对象的一些设置:将分配的内存空间都初始化为零值,不包括对象头。

2.3.2 对象的内存布局

对象头:包括两部分信息。第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。(Mark Word)。第二部分是类型指针,即对象指向它的类元数据的指针。虚拟机通过这个指针确定这个对象是哪个类的实例。(如果对象是一个JAVA数组,那在对象头中还必须有一块用于记录数组长度的数据。)

实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

对齐填充:没有特别的含义,起着占位符的作用。对象的大小必须是8字节的整数倍,对象头部分是8字节的一倍或者两倍,所以当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

2.3.3 对象的访问定位

句柄访问:java堆中会分配出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。好处:句柄的稳定,在对象被移动的时候(垃圾收集时经常需要移动对象),此时只需要改变句柄中的实例数据指针,而reference本身不用改变。
这里写图片描述
直接指针访问:reference中存储的直接就是对象地址,java堆对象应考虑如何放置访问类型数据的相关数据。最大好处就是速度快,节省了一次指针定位的世界。对象的访问在java中非常频繁,所以积少成多后就是一项非常可观的成本。(HotShop采用的就是这种)
这里写图片描述

2.4 实战:OutOfMemoryError异常

2.4.1 Java堆溢出

java堆用于存储对象实例,不断地创建对象,并且保证GC Roots都可达,这样垃圾回收机制就无法清楚这些对象,达到最大堆的容量后就会内存溢出。解决手段通过内存映像分析工具。

2.4.2 虚拟机栈和本地方法栈溢出

两种异常查看2.2.2。
实验发现:在单线程的情况下,无论是帧栈太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。如果是多线程的情况下,通过不断建立新的线程的方式可以产生内存溢出异常。这样的内存溢出与栈空间是否足够大没有联系,或者说为每个线程的栈分配的大小越大,越容易内存溢出。因为操作系统给每个进程分配的内存是有限制的,线程多了就不够分了,剩下的内存就慢慢耗尽了。

2.4.3 方法区和运行时常量池溢出

实验思路:运行时产生大量的类去填满方法区,直到溢出。
方法区溢出也是一种常见的内存溢出异常,因为一个类要被垃圾收集器回收掉,判定条件是非常苛刻的。所以在经常动态生成大量class的应用中,需要特别注意类的回收状况。

2.4.4 本机直接内存溢出

计算得知内存无法分配, 于是手动抛出异常。


第三章 垃圾收集器与内存分配策略

3.1 概述

程序计数器,虚拟机栈,本地方法栈三个区域随线程而生,随线程而灭,所以不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存就自然而然的回收掉了。但是java堆和方法区则不一样,这部分内存的分配和回收都是动态的,所以垃圾收集器所关注的是这部分的内存。

3.2 对象已死吗

垃圾收集器在工作时,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。

3.2.1 引用计数算法

算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效,计数器值就减一,任何时候计数器为零的对象就是不可能再被使用的。
存在的问题:主流的Java虚拟机并没有选用引用计数算法来管理内存,其中最主要的原因就是它很难解决对象之间相互循环引用的问题。(即两个对象如果互相引用着对方,会导致他们无法正常回收)

3.2.2 可达性算法

算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(就是从GC Roots到这个对象不可达),则证明此对象是不可用的。
这里写图片描述
GC Roots:在java中,可作为GC Roots的对象包括虚拟机栈(栈帧中的本地变量表)中引用的对象,方法区中类静态变量引用的对象,方法区中常量引用的对象,本地方法栈中JNI(Native)引用的对象。

3.2.3 再谈引用

JDK 1.2之前:Java中的引用的定义很传统:如果referernce类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
JDK 1.2之后:Java对引用的概念进行了扩充。

  • 强引用:就是在程序代码之间普遍存在的,类似”Objcet obj = new Objcet()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用:是用来描述一些还有用但是并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够内存,才会抛出内存溢出异常。提供了SoftReference类来实现软引用。
  • 弱引用:用来描述非必需对象,但是它的强度比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。提供了WeakReference类来实现弱引用。
  • 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。提供了PhantomReference来实现虚引用。

3.3.4 生存还是死亡

要真正宣告一个对象死亡:至少要经历两次标记过程。当可达性分析后不可达后会被第一次标记并且进行一次筛选。筛选的条件就是如果对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过了。
若这个对象被判定为有必要执行:那么将会把这个对象放到一个叫F-Queue的队列之中,并在一个由虚拟机自动建立、优先级低的线程去执行它,而且这个执行并不会承诺会等待它运行结束,因为如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能导致队列中的其他对象永久等待,甚至整个内存回收系统崩溃。
第二次标记逃脱:finalize()方法是对象最后一次拯救自己的机会,如果对象在这个方法中与引用链中的任何一个对象建立关联,在第二次标记的时候就会被移除即将回收的集合。
建议:它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。有些教材描述它适合做一些关闭外部资源类的工作,这完全是对这个方法用途的一种自我安慰。Finalize()可以做的所有工作,使用try-finally或者其他方法都可以做的更好更及时,所以笔者建议大家完全可以忘掉JAVA语言中有这个方法。

3.2.5 回收方法区

回收性价比:在新生代中,进行一次垃圾收集一般可以回收70%~95%的空间,而永久代(方法区)的垃圾收集效率远低于此。
主要收集两部分:
- 废弃常量:常量池中类,接口,方法,字段的符号,字面量没有被其他地方引用。类似于JAVA堆的对象回收。
- 无用的类:该类所有的实例都已经被回收,也就是JAVA堆中不存在该类的任何实例;加载该类的ClassLoader已经被回收;该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。虚拟机可以对满足以上三个条件的无用类进行回收,但是是否对类回收,虚拟机提供了参数进行控制。
建议:在大量使用反射,动态代理等频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

3.3 垃圾收集算法

在讨论垃圾收集器的上下文语境中,
- 并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发:用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。

3.3.1 标记-清除算法

算法:算法分为标记和清除两个阶段,首先标记出需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:主要有两个,一是效率问题,标记和清除两个过程的效率都不高;第二个是空间问题,标记清除之后会产生大量的不连续的内存碎片,会导致分配大对象的时候,无法找到足够内存的连续内存而不得不再一次触发垃圾收集动作。
这里写图片描述

3.3.2 复制算法

算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。代价是将内存缩小为了原来的一半。现在的商业虚拟机都采用这种收集算法来回收新生代。
这里写图片描述
为什么HotSpot虚拟机Eden和Survivor空间大小比例8:1?
新生代中的对象98%的对象是“朝生夕死”的,所以不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间。(详细过程请点击这篇博文查看)大小比例为8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%会被浪费。

分配担保: 如果另外一块survivo空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机智进入老年代。

3.3.3 标记-整理算法

算法:标记过程与标记清除算法一样,但是后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以为的内存。
这里写图片描述

3.3.4 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,就是根据对象存活的周期不同将内存划分为几块。一般是将JAVA堆分为新生代和老年代,新生代再每次垃圾收集时,都有大批对象死去,只有少量存活,采用复制算法,再老年代因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记-清理或者是标记-整理算法来进行回收。

3.4HotSpot的算法实现

3.4.1 枚举根节点

主流JAVA虚拟机使用的都是准确式GC,即再执行系统停顿下来后,不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。再HotSpot中是使用一组称OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

3.4.2 安全点

安全点(Safepoint):如果为每一条指令都生成对应的OopMap,需要大量的额外空间,所以程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。列如方法调用、循环跳转、异常跳转等。
考虑的问题如何在GC发生时让所有线程都“跑”到最近的安全点上再停顿下来。有两种方案选择:

  • 抢先式中断:再GC发生时,首先把所有的线程全部中断,如果发现有线程中断的地方不再安全点上,就恢复线程,让它跑到安全点上。现在几乎没有虚拟机实现采用这种方式来暂停线程从而响应GC事件。
  • 主动式中断:当GC需要中断线程的时候,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点式重合的。

3.4.3 安全区域

当程序不执行的时候,比如线程处于sleep或者blocked状态,这时候线程无法相应JVM的中断请求,“走”到安全的地方去中断挂起。这时候就需要安全区域(Safe Region)来解决。安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。也可以把安全区域看成扩展了的安全点。

3.5 垃圾收集器

3.5.1 Serial收集器

是一个单线程的收集器,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。到现在为止,它依然式虚拟机运行在Client模式下的默认新生代收集器

3.5.2 ParNew收集器

就是Serial收集器的多线程版本。是运行在Server模式下的虚拟机中首选的新生代收集器,除了Serial收集器目前只有它可以何CMS收集器配合工作。

3.5.3 Parallel Scavenge收集器

使用复制算法的多线程收集器。它的目标是达到一个可控制的吞吐量。吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

3.5.4 Serial Old收集器

Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。

3.5.5 Parallel Old收集器

Parallel Scavenge收集器老年代版本,多线程、“标记-整理”算法。

3.5.6 CMS收集器

CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法。分为四个步骤:初始标记,并发标记,重新标记,并发清除。初始标记,重新标记这两个步骤任然需要“Stop The World”。初始标记仅仅只是标记一下一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是修正并发标记期间因用户程序继续运作而导致标记发生变动的那一部分对象的标记记录。这个阶段的停顿时间比初始标记长一点,但是远比并发标记时间短。
优点:并发收集,低停顿。
缺点:对CPU资源非常敏感,总吞吐量会降低。无法处理浮动垃圾。会有大量空间碎片产生。

3.5.7 G1收集器

是一款面向服务端应用的垃圾收集器。HotShop赋予它的使命是在未来可以替换掉JDK 1.5发布的CMS收集器。
特点:
- 并行与并发
- 分代手机
- 空间整理
- 可预测的停顿
步骤:初始标记,并发标记,最终标记,筛选收回。

3.6内存分配与回收策略

3.6.1对象优先在Eden分配

大多数情况下,对象在新生代区中进行分配。当没有足够空间进行分配时,虚拟机将发起一次Minor GC。
新生代GC (Minor GC):发生在新生代的垃圾收集动作,因为JAVA对象大多数都是朝生夕死,所以非常频繁,一般速度比较快。
老年代GC (Major GC/Full GC):发生在老年代的GV,出现了Major GC,经常会伴着至少一次Minor GC。一般比新生代GC慢十倍以上。

3.6.2 大对象直接进入老年代

大对象是指需要大量连续内存空间的JAVA对象。

3.6.3 长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄计数器。

3.6.4 动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。

3.6.5 空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,那么Minor GC可以确保安全。

第六章 类文件结构

代码编译的结果从本地机器码转变为字节码,是储存格式发展的一小步,却是编程语言发展的一大步。

6.1 概述

计算机只认识0和1,所以我们写的程序需要经编译器翻译成由0和1构成的二进制格式才能由计算机执行。随着虚拟机的发展,将编写的程序编译成二进制本地机器码已不再是唯一的选择,更多的程序语言选择了与操作系统和机器指令集无关的,平台中立的格式作为程序编译后的存储格式。

6.2 无关性的基石

各种不同平台的虚拟机与所有平台都统一使用的程序储存格式——字节码(ByteCode)是构成平台无关性的基石。
除此之外,语言无关性正越来越被开发者所重视。时至今日,商业机构和开源机构已经在JAVA语言之外发展出了一大批在JAVA虚拟机之上运行的语言。Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。虚拟机并不关心Class的来源是何种文件,JRuby等其他语言的编译器一样可以把程序代码编译成Class文件。
字节码命令所能提供的语义描述能力比Java语言更加强大,这也为其他语言实现一些有别于Java的语言特性提供了基础。
这里写图片描述

6.3 Class类文件的结构

Class文件时一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。当遇到需要占用8位字节以上的空间的数据项时,则回按照高位在前的方式分割成若干个8位字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构体来存储数据,其中只有两种数据类型:无符号数和表。

无符号数:基本的数据类型,以u1,u2,u4,u8分别代表1 2 4 8个字节的无符号数,可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值。
表:是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表习惯性的以”_Info“结尾。整个Class文件本质就是一张表。

6.3.1 魔数与Class文件的版本

魔数:每个Class文件的头4个字节,它的唯一作用是确定这个文件是否位一个能被虚拟机接受的Class文件。值为:0xCAFFBABE(咖啡宝贝)。
版本和次、主版本:第四个字节存储的是Class文件的版本号,第五和第六个字节是次版本号,第七和第八个字节是主版本号。(java版本号从45开始)高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。

6.3.2 常量池

常量池:可以理解为Class文件之中的资源仓库,他是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时他还是在Class文件中第一个出现的表类型数据结构。
由于常量数量不固定,所以在常量池的入口要放一项U2类型的数据,代表常量池容量计数值。与Java语言习惯不一样的是,(只有)容量计数是从1而不是0。设计者将0空出来是有特殊考虑的,这也做目的是满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。
主要放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,符号引用则属于编译原理方面的概念,包括了下面三项常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

6.3.3 访问标志

在常量池之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类,是否为abstract类型,如果是类的话,是否被声明为final。

6.3.4 类索引,父类索引,接口索引集合

类索引和父类索引都是一个U2类型的数据,而接口索引集合是一组U2类型的数据集合,Class文件通过这三项数据来确定这个类的继承关系。类索引用于确定这个类的权限定名,父类索引用于确定这个类的父类的全限定名。除了Java.lang.object外,所有的类的父类索引都不为0。接口索引集合就是用来描述这个类实现了哪些接口。对于接口索引集合,入口的第一项——u2为接口计数器,如果没有实现任何接口,则为0。
这里写图片描述

6.3.5 字段表集合

字段表用来描述接口或者类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
简单名称:没有类型和参数修饰的方法或者字段名称,比如类中的inc()方法和m字段的简单名称是inc 和m。
全限定名:把类全名中的”.”替换成“/”,使用时最后一般回加入一个“;”表示全限定名结束。“org/claszz/TestClass”
描述符:描述字段的数据类型,方法的参数列表和返回值。如”java.lang.String[][]”记录为“[[Ljava/lang/String“ “int[]”被记录为“[I” 方法int indexOf᧤char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)将被描述为“[CII[CIII)I”。

6.3.6 方法表集合

对方法的描述与对字段的描述几乎采用了完全一致的方式。方法里的的JAVA代码通过编译器编译成字节码指令后,存放在方法属性表集合中的一个名为Code的属性里面。
在Java语言中,要重载一个方法,除了要与原方法有相同的简单名称之外,还必须有一个与原方法不同的特征签名,特征签名是一个在方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java元里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以生存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

6.3.7 属性表集合

在Class文件,字段表,方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。不要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息。JAVA虚拟机会在运行时忽略掉它不认识的属性。

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值