Java底层知识JVM二
一、Java的内存模型-JDK8
- 线程私有:
- 程序计数器:字节码指令
- 虚拟机栈:Java方法
- 本地方法栈:native方法
- 线程共享:
- MetaSpace:名称为元空间,类加载信息
- 常量池:字面量和符号引用量
- 堆:数组和类对象
1.1 程序计数器Program Counter Register
-
是当前线程锁执行的字节码行号指示器,逻辑地址
-
改变计算器的值来选取下一条需要执行的字节码指令,包括分支、循环等
-
和线程是一对一的关系,由于JVM的多线程是通过线程轮流切换并通过分配处理器执行时间来实现的,在任何一个确定的时间,处理器只会处理一个线程的一条指令,所以每个线程都有独立的程序计数器记录执行到哪一条指令
-
对Java方法计数,计数器则记录的是正在执行的虚拟机的字节码的指令地址,如果是Native方法则为null
1.2 Java虚拟机栈Stack
-
Java方法执行的内存模型
-
包含多个栈帧,因为每个方法被执行的时候都会创建一个栈帧,这是方法运行期间依赖的数据结构,栈帧里面包含局部变量表、操作栈、动态链接、返回地址等,补充:局部变量表包含方法执行过程中的所有变量,操作数栈用于入栈、出栈、复制、交换、产生消费变量
补充:局部变量表、操作栈与计数器讲解:
//创建文件
public class ByteCodeSample {
public static int add(int a,int b){
int c = 0;
c = a + b;
return c;
}
}
//终端反编译
F:\sc\src\main\java>javac com\derrick\jvm\model\ByteCodeSample.java
//口语化形式来描述字节码文件
javap -verbose javac com\derrick\jvm\model\ByteCodeSample.class
//获取add方法的描述
public static int add(int, int);
descriptor: (II)I //括号两个II表示接收两个int变量,括号外I表示返回值为int
flags: ACC_PUBLIC, ACC_STATIC //方法是public的,也是static的
Code:
stack=2, locals=3, args_size=2 //操作数栈深度为2,本地变量容量为3,变量参数容量为2
0: iconst_0
1: istore_2
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: iload_2
7: ireturn
LineNumberTable:
line 5: 0 //代码第五行对应字节码的第0行
line 6: 2
line 7: 6
补充:递归产生java.lang.StackOverflowError异常
当递归层数较小的时候可以计算出结果,但是递归层数大了之后结果为Exception in thread “main” java.lang.StackOverflowError,这是因为线程每执行一个方法时都会创建一个栈帧,并将栈帧压入到虚拟机栈中,当方法执行完毕后才将栈帧出栈,由于递归不断调用自己,递归过深,栈帧数超出虚拟栈深度
public class Fibonacci {
public static int fibonacci(int n){
if(n ==0)
return 0;
if(n==1)
return 1;
return fibonacci(n-1)+fibonacci(n-2);
}
public static void main(String[] args){
System.out.println(fibonacci(100000));
}
}
1.3 元空间MetaSpace与永久代PermGen区别
两者都是用来存在Class的信息,如method,field等,是方法区的实现,但是元空间使用本地内存,而永久代使用的是jvm的内存,则字符串常量池在永久代中,容易出现性能问题和内存溢出;类和方法的信息大小难以确定,给永久代的大小指定步确定;永久代会给GC带来不必要的复杂性
1.4 Java堆,即Java Heap
Java Heap是被所有线程共享的区,在虚拟机启动时创建,唯一目的便是存放对象实例,几乎所有对象实例都在这里分配内存,通过-Xmx控制其大小
二、JVM
2.1 调优参数
当调用java程序去执行指令的时候,可以调用以下3个参数去分别调整Java的堆、线程所占大小
-Xss:规定了每个线程虚拟机栈(堆栈)的大小,一般情况下256K,会影响并发线程数的大小-
-Xms:初始的Java堆的大小,即该进程创建出来的时候专属Java堆的大小
-Xmx:当对象容量超过Java堆初始容量,堆扩容到最大值
java -Xms128m -Xmx128m -Xss256k -jar xxx.jar
2.2 堆和栈区别
内存分配策略:静态存储(编译时确定每个数据目标在运行时的存储空间要求,因而在编译时就便可以分配固定的内存空间,则要求程序不含可变数据结构、递归等存在 );栈式存储(数据区需求在编译时未知,运行时模块入口前确定, 运行时当进入一个程序模块的时候,必须知道该程序模块数据区的大小,才能分配内存);堆式存储(专门负责在编译时或运行时模块入口都无法确定存储要求的数据分配)
联系: 引用对象、数组时,栈里定义变量保存堆中目标的首地址
管理方式: 栈自动释放,JVM可以自己针对栈进行操作,该内存空间的释放是编译器就可以操作的内容;而堆是由垃圾回收器进行自动回收
空间大小: 栈比堆小,堆空间在一个java程序中需要存储较多的对象数据
碎片相关: 栈产生的碎片远小于堆,这是因为堆操作量比较大
分配方式: 栈支持静态和动态分配,静态分配是本身由编译器分配好的,而动态分配是根据情况,堆仅支持动态分配
效率: 栈的效率比堆高
2.3 元空间、堆、线程独占部分间的联系——内存角度
public class HelloJessi {
private String name; //field
public void sayHello(){ //method
System.out.println("Hello"+name);
}
public void SetName(String name){ //method
this.name=name;
}
public static void main(String[] args){
int a=1;
HelloJessi hj = new HelloJessi(); //hj是存在于虚拟站的引用变量,指向我们真正创建好的实例
hj.SetName("Derrick");
hj.sayHello();
}
}
2.4 不同JDK版本之间的intern()方法的区别——JDK6 VS JDK6+
String s = new String("Derrick");
s.intern();
JDK6:当调用intern方法时,如果当前字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用,否则将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
JDK6+:当调用intern方法是,如果当前字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用,否则,如果该字符串已经存在java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回引用,如果堆中不存在,则在池中创建该字符串并返回其引用,避免常量池爆炸
public class InternDifference {
public static void main(String[] args){
String s1 = new String("a");
s1.intern();
String s2 = "a";
System.out.println(s1==s2);
String s3 = new String("a")+new String("a");
s3.intern();
String s4 = "aa";
System.out.println(s3==s4);
}
}
//JDK6 运行结果:false false
//JDK6+运行结果:false true
JDK6:interm是副本
JDK6+: interm是引用