第一章主要讲java发展时,jvm发展史,这里就跳过了
由于java的虚拟机自动内存管理机制,所以我们大部分情况下不会出现内存泄露和内存溢出的问题,但一旦出现,还是的有排查异常解决问题的能力,这里就需要理解虚拟机是怎么使用内存的。
这一章主要讲的是jvm内存的各个区域和其作用、服务对象、和可能产生的问题。
上图是jvm运行时的数据区
PS:jvm事实上也算是一个软件,从硬件->操作系统->JVM(JRE)->java程序
下图为java文件编译成class->运行jvm->运行本地功能功能的图(网上找的):
程序计数器是一块较小的内存空间。简单来说这部分是用来存放下一条指令的内存地址,当要执行下一条指令是,计数+1,指向下一条指令的内存地址。
程序计数器这个内存区域也是唯一一个没有规定任何OutOfMemoryError(内存溢出错误)的区域
原因很简单,因为程序计数器所需要存储的内容仅仅就是下一个需要待执行的命令的地址,所以无论代码怎么样都不会内存溢出。
PS:如果是多线程,那么每条线程都需要一个独立的呈程序计数器,它们之间互不影响,单独储存,这样的内存区域称为"线程私有"内存。
这块也是线程私有的,它的生命周期和线程相同,其描述的是java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧
栈帧:用于存储局部变量表、操作数栈、动态链接等。
方法从调用到执行完毕=栈帧在虚拟机栈中从入栈到出栈。
局部变量表存放编译时(写代码时)的那些可知的基本数据类型、对象引用(不是对象,只是指向地址的引用指针或相关位置)、returnAddress类似(返回地址,指向字节码指令的地址)。
其中基本数据类型64位的long和double会占用2个局部变量空间(slot),其余的基础数据类型占用一个空间。
这里虚拟机栈有两种异常情况的抛出处理:
1.StackOverflowError异常(堆栈溢出):栈深度大于虚拟机所允许的深度
2.OutOfMemoryError异常(内存溢出):所需内存超出拥有的内存
本地方法栈和java虚拟机栈类似,但是虚拟机栈执行的是java方法服务,而本地方法栈执行的是虚拟机使用到的Native方法服务。
其它的里面的结构:栈帧、线程私有,生命周期、异常抛出处理等都和java虚拟机栈类似。
java堆是java虚拟机管理内存中最大的一块。同时也是被所有线程共享的一块内存区域,所以它不是线程私有。
堆的作用是存放对象实例,几乎所有的对象实例和数组都会在这里分配内存
java堆同时也是垃圾回收(GC堆)管理的主要区域
这里存在的异常抛出处理是:OutOfMemoryError异常(内存溢出)
方法区和堆一样,都是各个线程共享的内存区域。
方法区的作用是储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区的别名Non-Heap(非堆)。
同样现在GC加入了方法区,所以GC也同时管理方法区,这个方法区主要回收的是针对常量池的回收和对类型的卸载。
这里存在的异常抛出处理是:OutOfMemoryError异常(内存溢出)
运行时常量池是属于方法区的一部分,用于存放Class文件编译时生成的各种字面两和符号引用。这些内容在类加载后加入运行时常量池。
这里存在的异常抛出处理是:OutOfMemoryError异常(内存溢出)
直接内存不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但这部分内存被频繁使用,也会有OutOfMemoryError异常。
直接内存的分配不会受到java堆大小的限制(java虚拟机规定内存),但还是受本机总内存的限制(就是电脑内存)。
虚拟机有好多不同的而HotSpot VM 是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机
这里是来理解我们代码中比较常用的创建对象的流程,比如:new String();
第一步:虚拟机遇到一条new指令时,先检查其参数是否能在常量池定位到类的符号引用,且检查该符号引用代表的类是否已被加载、解析、初始化过。有则进入下一步,没有则先执行类加载过程。
第二步:虚拟机为新生对象分配内存,其所需内存大小在类加载后确定,堆分配内存给对象。这里的堆分配分两种
1.java堆内存是绝对规整的:使用指针碰撞来分配内存(内存分两块,一块放空闲的,一块放使用的,中间放个指针分界)。
2.java堆内存是不规整的:使用空闲列表来分配内存(虚拟机维护一个列表,记录内存块的可用情况,实时更新列表,划分可用内存块)
这里需考虑分配冲突问题,有两种解决方法:
1.对分配内存空间的动作进行同步处理
2.内存分配的动作按照线程划分不同空间进行,每个线程在java堆中预先分配一小块内存(本地线程分配缓冲)
第三步:内存分配完成后,虚拟机需要将分配到的内存空间初始化为零值(不包括对象头)
第四步:虚拟机对对象进行必要的设置(例:对象属于哪个类的实例、如何找到类的元数据信息、对象的哈希吗、对象的GC分代年龄),这些信息存放在对象的对象头中。
至此,对象创建完成,不过一般来说,创建完成后还会将对象初始化。
在HotSpot虚拟机中,对象在内存中存储布局分为3块区域:对象头(Header)、实例数据(Instancs Data)和对齐填充(Padding).
第一部分:存储对象自身的运行时数据(哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等)
第二部分:类型指针(即对象指向它的类元数据的指针),虚拟机通过这指针来识别是属于哪个类实例。
包含对象真正存储的有效信息(例:代码中的成员字段)。
对齐填充并不是必然存在的,它的作用仅仅是占位符的作用,因为HotSpot VM内存要求对象起始地址必须是8字节的整倍数,所以有缺的话,就需要通过对齐填充来补全。
之前创建了对象,这里讲使用对象。java程序是需要通过栈上的reference数据来操作堆上的具体对象的。
而reference类型只规定了一个指向对象的引用,并没有定义这个引用怎么去定位、访问堆中对象的具体位置。所以对象访问的方式是不同的,主流有两种访问方式:使用句柄、直接指针
使用句柄:java堆会划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄包含对象实例数据和类型数据等具体的地址信息。
直接执政:reference中存储的就是对象地址,没有像句柄一样的中间件。
使用句柄的优势:reference存储的是稳定的句柄位置,对象移动只会改变句柄的实例指针,不会改变reference
直接指针访问的优势:速度更快
在这里会通过实际的代码,体验各种内存区域的OutOfMemoryError异常。
java堆用于存储对象实例,所以不断创建对象,防止GC回收,就能造成堆溢出。
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args){
List list=new ArrayList();
while(true){
list.add(new OOMObject());
}
}
}
解决方法:通过内存映像分析工具进行分析,分清是内存泄露还是内存溢出。再找到对应出现问题的对象加以解决。
栈有两种情况,一种是栈溢出,一种是栈内存溢出
public class StackSOError {
private int stackLength=1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
StackSOError soError=new StackSOError();
try{
soError.stackLeak();
}catch(Throwable e){
System.out.println("沙丁鱼的栈帧长度:"+soError.stackLength+" 出现栈溢出");
throw e;
}
}
}
由于代码可能造成操作系统假死状态,这边就不演示了。总的来说和堆溢出的代码差不多,就是将对象转变成立线程来做。
public class RuntimeConstantPoolOOM {
public static void main(String[] args){
List list=new ArrayList();
int i=0;
while(true){
list.add(String.valueOf(i++).intern());
//System.out.println(list);
}
}
}
因为等了段时间都没抛出异常,所以暂时就不截图结果了。
直接内存也会出现溢出,不过情况和其他的都差不多,都是内存不足就出现了,这里也不写了。
这一章主要是虚拟机运行时数据区的体系结构、对象创建使用的虚拟机内部流程、溢出情况出现的案例。