java内存管理
一、什么是虚拟机,什么是java虚拟机
1.1虚拟机
定义: 模拟某种计算机体系结构,执行特定指令集的软件
分类 系统虚拟机(VMware,Virtual Box等), 进程虚拟机
1)进程虚拟机
特点:并不会完整的模拟一个操作系统的运行环境,仅仅提供了特定指令集的运行环境
实例: JVM, Adobe Flash, FC模拟器
2)高级语言虚拟机
特点:把特定指令集范围进一步限定为高级语言。
属于进程虚拟机的一种,如有 JVM, .NET CLR, P-Code
3)java语言虚拟机
可执行java语言的高级语言虚拟机。java语言虚拟机并不一定可以称为JVM, 例如:Apache Harmony
4)java虚拟机
- 必须通过java TCK 的兼容性测试的java语言虚拟机才能称为java虚拟机
- java虚拟机并非一定要执行java程序,java虚拟机是和java编译后的class文件相关。
- 业界三大商用JVM : Orcale HotSpot , Oracle JRockit VM , IBM J9 VM
1.2概念模型和具体实现
共有设计,私有实现
java虚拟机规范声明了概念模型,这些概念模型不约束虚拟机的具体实现,只要求虚拟机实现的效果在外部看起来和规范描述一样即可。
例如规范中规定了java堆中的对象所在的内存,需要虚拟机自动完成垃圾回收,这个实现过程可以不同,但是效果需要相同。
1.3java虚拟机运行时数据区
- 在java虚拟机规范中定义了若干中程序运行期间会使用到的存储不同类型的区域。
- 有一些区域是全局共享的,随着虚拟机的启动而创建,随着虚拟机退出而销毁。有些区域是线程私有的,随着线程开始和结束而创建和销毁。
- 是所有java虚拟机共同的内存区域概念模型
运行时数据区域的划分:
- 程序计数器
- java堆
- java虚拟机栈
- 本地方法栈
- 方法区
其中 :
方法区和堆是所有线程共享数据区。
虚拟机栈、本地方法栈、程序计数器 是线程私有的。
二、程序计数器区(pc)
- 是所有java运行时内存区域最小的一块,
- 作用: 可以看做是当前线程所执行的字节码的行号指示器。(可以看做是指针,指向当前运行的那行字节码代码)
- 注意:如果正在执行一个java方法,则pc记录的是正在执行的虚拟机字节码指令的地址;如果正在执行 Native方法,则pc为空。
- 此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
三、java虚拟机栈和本地方法栈
3.1 java虚拟机栈的概念和特征
- 线程私有,生命周期和线程相同
java虚拟机栈描述的是java方法执行时候的内存概念模型,每个方法在执行时都会创建一个栈帧(用来创建这个方法的操作数栈,局部变量表,方法出口,动态连接等信息)。每一个方法在调用和结束的过程,就对应了一个栈帧在虚拟机的入栈和出栈的过程。 - 后进先出(LIFO)栈
靠后执行的方法会先优先完成。 - 存储栈帧,支撑java方法的调用、执行和退出
- 可能会出现OutOfMemoryError异常和StackOverflowError异常
线程请求的栈深度大于java虚拟机所允许的最大深度将会抛出 StackOverflowError异常。
若java虚拟机被设计为可动态扩展,而动态扩展时又不可以申请到足够的内存时就会抛出OutOfMemoryError异常。
3.2 本地方法栈的概念和特征
- 线程私有
- 后进先出(LIFO)栈
- 作用是支撑Native方法的调用、执行和退出
- 可能会出现OutOfMemoryError异常和StackOverflowError异常
- 一些虚拟机(如HotSpot)将java虚拟机栈和本地方法栈合并实现了
3.3 栈帧的概念和特征
- java虚拟机栈中存储的内容,它被用于存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法和异常分派
- 一个完整的栈帧包含: 局部变量表、操作数栈、动态链接信息、方法正常完成和异常完成信息
在编译程序代码时,栈帧需要的局部变量表的大小和操作数栈的深度,在编译期间就已经完全确定了,java虚拟机会把这些数据完全写在class文件的code表中。因此一个栈帧需要分配的内存大小不会受程序运行期间变量数据的影响,而仅仅取决于具体的java虚拟机。
在一个线程里面方法的调用链可能很长,很多方法可能都同时处于执行的状态,对于执行引擎而言,在活动线程之中,只有位于java虚拟机栈顶的栈帧才是有效的,这个栈帧被称为当前栈帧,与此栈帧相关联的方法被称为当前方法。虚拟机中所有执行的字节码指令都针对当前方法和当前栈帧操作。
局部变量表概念和特征
定义: 是一组变量值的存储空间,用于存储方法,参数和方法内部定义的局部变量等等。
在java编译器编译class文件时,就已经确定了该方法需要的局部变量表的最大容量,局部变量表是以槽(Slot)为最小单位的。java虚拟机规范中没有明确一个slot的具体占用的内存大小,只是描述了任何slot可以储存的类型。
- 由若干Slot组成,长度由编译期决定
- 单个Slot可以存储一个类型为 boolean, byte, char, short, float, reference 和 returnAddress的数据,两个Slot可以存储一个类型为long或者double的数据。
其中, reference类型表示一个对象实例的引用,通过reference可以间接或者直接查找到变量的实例数据,还可以通过reference直接或者间接的查找到这个对象的类型数据,而returnAddress 已经弃用。 - 局部变量表用于方法间参数传递,以及方法执行过程中存储基础数据类型的值和对象的引用。
在方法执行中,如果正在调用的方法是一个实例方法,那这个方法的局部变量的第0号Slot将默认作为存储这个方法的实例引用,在方法之中可以通过关键字diss俩访问到这个隐含的参数。剩下的方法参数将会按照参数表的顺序排列,占用从1开始的局部变量表空间。在参数表分配完毕之后,在根据方法表中的局部变量定义顺序和他们的作用域来分配剩下的局部变量Slot,为了节省栈帧的空间,局部变量表中Slot是可以被重用的。
操作数栈的概念和特征
- 单个操作数栈的元素,被称为Entry
- 后进先出栈,由若干个Entry组成,操作数栈最大栈深度在编译期间决定
- 单个Entry即可以存储一个java虚拟机中定义的任意数据类型的值,包括long和double类型,但是存储long和double类型的Entry深度为2,其他类型的深度为1。
- 在方法执行过程中,操作数栈用处存储计算参数和计算结果;在方法调用时,操作数栈也用来准备调用方法的参数以及退出时的返回结果。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中会遇到各种字节码指令往操作栈中写入和提取内容就对应了操作数栈的出栈和入栈操作。
3.4 本地变量表和操作数栈实例
在cmd中输入
copy con Test.java
然后输入程序:
public class Test{
public int calc(){
int a =100;
int b =200;
int c =300;
return (a+b)*c;
}
}
然后编译
javac Test.java
再查看字节码:
javap -verbose Test.class
得到:
0: bipush 100 //把100入栈到操作数栈的栈顶
2: istore_1 //把操作数栈顶的元素出栈并把此元素存储在局部变量表中的1号Slot
3: sipush 200
6: istore_2
7: sipush 300
10: istore_3
11: iload_1 //将局变量表中的为1号的操作数入栈到操作数栈栈顶
12: iload_2 //将局变量表中的为2号的操作数入栈到操作数栈栈顶
13: iadd //将操作数栈栈顶的两个元素出栈,然后相加入栈
14: iload_3
15: imul
16: ireturn
3.5 内存异常实例
注意: 在 idea中可以通过设置选项中的run --> Edit configuration --> configuration --> VM options 设置java虚拟机栈的大小 添加: -Xss128k
即可。
StackOverflowError异常
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try{
oom.stackLeak();
}catch (Throwable e){
System.out.println("length" + oom.stackLength);
throw e;
}
}
}
OutOfMemoryError异常
public class JavaVMStackOOM {
private void dontStop(){
while(true){
}
}
public void stackLeakThread(){
while(true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakThread();
}
}
四、java 堆
4.1 java堆的概念
特征:
- 全局共享
- 通常是java虚拟机中最大的一块内存区域
- 作用是作为java对象的主要存储区
- JVMS明确要求该区域需要实现自动内存管理,即常说的GC,单并不限制采用哪种算法和技术去实现
- 可能出现OutOfMemoryError
java堆的实现可以是固定大小的,也可以是动态扩展的,现在的主流虚拟机都是按照可扩展方式的来实现java堆的,java堆没有内存可用时就会抛出此 OOM 异常。
划分方式:
- TLAB: java堆中的对象时线程共享的,所以就会产生数据竞争,为了避免这种竞争,java虚拟机很可能会将堆又根据各个线程来划分出若干个线程私有的内存缓冲区,这一类缓冲区被称为TLAB,即线程本地分配缓存。这时各个线程会在各自的TLAB中分配对象,仅在TLAB用完时才会加锁,并向java堆分配新的TLAB内存。 -Xmx512m(设置最大值) -Xms16m(设置最小值)
- 新生代,老年代,永久代,这是基于一种GC回收内存的算法来划分的。
4.2 栈和堆
从栈到堆的关联过程:
第二种实现方式:
对比:
第二种方式中,reference中存储的是稳定的句柄的地址,在对象被移动时(对象经常被移动,垃圾回收时会发生移动),只会改变句柄池中的到对象类型数据的指针,而reference不会改变。
第一种方式,速度更快,对比使用句柄的方式,节省了指针开销,reference直接指向了对象的实例数据。
4.3 java堆内存异常实战
可能发生的异常:
- 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那java虚拟机将会抛出一个OOM异常。java堆是出现内存异常概率最大的区域。
出现OOM实例如下:
不断的创建对象,并且保证这些内存不被回收即可做到。
public class javaHeapOOM {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while(true){
list.add(new OOMObject());
}
}
}
五、方法区和运行时常量池
5.1 方法区的概念
- 全局共享
- 作用是存储java类的结构信息
- JVMS不要求该区域实现自动内存管理,但是基本都能实现自动管理该区域的内存
- 可能出现OutOfMemoryError异常
注意:
类实例数据 和 类类型信息
实例数据是指在类中定义的各种实例对象以及它们的值,而类型信息是指定义在类代码中的常量,静态变量,类中声明的各种方法,字段等,还会包括即时编译器编译产生的数据。
5.2 运行时常量池的概念
- 全局共享
- 是方法区的一部分
- 作用是存储java类文件常量池中的符号信息
5.3 HotSpot 方法区实现的变迁
永久代与方法区
- 在JDK1.2~JDK1.6, HotSpot使用永久代实现方法区
- 在JDK7开始,HotSpot开始了移除永久代的计划
- 符号表被移到Native Heap中
- 字符串常量和类的静态引用被移到Java Heap中
- 在JDK8开始,永久代已被元空间(MetaSapce)所代替
5.4 方法区内存异常实战
永久代变迁过程导致的异常差异:
实例1:
public class RuntimeConstantPoolChange {
public static void main(String[] args) {
String str1 = new StringBuilder("hptj").append("zzj").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
intern方法:定期的维护了一个字符串池,如果字符串中有这个字符串,则会返回这个池中常量的地址,如果这个字符串中没有这个字符串所需的常量,则jvm会先把这个字符串加到字符串常量池中,然后再返回这个字符串的地址。
可知:java1.7开始后字符串常量池被移到了Heap中,
当采用jdk1.6运行时,则出现 false false ,因为 str1.intern返回的是常量池中的地址,而str1本生的地址是在heap中的是不可能相同的。
当采用jdk1.7运行时,则出现 true false , 因为 str1.intern和heap都是堆中的地址,则这个hptjzzj字符串在heap中没有出现过,则会加入到heap中的常量池中,并返回此地址和str1在堆中的地址一致,而java字符串在堆常量池中已经存在,当new之后会新建一个对象,所以是不相等的。
六、直接内存
6.1 直接内存的概念和特征
- 并非JVMS定义的标准java运行时内存区域
- 随JDK1.4中加入的NIO被引入,目的是避免在java堆和Native堆中来回复制数据带来的性能损耗
- 全局共享
- 能被自动管理,但是检测手段上可能会有一些简陋
- 可能出现OutOfMempryError异常