PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
代码:
public class PCRegisterTest {
public static void main(String[] args) {
int i = 1;
int j = 2;
int k = i + j;
String s = "abc";
}
}
对生产的class文件反编译得到:
虚拟机栈描述的是Java方法执行的内存模型。
虚拟机栈是线程私有的,生命周期与线程相同。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈)也可以设置固定不变,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
特点:
配置设置:
可以通过参数设置大小(栈内存等于线程分配的内存 减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量)),IDEA中可以在VM options中设置。
-Xms 为jvm启动时分配的内存,比如-Xms200m,表示分配200M
-Xmx 为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存
-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M
局部变量表也被称之为局部变量数组或本地变量表定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference) ,以及returnAddress类型。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
例如:LocalVariablesTest类有以下成员方法
public static void main(String[] args) {
LocalVariablesTest test = new LocalVariablesTest();
int num = 10;
test.test1();
}
public void test1() {
Date date = new Date();
String name1 = "atguigu.com";
test2(date, name1);
System.out.println(date + name1);
}
反编译后:
Length表示变量的作用域长度。例如Start 8 Length 8 表示从第bipush 10(常量池中的第10号) 开始声明,到第16(8+8)的偏移坐标开始失效,也就是return指令
局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没
有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一
个boolean、byte、char、short、int、float、reference或returnAddress类型的数据
Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference和returnAddress 8种类型。
对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。64位的数据类型只有long和double两种。
第0位索引的Slot默认是用于传递方 法所属对象实例的引用,在方法中可以通过关键字"this"来访问到这个隐含的参数。其余参数则按照参数表顺序排列。
Slot位是可以复用的。从而达到节省资源的目的:
代码:
public int getOperandStackReturn() {
int a = 1;
int b = 2;
return a + b;
}
public void operandStackTest() {
int a = getOperandStackReturn();
int b = 3;
int c = a + b;
}
执行operandStackTest()方法对应的操作:
例如:
public class DynamicLinkingTest {
int num = 10;
public void methodA(){
System.out.println("methodA()....");
}
public void methodB(){
System.out.println("methodB()....");
methodA();
num++;
}
}
methodB()对应的字节码文件解析:
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与 调试相关的信息,这部分信息完全取决于具体的虚拟机实现,这里不再详述。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的,这类方法的调用称为解析(Resolution)。
在Java虚拟机里,一共提供了5条调用方法的字节码指令。分别是:
invokestatic(调用静态方法)
invokespecial(调用实例构造器<init>方法、私有方法和父类方法)
invokevirtual(调用所有的虚方法)
invokeinterface(调用接口方法,会在运行时再确定一个实现此接口的对象)
invokedynamic(先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方
法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令
的分派逻辑是由用户所设定的引导方法决定的。)
调用invokestatic、invokespecial指令的方法,都可以在解析阶段确定唯一的调用版本,也就是静态方法、私有方法、实例构造器、父类方法这4类。它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去final方法)。
虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,又或者说多态选择的结果肯定是唯一的。所以final方法是一种非虚方法。
静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。
静态分派的典型应用是方法重载overlord。
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello,guy!"");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man); //hello,guy!
sr.sayHello(woman); //hello,guy!
}
}
这里的Human就是静态类型,它对Man或者Woman进行了"包装",在编译期时我们就可以得知的类型,而它的实际类型(这里指的是Man或者Woman)只有在运行期才能知道。当然,如果我们运用强转换会改变它的静态类型。
sr.sayHello((Man)man); //hello,gentleman!
sr.sayHello((Woman)woman); //hello,lady!
静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
很多情况下这个重载版本并不是"唯一的",往往只能确定一个"更加合适的"版本。
还是用上面的例子,将public void sayHello(Man guy) 方法注释掉
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
/*public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}*/
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello((Man)man); //hello,guy!
sr.sayHello((Woman)woman); //hello,lady!
}
最后"sr.sayHello((Man)man); "的输出是hello,guy!,原因是它会去寻找"更加合适的"版本。
比如一个字符’a’,调用的方法有char、int、long、float、double等多个重载版本,若注释掉char重载版本,它会去调用参数为int类型的重载版本(因为‘a’的Unicode数值为十进制数字97),若再去掉int类型的版本,会去调用long版本,按照char->int->long->float->double的顺序转型进行匹配。
动态分派
动态分派的典型应用是方法重写override
package com.atguigu.java2;
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello(); //man say hello
woman.sayHello(); //woman say hello
}
}
man.sayHello();和woman.sayHello();对应的字节码指令是"invokevirtual #常量池引用",然而invokevirtual指令的运行时解析过程大致分为以下几个步骤:
简单的说就是子类中若有签名匹配的则直接选择子类的方法,若没有则向父类查找。
单分派和多分派
根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
public class Dispatch {
static class QQ {
}
static class _360
{
}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father {
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360()); //father choose 360
son.hardChoice(new QQ()); //son choose qq
}
}
(深入java虚拟机p285)
编译阶段编译器的选择过程(是静态分派的过程):要进行两次宗量的选择,一是静态类型是Father还是Son,二是方法参数是QQ还是360。这次选择结果的 最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向 Father.hardChoice(360)及Father.hardChoice(QQ)方法的符号引用。因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。
再看看运行阶段虚拟机的选择,也就是动态分派的过程。在执行"son.hardChoice(new QQ())“这句代码时,更准确地说,是在执行这句代码所对应的invokevirtual指令时,由于 编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机此时不会关心传递过来的 参数"QQ"到底是"腾讯QQ"还是"奇瑞QQ”,因为这时参数的静态类型、实际类型都对方法的 选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接受者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。
根据上述论证的结果,我们可以总结一句:今天(直至还未发布的Java 1.8)的Java语言 是一门静态多分派、动态单分派的语言。强调"今天的Java语言"是因为这个结论未必会恒久 不变,C#在3.0及之前的版本与Java一样是动态单分派语言,但在C#4.0中引入了dynamic类型 后,就可以很方便地实现动态多分派。
新生代与老年代默认的初始内存比例是1:2。可以通过-NewRatio参数设置它们的比例,一般情况下不会去修改。
新生代中的Eden和Survivor的大小比例默认是 8:1(官方文档指出是8:1,但实际中可能因为内存的自动适应等原因,比例为6:1),当然我们可以通过"-XX:SurvivorRatio"参数进行更改(实际操作中有些细节需要注意)。
几乎所有的Java对象都是在Eden中被new出来的(也有可能会在老年代中创建,后面提起空间分配担保再解释)。
绝大部分对象的销毁都处于新生代中(根据IBM公司研究大概在80%左右,也就是为什么Eden区跟Survivor区默认为8:1的原因)。
可以用参数"-Xmn"设置新生代的最大内存(比较少用到)。
1.注:当Eden区空间满了,这时候再创建对象的时候才会发生GC,并不是对象一死亡就发生GC。
2.
以上为对象比较普遍的分类与回收,当然下面是比较详细的情况:
例如:
public static StringBuffer createStringBuffer(String str){
StringBuffer sb = new StringBuffer();
sb.append(str);
return sb;
}
public static void test(){
StringBuffer stringBuffer = createStringBuffer("hello");
}
test在createStringBuffer()方法的外部,却能引用到createStringBuffer()方法中的对象sb,则认为对象发生逃逸。
public static StringBuffer createStringBuffer(String str){
StringBuffer sb = new StringBuffer();
sb.append(str);
return sb.toString();
}
public static void test(){
String string = createStringBuffer("hello");
}
这个情况就没有发生逃逸:因为createStringBuffer()中返回时调用toString(),在方法外部就无法引用对象sb了。
栈上分配:
同步消除:
标量替换:
标量(Scalar):是指一个数据已经无法再分解成更小 的数据来表示了,Java虚拟机中的原始数据类型(int、long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。
把一个Java对象拆散, 根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。
如果逃 逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替,就可以将这些成员变量分配在栈上。
public class ScalarReplace {
public static class Position {
public int x;
public int y;
public Position(int x, int y) {
this.x = x;
this.y = y;
}
}
/**
*若开启标量替换,test1方法会转变成test2方法,将操作数x,y存入栈中
*/
public static void test1() {
Position p = new Position(1,1);
}
public static void test2() {
int x = 1;
int y = 1;
}
}
常量池表:
是class文件的一部分,用于存放编译器生成的各种字面量与符号引用,这部分内容将会在类加载后存放到方法区的运行时常量池中。
包括了关于类、方法、接口等中的常量,也包括字符串常量和符号引用
public class MethodInnerStrucTest extends Object implements Comparable<String>,Serializable {
public int num = 10;
private static String str = "测试方法的内部结构";
public static int test2(int cal){
int result = 0;
try {
int value = 30;
result = value / cal;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
补充:
- 全局常量:用static final修饰符修饰的,该常量在编译的时候就被分配了(也就是还没有被类加载器所加载就已经完成了分配)
运行时常量池:
注:方法区的回收效果并不理想,尤其是对类型的回收。所以Java虚拟机规范中并没有强制规定对方法区进行回收,但有时候确实有必要对方法区进行回收。
直接内存处于Java堆外,向系统申请的内存。
来源于NIO(JDK1.4后引入,New-IO),通过堆中的DirectByteBuffer操作Native内存。
直接内存大小可以通过参数"MaxDirectMemorySize"设置,若不指定则跟-Xmx参数一致(默认为-1,无上限)。
直接内存也会发生OOM。
不受JVM管理,回收成本高。
访问直接内存速度会快于Java堆,即读写性能高,若读写频率高的话建议使用直接内存。
2