java的变量和常量以及常量池

首先变量名和常量名都是用户自行定义的标志符,遵循先定义后使用的原则,常量和变量的区别是常量之后会不会改变

变量

占据着内存的某一存储区域,该区域有自己的名称和数据类型,该区域的数据可以在同一类型范围内不断的变化,那么为什么定义变量,可以用来存放同一类型的变量可以重复使用,每一个变量都有他自己的作用范围,定义开始到定义它的代码块结束,同一代码块范围内不允许有多个相同名字的局部变量
局部变量和成员变量:
局部变量:声明在方法括号里的变量,只能在方法体中使用和访问,其它方法体访问不到,是类中方法体内定义的变量所以叫局部变量
成员变量:声明在类括号内,方法括号外的变量,又称为field或全局变量,成员变量就是全局变量,其实java内没有全剧变量的概念因为java面向对象的特性所有变量都是类的成员之一所以指的是这个类的变量

常量

在程序运行时有两个作用,代表常数便于程序的修改,增强程序的可读性,常量也可以先声明在赋值,但只能赋值一次,主要利用final关键字来进行常量定义
第一种是一个值,这个值本身,就叫做常量
第二种是不可变的变量也就是我们都知道的关键字final修饰的变量



过渡
在进行常量池的内容之前先进行一个简单的例子String s = new String("hello");这个语句创建了几个字符串?
答案是两个。这两个字符串创建的时间不同,一个是编译期另一个是运行期。为什么会出现这种情况?



常量池
常量池在java用于保存在编译期已经确定的,已编译的class文件中的一份数据,包括了关于类,方法、接口等中的常量,也包括字符串常量,执行产生的常量也放入常量池,故认为常量池是JVM的一块特殊的内存空间,所以首先表达一下JVM的内存分布,有程序计数器,本地方法栈,虚拟栈,方法区,虚拟机堆。
程序计数器:是JVM执行程序的流水线,存放一些跳转指令
本地方法栈:是JVM调用操作系统方法所使用的栈
虚拟机栈:是JVM执行java代码所使用的栈
方法区:存放了一些常量、静态变量、类信息,可以理解为class文件在内存中的存放位置,这个方法区和常量池密不可分,常量池可以理解为class文件的资源仓库
虚拟机堆:是JVM执行java代码所使用的堆

常量池的好处
为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享,节省内存空间,常量池中所有相同的字符串常量被合并只占用一个空间,节省运算时间比较两个引用变量只用==判断引用是否相等就可以判断实际值是否相等,在需要一个对象时就可以从池中取出来一个在需要重复创建相等变量时节省了大量时间。所以八个基本类都实现了常量池(浮点类型的包装类没有实现,整型的包装类也只有在对应值)。


java的常量池分为三种形态

(1)class文件常量池:class文件中的常量池,class文件中不仅仅包含类的版本、字段、方法、接口等信息,还有就是占用class文件绝大部分空间的常量池主要存放字面量和符号引用(包括类和接口的全限定名,字段的名称和描述符,方法的名称和描述符),每个class文件常量池都对应一个运行时常量池。即在编译的时候如果发现其它类方法的调用或者对其它类字段的引用的话,记录进class文件中的只是一个文本形式的符号引用

(2)运行时常量池:是JVM在完成类的装载后,将存在于class文件中的常量池载入到内存中并保存到方法区(即java在执行某个类的时候必须经过加载、连接(包括验证、准备、解析)、初始化),在类加载到内存中时就会将class常量池的内容存放到运行时常量池,运行时常量也是每个类都有一个,在经过解析的时候就会根据文本信息把符号引用替换成直接引用,解析的过程会查询全局字符串池以保证运行时常量池所引用的字符串与全局字符串池是一样的
可与用intern的方法进行扩充,我们常说的常量池就是指方法区中的运行时常量池,它是方法区的一部分,用于存放编译期生成的字面量和符号应用这部分内容将在类加载后进入方法区的时候存入到运行时常量池,而且运行时常量池还有更重要的特性“动态性”,java要求编译期的常量池的内容可以放入池中。
java的八个基本类的包装类大部分都实现了常量池技术,但是String也实现了常量池也就出现了全局字符串常量池

(3)全局字符串池:这里的内容是在类加载完成后经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象的实例引用值存到该全局字符串池中(这里存的是引用值而不是具体的实例对象,具体的实例对象在堆中开辟了一块内存空间),实现该功能的是一个String Table类,它是一个哈希表里面存的是驻留字符串(双引号括起来的)的引用(意思是不是字符串的本体),在每个JVM里只有一份。

只有更好的关注编译期的行为,才能更好的理解常量池,运行时的常量池的常量,基本来源于各个class文件的常量池,程序运行时,除非手动向常量池中添加常量(例如intern方法)否则JVM不会自动添加常量到常量池,实际上有多种常量池比如整形常量池等等,但是数值类型的常量池不可以手动添加常量,程序启动时常量池中的常量就已经确定了。
比如一个程序在经过编译之后在该类的class常量池中存放一些符号引用,然后类加载后将class常量池中的符号引用转存到运行时常量池在经过验证准备阶段之后在堆中生成驻留字符串的实例对象,将这个对象的引用存到全局字符串池,在解析阶段要把运行时常量池中的符号引用替换成直接引用,就会直接查询String Table保证里面的引用值和运行时常量池中的引用值一致。

字符串驻留

