java字面量和常量 jvm字面量_java字面量和常量


java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行。

JVM基本结构

java字面量和常量 jvm字面量_java_02


从这个结构不难看出,class文件被jvm装载以后,经过jvm的内存空间调配,最终是由执行引擎完成class文件的执行。

java字面量和常量 jvm字面量_JVM_03

内存空间:

JVM内存空间包含:方法区、java堆、java栈、本地方法栈。

内存空间

特点

说明

方法区

线程共享

存放类信息、常量、静态变量;

java堆

线程共享

我们的类的实例就放在这个区域,可以想象你的一个系统会产生很多实例,因此java堆的空间也是最大的。堆同时也是垃圾收集器管理的主要区域,从内存回收的角度看,java堆可以分为“新生代”和“老年带”。java堆可以处于物理上不连续的内存空间中,只需要其是逻辑上连续的即可,如我们的磁盘空间。【java堆空间不足了,程序抛出OutOfMemoryError异常。】

java栈

线程私有

生命周期与线程相同,一个线程对应一个java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”,而栈帧中包括了方法中的局部变量、用于存放中间状态值的操作栈。当无法申请到足够内存时,则发生OutOfMemoryError。 【java栈空间不足了,程序会抛出StackOverflowError异常】。如果递归深度很深,就会执行大量的方法,方法越多java栈的占用空间越大。

本地方法栈

和java栈类似

区别:它是用来表示执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。

PC寄存器

控制程序指令执行顺序

类已经加载了,实例对象、方法、静态变量都去了自己改去的地方,那么程序该怎么执行,哪个方法先执行,哪个方法后执行,这些指令执行的顺序就是PC寄存器在管。执行引擎根据PC寄存器调配的指令顺序,依次执行程序指令。

方法区中有一个运行时常量池。注意这里不是字符串常量池,它存储的是类编译时期生成的各种字面量和符号引用,并且每个类都有一个。
字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。

在JDK1.6之前,字符串常量池一直放在方法区中,但是到了jdk1.7的时候,常量池便被移出方法区,而转到Java堆中区了。
在HotSpot虚拟机里实现的字符串常量池功能的是一个StringTable类,它是一个Hash表。这个哈希表在每个HotSpot虚拟机的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,并且相同字符串只保留一份。

内存溢出(Overflow)和内存泄漏(Resource leak)

泄漏:内存不释放; 溢出是指内存不够用。 导致不够用的原因很多 泄漏只是其中一种

内存泄漏:申请的空间没有及时释放或者干脆丢了指针没法释放.不是泄漏,是漏不出来。造成可用内存越来越少。
比如在方法内一个对象被全局的HashMap持有,方法执行结束没有释放就会导致内存泄漏。
再有就是当一个对象被存储进HashSet后,其hashcode计算相关的变量被修改了,这也有可能导致内存泄漏,因为这时候这个对象基本已经不可达了。


内存模型

一个java线程对一个变量的更新怎么通知到另外一个线程呢?我们知道java当中的实例对象、数组元素都放在java堆中,java堆是线程共享的。(我们这里把java堆称为主内存),而每一个线程都是自己私有的内存空间(称为工作内存),如果线程1要向线程2通信,一定会经过类似的流程:

java字面量和常量 jvm字面量_java字面量和常量_04

1、 线程1将自己工作内存中的X更新为1并刷新到主内存中;
2、 线程2从主内存读取变量X=1,更新到自己的工作内存中,从而线程2读取的X就是线程1更新后的值。

从上面的流程看出线程之间的通信都需要经过主内存,而主内存与工作内存的交互,则需要Java内存模型(JMM)来管理器。下图演示了JMM如何管理主内存和工作内存:

java字面量和常量 jvm字面量_JVM_05

当线程1需要将一个更新后的变量值刷新到主内存中时,需要经过两个步骤:
1、 工作内存 执行store操作;
2、 主内存 执行write操作;
完成这两步即可将工作内存中的变量值刷新到主内存,即线程1工作内存和主内存的变量值保持一致;

当线程2需要从主内存中读取变量的最新值时,同样需要经过两个步骤:
1、主内存 执行read操作,将变量值从主内存中读取出来;
2、工作内存 执行load操作,将读取出来的变量值更新到本地内存的副本;
完成这两步,线程2的变量和主内存的变量值就保持一致了。

可见性(关键字volatile)

由于工作内存这个中间层的出现,线程1和线程2必然存在延迟的问题,例如线程1在工作内存中更新了变量,但还没刷新到主内存,而此时线程2获取到的变量值就是未更新的变量值,又或者线程1成功将变量更新到主内存,但线程2依然使用自己工作内存中的变量值,同样会出问题。不管出现哪种情况都可能导致线程间的通信不能达到预期的目的。

//线程1 
	boolean stop = false; 
	while(!stop){ doSomething(); } 
//线程2 
	stop = true;

这个经典的例子表示线程2通过修改stop的值,控制线程1中断,但在真实环境中可能会出现意想不到的结果,线程2在执行之后,线程1并没有立刻中断甚至一直不会中断。出现这种现象的原因就是线程2对线程1的变量更新无法第一时间获取到。

Volatile保证两件事:
1、 线程1工作内存中的变量更新会强制立即写入到主内存;
2、 线程2工作内存中的变量会强制立即失效,这使得线程2必须去主内存中获取最新的变量值。
所以这就理解了Volatile保证了变量的可见性,因为线程1对变量的修改能第一时间让线程2可见。

指令重排序

int a = 0;

boolean flag = false;
//线程1
public void writer() {
	a = 1;
	flag = true;
}
//线程2
public void reader() {
	if (flag) {
		int i= a+1;
...... }
}

正常情况 a = 1;flag = true;–>int i=a+1=1+1=2

但真实情况却不一定如此:

java字面量和常量 jvm字面量_java字面量和常量_06


指令重排序的简单演示,两个赋值语句尽管他们的代码顺序是一前一后,但真正执行时却不一定按照代码顺序执行。你可能会说,有这个指令重排序那不是乱套了吗?我写的程序都不按我的代码流程走,这怎么玩?这个你可以放心,你的程序不会乱套,因为java和CPU、内存之间都有一套严格的指令重排序规则,哪些可以重排,哪些不能重排都有规矩的。下列流程演示了一个java程序从编译到执行会经历哪些重排序:

java字面量和常量 jvm字面量_java字面量和常量_07

第一步属于编译器重排查,编译器重排序会按JMM的规范严格进行,换言之编译器重排序一般不会对程序的正确逻辑造成影响。
第二、三步属于处理器重排序,处理器重排序JMM就不好管了,怎么办呢?它会要求java编译器在生成指令时加入内存屏障,内存屏障是什么?你可以理解为一个不透风的保护罩,把不能重排序的java指令保护起来,那么处理器在遇到内存屏障保护的指令时就不会对它进行重排序了。

在同一个线程中,不会被重排序的逻辑:

java字面量和常量 jvm字面量_JVM_08


这三种情况中,任意改变一个代码的顺序,结果都会大不相同,对于这样的逻辑代码,是不会被重排序的。注意这是指单线程中不会被重排序,如果在多线程环境下,还是会产生逻辑问题,例如我们一开始举的例子。