前言:
介绍JVM的内存模型和内存溢出异常
.java .class(字节码文件)
- javac : 编译指令
- Javadoc 指令:它从程序源代码中抽取类、方法、成员等注释形成一个和源代码配套的API帮助文档。也就是说,只要在编写程序时以一套特定的标签作注释,在程序编写完成后,通过Javadoc就可以同时形成程序的开发文档了。
javadoc命令是用来生成自己API文档的,类或者方法上通过标记进行解释如:@Author。javadoc 会抽取这些标记,形成和源代码配套的API帮助文档。
使用方式:使用命令行在目标文件所在目录输入javadoc +文件名.java。 - javap :反汇编指令,可以进行反汇编查看编译之后的字节码文件
- jar :java archive java 归档,包含了大量的Java类文件和相关数据文件
Java虚拟机向多语言虚拟机发展,可以在JVM上面运行的语言越来越多,并不只是Java
Java8 中添加了Lambda 更加适合于函数式编程(函数式编程更加适合于并行)
一:JVM 的内存模型
JVM在内存中有一块自己的管理区域,执行时会将这块区域划分为不同的数据区,有些区域随JVM进程的启动而存在,有些区域则依赖用户线程的启动/结束而创建/销毁
- 程序计数器(Program counter Register): 线程私有,相互独立,指向下一条将要执行的字节码
- 虚拟机栈(VM stock) : 线程私有,与线程的生命周期相同。描述Java方法执行的内存模型:每个方法执行时会创建一个栈帧:用于存储局部变量表,操作数栈,动态链接。方法的调用执行到结束就是一个栈帧入栈到出栈的过程
局部变量表存储了编译时期可知的基本数据类型 - 本地方法栈(Native Method stock):与虚拟机栈作用相似,VMStock 中是虚拟机执行的java 方法,Native Method Stock 是虚拟机执行的Native 方法。
- Java 堆(java Heap):虚拟机启动时创建,所有的对象实例,数组都要在对上面进行分配,也是垃圾收集管理的主要区域,GC堆(Garbage Collection Heap),要求在内存中不一定物理连续,但是一定要逻辑连续
- 方法区(Method Area):存储被加载的类信息,常量,静态变量,即时编译器编译后的代码
这个区域的内存回收主要针对常量池的回收会对类型的卸载。
JVM的内存模型:
方法区中有一个重要的部分: 运行时常量池
JDK1.7 以前字符串常量池在Pergem区中(永久带),大小固定,不能被垃圾回收,大量使用字符串native 方法intern 会造成(JDK1.6 以前常量池也在永久带中)
JDK1.7 以后虚拟机去永久化发展
JDK1.7 之后字符串常量区就在堆中,可以被垃圾回收
对象创建
虚拟机意见new 指令时,会先检查是否能在常量池中定义到一个类的符号引用,检查这个符号引用是否被加载,如果没有加载,要先加载类。
为对象实例分配内存空间,类加载之后就可以确定对象所需的内存空间
内存分配的方式
如果java 堆内存是绝对规整的,那么所有用过的内存放在一边,没有用过的放在一边,中间用一个指针作为分界指示器,分配内存就只是将指针向空闲的区域移动对象大小的空间,这叫 “指针碰壁”
如果Java堆内存不是规整的,那么虚拟机要维护一个列表,记录Java堆的内存使用情况,当为对象实例分配内存的时候,从列表中找一块适合的空间分配给对象,这叫 “空闲列表” 法
Java堆空间是否规整取决于垃圾收集器是否带有压缩整理功能
内存分时还要考虑,分配是否安全,以防出现,为A分配空间,指针还没来得及修改,B就使用了那片空间。解决方案,一是: 保证更新方式对的原子性;二是: 把内存的分配动作按照线程划分在不同的空间中进行,每个线程预先在Java堆中分配一个内存(TLBA),每个线程在自己的空间中完成对象的内存分配,只有当TLBA 用完并分配新的TLBA时,保证操作的原子性
内存分配之后,内存要将分配的内存空间进行初始化,如果使用TLBA,这个擦欧总可以提前到TLBA进行内存分配时执行,对象中的字段,进行对应类型的零值初始化。
在虚拟机的角度,对象的创建已经完成,但是在程序员的角度对象的创建才开始,new 指令之后,执行 方法,按照程序员的意愿对实例进行初始化。
对象的内存布局
对象自己的存储布局:对象头,实例数据,对齐填空
- 对象头包含两部分:一是对象自己的运行时数据(哈希表,GC年龄带,锁状态标志,线程持有的锁…) 对象头中的数据是与对象自身定义的数据无关的数据。
另一部分信息是类型指针,JVM 通过这个来确定这个对象是那个类的实例 - 实例数据才是才是对象存储的真正有意义的数据,也是程序中定义的各种字段,包括自己的,从父类继承的。
JVM通过对象头的信息得到对象的大小。
对象的访问定位
JVM通过虚拟机栈中的reference 对对象进行访问。reference 只规定了一个引用但是并没有规定JVM如何通过这个引用去访问Java堆中的对象,
- 带有句柄池,直接指针。reference 存储对象句柄的地址。
- reference直接存储Java堆中对象的地址
reference 直接指向对象实例数据,当的对象移动时(这在堆中十分常见)就需要修改reference 的值,但是这样的访问速度快
使用句柄池,对象移动之后就只需要修改句柄池中指向对象实例数据的指针
二:内存溢出异常:OutOfMenoryError
介绍内存溢出的另一个原因是为了,高清楚一些数据到底保存在JVM的那块区域
- Java堆溢出
堆中的对象太多,并且JVM没有进行垃圾回收就会出现溢出 - 虚拟栈和本地方法栈的溢出
1. 线程请求的深度大于JVM所允许的最大深度(StackOverFlower),操作系统为每个线程分配的内存空间是有限制的,当该线程请求的栈内存大于这个限制就会抛出StackOverFlower
2. 当有多个线程,虚拟机扩展栈时无法申请到空间(OutOfMenoryError)
这虽是两种情况,但本质上是描述的一个问题。在单线程的情况下只会出现第一种情况的异常 - 方法区溢出:方法去存放CLAss相关信息,类名,访问修饰符,运行时常量池,字段描述,方法描述等,大量的运行时产生的类填满这个区域就会产生溢出