首先要知道可变类和不可变类
可变类:当获得这个类的实例引用,可以改变类的实例内容
不可变类:当获得这个类的实例引用,不可以改变这个实例的内容,不可变类的实例一但被创建,其内在成员变量的值就不能被改变,比如String
为什么使用不可变对象,以Sring为例:不可变对象可以提高Sting Pool的效率(即字符串常量池)和安全性,如果知道一个对象是不可变的,那么就需要拷贝这个对象的内容时,就不需要复制本身而是复制地址,同时不可变的对象对于线程也是安全的不会因为多线程同时进行导致值的改变。

java会确保一个字符常量只有一个拷贝,双引号里的都是字符串它们在编译期就被确定了,但是用new String()创建的字符串不是常量不能再编译期确定,所以new String()创建的字符串不能放进常量池中,有自己的空间,在使用intern之后可以将其保存到一个全局String表里,如果具有相同值的Unicode字符串在这个表里,那么该方法返回表中已有字符串的地址并且不会将外部的字符串放入常量池,如果不同会将外部字符串放入池中,并返回字符串的句柄(但是不是将自己的地址注册到常量池中,因为S1 == S1.intern()的结果是false,如果是将地址注册到常量池中那么这个使用==来测试是否为同一个字符串的结果应该是true,也就是说之前的字符串还是存在的)。
String是不可变的所以在进行拼接的时候,会生成很多临时变量,这是建议使用StringBuffer的原因,因为StringBuffer可变。


常量池的具体结构

在java语言中一切都是动态的,编译时如果发现对其他类方法的调用或者对其他类字段的引用的语句,记录进class文件中的只能是一个文本形式的符号引用,在连接过程中,虚拟机根据这个文本信息去查找对应的方法或字段,所以与java的所谓常量不同,class文件中的常量内容丰富,这些常量集中在class中的一个区域存放,一个接着一个就是常量池,在java程序中有很多东西是永恒的,不会再运行过程中变化,比如一个类的名字,一个类字段的名字/所属类型,一个类方法的名字/返回类型/参数名与所属的类型,一个常量,还有在程序中的字面值,这些在JVM解释执行的时候非常重要,会用一部分字节分类存储这些代码,这些字节就是常量池但是只有JVM加载class后在方法区为它们开辟了空间
常量池中的常量表,这些常量表之间又有不同,class文件中一共有11种常量表:
(1)_Utf8:用UTF-8编码方式来表示程序中所有的重要常量字符串
(3-6)_Integer、_Float、_Long、_Double:所有基本数据类型的字面值
(7)_Class:使用符号引用来表示类或接口,知道所有类名都以_Utf8表的形式存储,但是不知道_Utf8表中哪些字符串是类名哪些是方法名,因此必须用一个指向类名字符串的符号引用常量来表明
(8)_String:指向包含字符串字面值的Utf8表
(9-11)_Fieldref、_Methodren、_InterfaceMethodref:指向包含该字段或方法的名字和描述符的Utf8表以及指向包含该字段或方法的名字和描述符_NameAndType表
(12)_NameAndType:指向包含该字段或方法的名字和描述符的Utf8表
在源代码中的每一个字面值字符串,都会在编译成class文件阶段,形成标志号(8)的常量表,在JVM加载时会为对应的常量池建立一个内存的数据结构,并存放在方法区中,同时JVM会自动为常量表中的字符串常量的字面值在堆中创建新的String对象,然后把_String常量表的入口地址变为这个堆中String对象的直接地址(常量池解析)
源代码中所有相同字面值的字符串常量只可能建立唯一一个拘留字符串的对象,实际上JVM是通过一个记录了拘留字符串引用的内部数据结构来维持这一特性,在java中可以调用String的intern()方法来使得一个常规字符串对象成为拘留字符串对象
String s = new String("Hello world");在运行指令之前JVM就会在堆中创建一个拘留字符串,然后用这个拘留字符串的值来初始化堆中new指令创建出来的新的String对象
String s = "Hello World";此时局部变量存储的是早已创建好的拘留字符串的堆地址
new String创建的字符串不是常量不能在编译期确定,所以new出的String对象不放入常量池有自己的地址空间,而且由于String对象的不变性机制会使修改字符串时产生大量的对象,因为每次改变字符串时都会生成一个新的字符串,java为了更有效的使用内存常量池在编译器遇见字符串时会检查池内是否已经存在相同的String字符串,找到了就会把新变量的引用指向现有的字符串对象,没找到就创建新的,所以对一个字符串对象的修改会产生新的对象,之前的依然存在并等待垃圾回收。


class文件结构(可以用winhex打开二进制格式 文件)
因为在class文件的一些字节构成了常量池,所以进入class文件进行了解
开头的四个字节是class文件的魔数,用来标示这是一个class文件,紧接着四个字节是java的版本号,接下就是常量池入口,入口处用两个字节标示常量池常量数量,常量池中存放了各种类型的常量都有自己的类型和存储规范
魔数:唯一作用就是确定了这个文件是否被JVM接受,很多文件存储标准中都使用魔数来进行身份识别
版本号:第五、六个字节是次版本号,第七个和第八个是主版本号
常量池入口:由于常量池中的常量的数量不是固定的,所以常量池入口需要放置一项u2类型的数据,代表常量池的容量计数制
常量池:主要存放两类常量(字面量和符号引用)即语言层面的常量概念和类和接口的权限定名、字段的名称和描述符、方法的名称和描述符

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值