JVM启动流程:
《深入理解Java虚拟机(第二版)》中的描述是下面这个样子的:
Java中的内存分配:
Java程序在运行时,需要在内存中的分配空间。为了提高运算效率,就对数据进行了不同空间的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。
具体划分为如下5个内存空间:(非常重要)
栈:存放局部变量 堆:存放所有new出来的东西 方法区:被虚拟机加载的类信息、常量、静态常量等。 程序计数器(和系统相关) 本地方法栈
1、程序计数器:
每个线程拥有一个PC寄存器
在线程创建时创建
指向下一条指令的地址
执行本地方法时,PC的值为undefined
2、方法区:
保存装载的类信息
类型的常量池
字段,方法信息
方法字节码
通常和永久区(Perm)关联在一起
3、堆内存:
和程序开发密切相关
应用系统对象都保存在Java堆中
所有线程共享Java堆
对分代GC来说,堆也是分代的
GC管理的主要区域
现在的GC基本都采用分代收集算法,如果是分代的,那么堆也是分代的。如果堆是分代的,那堆空间应该是下面这个样子:
上图是堆的基本结构,在之后的文章中再进行详解。
4、栈内存:
线程私有,生命周期和线程相同 栈由一系列帧组成(因此Java栈也叫做帧栈) 帧保存一个方法的局部变量、操作数栈、常量池指针 每一次方法调用创建一个帧,并压栈
解释:
Java虚拟机栈描述的是Java方法执行的内存模型:每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
在Java虚拟机规范中,对这个区域规定了两种异常情况:
(1)如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)
(2)虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现OOM
4.1 Java栈之局部变量表:包含参数和局部变量
局部变量表存放了基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配。
例如,我写出下面这段代码:
package test03; /** * Created by smyhvae on 2015/8/15. */ public class StackDemo { //静态方法 public static int runStatic(int i, long l, float f, Object o, byte b) { return 0; } //实例方法 public int runInstance(char c, short s, boolean b) { return 0; } } |
上方代码中,静态方法有6个形参,实例方法有3个形参。其对应的局部变量表如下:
上方表格中,静态方法和实例方法对应的局部变量表基本类似。但有以下区别:实例方法的表中,第一个位置存放的是当前对象的引用。
4、2 Java栈之函数调用组成栈帧:
方法每次被调用的时候都会创建一个栈帧,例如下面这个方法:
public static int runStatic(int i,long l,float f,Object o ,byte b){ return runStatic(i,l,f,o,b); } |
当它每次被调用的时候,都会创建一个帧,方法调用结束后,帧出栈。如下图所示:
4.3 Java栈之操作数栈
Java没有寄存器,所有参数传递都是使用操作数栈
例如下面这段代码:
1 2 3 4 5 | public static int add(int a,int b){ int c=0; c=a+b; return c; } |
压栈的步骤如下:
0: iconst_0 // 0压栈
1: istore_2 // 弹出int,存放于局部变量2
2: iload_0 // 把局部变量0压栈
3: iload_1 // 局部变量1压栈
4: iadd //弹出2个变量,求和,结果压栈
5: istore_2 //弹出结果,放于局部变量2
6: iload_2 //局部变量2压栈
7: ireturn //返回
如果计算100+98的值,那么操作数栈的变化如下图所示:
4.4 Java栈之栈上分配:
小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
直接分配在栈上,可以自动回收,减轻GC压力
大对象或者逃逸对象无法栈上分配
栈、堆、方法区交互:
三、内存模型:
每一个线程有一个工作内存。工作内存和主存独立。工作内存存放主存中变量的值的拷贝。
当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作。
每一个操作都是原子的,即执行期间不会被中断
对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。如果需要在其他线程中立即可见,需要使用volatile关键字作为标识。
1、可见性:
一个线程修改了变量,其他线程可以立即知道
保证可见性的方法:
volatile
synchronized (unlock之前,写变量值回主存)
final(一旦初始化完成,其他线程就可见)
2、有序性:
在本线程内,操作都是有序的
在线程外观察,操作都是无序的。(指令重排 或 主内存同步延时)
3、指令重排:
指令重排:破坏了线程间的有序性:
指令重排:保证有序性的方法:
指令重排的基本原则:
程序顺序原则:一个线程内保证语义的串行性
volatile规则:volatile变量的写,先发生于读
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
传递性:A先于B,B先于C 那么A必然先于C
线程的start方法先于它的每一个动作
线程的所有操作先于线程的终结(Thread.join())
线程的中断(interrupt())先于被中断线程的代码
对象的构造函数执行结束先于finalize()方法
四、解释运行和编译运行的概念:
解释运行:
解释执行以解释方式运行字节码
解释执行的意思是:读一句执行一句
编译运行(JIT):
将字节码编译成机器码
直接执行机器码
运行时编译
编译后性能有数量级的提升
编译运行的性能优于解释运行
Q:简单说说 Java 的 JVM 内存结构分为哪几个部分?
A:JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈,运行时常量池(六个部分,分别解释如下)
· 虚拟机栈:
线程私有的,每个方法在执行时会创建一个栈帧,用来存储局部变量表、操作数栈、动态连接、方法返回地址等;其中局部变量表用于存放 8 种基本数据类型(boolean、byte、char、short、int、float、long、double)和 reference 类型。每个方法从调用到执行完毕对应一个栈帧在虚拟机栈中的入栈和出栈。
· 堆:
线程共享的,在虚拟机启动时创建,用于存放对象实例。
· 方法区:
线程共享的,用于存储已被虚拟机加载的类信息、常量、静态变量等。
· 程序计数器:
线程私有的,是当前线程所执行的字节码行号指示器,每个线程都有一个独立的程序计数器,字节码解释器工作时通过改变它的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复都依赖于它。
· 本地方法栈:
线程私有的,主要为虚拟机用到的 native 方法服务,与虚拟机栈类似。
· 运行时常量池(Runtime Constant Pool)
,存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。
Q: 堆和栈的特点
当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
堆内存用来存放由new创建的对象和数组。
java中变量在内存中的分配
1、类变量(static修饰的变量):在程序加载时系统就为它在堆中开辟了内存,堆中的内存地址存放于栈以便于高速访问。静态变量的生命周期--一直持续到整个'系统'关闭
2、实例变量:当你使用java关键字new的时候,系统在堆中开辟并不一定是连续的空间分配给变量(比如说类实例),然后根据零散的堆内存地址,通过哈希算法换算为一长串数字以表征这个变量在堆中的'物理位置'。 实例变量的生命周期--当实例变量的引用丢失后,将被GC(垃圾回收器)列入可回收“名单”中,但并不是马上就释放堆中内存
3、局部变量:局部变量,由声明在某方法,或某代码段里(比如for循环),执行到它的时候在栈中开辟内存,当局部变量一但脱离作用域,内存立即释放
Q:你能不能谈谈,java GC是在什么时候,对什么东西,做了什么事情?
A:
Q:简单谈谈你对 Java 虚拟机内存模型 JMM 的认识和理解及并发中的原子性、可见性、有序性的理解?
这是一个很泛很大且很有水准的面试题,也算是对并发基础原理实质的一个深度问题,想要在面试中简短的回答好不是特别容易,本解析也仅供参考,具体理解可自己查阅其他资料。
Java 内存模型主要目标是定义程序中变量(此处变量特指实例字段、静态字段等,但不包括局部变量和函数参数,因为这两种是线程私有无法共享)在虚拟机中存储到内存与从内存读取出来的规则细节,Java 内存模型规定所有变量都存储在主内存中,每条线程还有自己的工作内存,工作内存保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行而不能直接读写主内存中的变量,不同线程之间无法相互直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成(具体如下图)。
Java 内存模型对主内存与工作内存之间的具体交互协议定义了八种操作,具体如下:
· lock(锁定):作用于主内存变量,把一个变量标识为一条线程独占状态。
· unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
· read(读取):作用于主内存变量,把一个变量从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
· load(载入):作用于工作内存变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
· use(使用):作用于工作内存变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时执行此操作。
· assign(赋值):作用于工作内存变量,把一个从执行引擎接收的值赋值给工作内存的变量,每当虚拟机遇到一个需要给变量进行赋值的字节码指令时执行此操作。
· store(存储):作用于工作内存变量,把工作内存中一个变量的值传递到主内存中,以便后续 write 操作。
· write(写入):作用于主内存变量,把 store 操作从工作内存中得到的值放入主内存变量中。
如果要把一个变量从主内存复制到工作内存就必须按顺序执行 read 和 load 操作,从工作内存同步回主内存就必须顺序执行 store 和 write 操作,但是 JVM 只要求了操作的顺序而没有要求上述操作必须保证连续性,所以实质执行中 read 和 load 间及 store 和 write 间是可以插入其他指令的。
Java 内存模型还会对指令进行重排序操作,在执行程序时为了提高性能编译器和处理器经常会对指令进行重排序操作,重排序主要分下面几类:
· 编译器优化重排序:编译器在不改变单线程程序语义的前提下可以重新安排语句的执行顺序。
· 指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行,如果不存在数据依赖性处理器可以改变语句对应机器指令的执行顺序。
· 内存系统重排序:由于处理器使用缓存和读写缓冲区使得加载和存储操作看上去可能是在乱序执行。
其实 Java JMM 内存模型是围绕并发编程中原子性、可见性、有序性三个特征来建立的,关于原子性、可见性、有序性的理解如下:
· 原子性:就是说一个操作不能被打断,要么执行完要么不执行,类似事务操作,Java 基本类型数据的访问大都是原子操作,long 和 double 类型是 64 位,在 32 位 JVM 中会将 64 位数据的读写操作分成两次 32 位来处理,所以 long 和 double 在 32 位 JVM 中是非原子操作,也就是说在并发访问时是线程非安全的,要想保证原子性就得对访问该数据的地方进行同步操作,譬如 synchronized 等。
· 可见性:就是说当一个线程对共享变量做了修改后其他线程可以立即感知到该共享变量的改变,从 Java 内存模型我们就能看出来多线程访问共享变量都要经过线程工作内存到主存的复制和主存到线程工作内存的复制操作,所以普通共享变量就无法保证可见性了;Java 提供了 volatile 修饰符来保证变量的可见性,每次使用 volatile 变量都会主动从主存中刷新,除此之外 synchronized、Lock、final 都可以保证变量的可见性。
· 有序性:就是说 Java 内存模型中的指令重排不会影响单线程的执行顺序,但是会影响多线程并发执行的正确性,所以在并发中我们必须要想办法保证并发代码的有序性;在 Java 里可以通过 volatile 关键字保证一定的有序性,还可以通过 synchronized、Lock 来保证有序性,因为 synchronized、Lock 保证了每一时刻只有一个线程执行同步代码相当于单线程执行,所以自然不会有有序性的问题;除此之外 Java 内存模型通过 happens-before 原则如果能推导出来两个操作的执行顺序就能先天保证有序性,否则无法保证,关于 happens-before 原则可以查阅相关资料