一、基础知识
1、jdk jre jvm
- jdk=jre(java的运行环境)+编译工具
- jre=jvm+基础类库
- jdk > jre >jvm
2、jvm(Java Virtual Machine)
- java二进制字节码的运行环境 (.class -----jvm----->二进制机器码)
- 跨平台,一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 多态
二、jvm内存
- 线程私有:虚拟机栈,本地方法栈,程序计数器
- 线程共享:方法区,堆
1、程序计数器 Program Counter Register
指令计数器,记录下一条jvm指令的执行地址,在寄存器中寄存信息,确保jvm中多线程正常运行。
在程序开始执行前,必须将它的起始地址,即程序的一条指令所在的内存单元地址送入程序计数器,因此程序计数器的内容是从内存提取的第一条指令的地址。当执行指令时,CPU将自动修改PC的内容,即每执行一条指令程序计数器增加一个量,这个量等于指令所含的字节数,以便使其保持的总是将要执行的下一条指令的地址;
执行java方法时,程序计数器时有值的,执行的native本地方法时,程序计数器的值为空,因为native方法是java通过JNI直接调用本地C/C++库
由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此在任一时刻,一个CPU的内核只会执行一条线程的指令。因此为了能够是的每个线程都在切换后能够恢复到切换之前的程序执行位置,每个线程都需要自己独立的程序计数器,并且不能互相干扰,因此,程序计数器时每个线程所有的。
特点:
- 线程私有,生命周期同线程的生命周期一样
- 不会发生OOM
2、虚拟机栈(java栈)
栈是线程运行需要的内存空间 ,栈存放的是一个个栈帧(每个方法运行时需要的内存),每个栈帧用来存储局部变量表,操作栈,动态链接,方法出口等信息,每个方法被执行的时候会创建一个栈帧并压栈、方法执行完毕后,出栈。
活动栈帧是线程正在执行的方法,唯一虚拟机栈的顶部 ,每个线程只有一个活动栈帧。
虚拟机栈特点:
- 线程私有
- 栈溢出(栈帧过多(递归),栈帧过大),会抛出java.lang.StackOverFlowError
- 垃圾回收不涉及栈内存
- 栈内存并不是划分的越大越好(栈内存越大,物理内存不变,线程数就变少)
(1)局部变量表
- 局部变量表是32位,如果变量是double(64位),会存储两个32位
- 局部变量表定义对象,存储的对象的引用,并不是对象本身
- 方法内的非静态局部变量如果没有逃离方法的作用范围是线程安全的
3、本地方法栈
本地方法栈和虚拟机栈的作用和原理非常相似,区别只不过是java栈是为执行java方法服务的,而本地方法栈则是为执行本地方法(Native method)服务的,如Object 的clone()方法,hashCode(),notify()等。
4、Heap堆
堆在虚拟机启动时创建,是java虚拟机所管理的内存中最大的一块,java堆是所在进程所有线程共享的一块内存区域,也是垃圾收集器管理的主要区域,因此很多时候也被称为GC堆。
特点:
- 线程共享
- 有垃圾回收机制
- 会发生OOM
5、方法区
(1)方法区组成
存储了类字节码(类的名称,构造函数,方法,字段),静态常量,常量以及编译器编译后的代码等。
运行时常量池也是其重要组成部分
特点:
- 线程共享,需要考虑线程安全
- 会发生OOM
(2)运行时常量池
二进制字节码文件包含类基本信息,常量池,类方法(包含了虚拟机指令),常量池就是一张表,用于存放编译器生成的各种字面两盒符号引用,虚拟机指令可以根据这张表找到执行的类名,方法名,参数类型,字面量等信息。
运行时常量池,二进制字节码被加载,常量池信息都会加载到运行时常量池,这时常量池中的字符串仅是符号,还没有变成java字符创对象,只有方法运行到这里才变为对象;在运行期也可将常量放入运行时常量池中,如String的intern()。
(3)StringTable串池(底层是hash表)
运行时常量池的字符串只有在运行到这个地方时才会去创建成字符串对象;创建前先去Stringtable中检索是否存在这个字符串,如果存在,则返回Stringtable中的内存地址,如果不存在,创建并加入Stringtable,返回内存地址。利用这个机制,可以避免重复创建字符串对象。
字符创常量拼接是编译器完成的。
//StringTable
public static void main(Stirng[] args){
String s1="a";//运行到这一步,先去StringTable中检索,没有则添加进去,Stringtable中有了"a",s1指向内存地址
String s2="b";//同第一步
String s3="ab";//同第一步
String s4="a"+"b";//在编译器会拼接字符串,所以s4="ab",检索Stringtable,已经存在s1==s4
String s5=s1+s2;//new Stringbuilder().append("a").append("b").toString() toString()是new String("ab");
}
(4)string.intern(),主动将串池中还没有字符串对象放入串池,jdk1.8逻辑如下:
public static void main(String[] args){
//第一种情况
String s="ab";
String s2=new String("a")+new String("b");
String s3=s2.intern();
System.out.println(s==s3); //true
System.out.println(s==s2); //fasle
//分析 第一步把“ab”放入Stringtable,
第二步创建s2,s2对象在堆中,
第三步,先判断Stringtable存在了“ab”,S2存入Stringtable失败,
s3返回的是Stringtable中的内存地址,s2地址没变,所以s==s3,s!=s2
//第二种情况
String s2=new String("a")+new String("b");
String s3=s2.intern();
String s="ab";
System.out.println(s==s3); //true
System.out.println(s==s2); //true
//分析 第二步,Stringtable中没有“ab”,S2添加成功,s2指向了Stringtable中的内存地址,
返回的是s3,s3也是指向Stringtable中的内存地址
第三步,StringTable中已经有了“ab”,直接返回地址,所以 s2==s3==s
}
(6)总结
- 方法体中的引用变量和基本类型的变量都在栈上,其他都在堆上
- 成员变量作为对象的属性,当然是放在堆里了
- Stringtable的存放区域变化
- 字节码文件.class可以通过javap命令反编译成我们可以读懂的代码