JVM 知识点:
内存模型:
Java
虚拟机主要分为以下一个区
:
方法区:
1.
有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生
GC
,在这里进行的
GC
主要是对方法区里的常量池和对类 型的卸载
2.
方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代
码等数据。
3.
该区域是被线程共享的。
4.
方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有
动态性,也就是说常量并不一定是编 译时确定,运行时生成的常量也会存在这个常量池中。
虚拟机栈
:
1.
虚拟机栈也就是我们平常所称的栈内存
,
它为
java
方法服务,每个方法在执行的时候都会创建
一个栈帧,用于存储局部变量表、操 作数栈、动态链接和方法出口等信息。
2.
虚拟机栈是线程私有的,它的生命周期与线程相同。
3.
局部变量表里存储的是基本数据类型、
returnAddress
类型(指向一条字节码指令的地址)
和对象引用,这个对象引用有可能是指 向对象起始地址的一个指针,也有可能是代表对象的
句柄或者与对象相关联的 位置。局部变量所需的内存空间在编译器间确定
4.
操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来
访问,而是压栈和出栈的方式
5.
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持
方法调用过程中的动态连接
.
动态链接就是将常量池中的符号引用在运行期转化为直接引用。
本地方法栈:
本地方法栈和虚拟机栈类似,只不过本地方法栈为
Native
方法服务。
堆:
Java
堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里
创建,因此该区域经常发生垃圾回收操作 。
程序计数器:
内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指
令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区
域是唯一一个
java
虚拟机规范没有规定任何
OOM
情况的区域。
堆和栈的区别:
栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆是存
储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连
续,会有碎片。
1
、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储
Java
中的对象。无论是成员变量,局部变量,
还是类变量,它们指向的对象都存储在堆内存中。
2
、共享性不同
栈内存是线程私有的。
堆内存是所有线程共有的。
3
、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:
java.lang.StackOverFlowError
。
堆空间不足:
java.lang.OutOfMemoryError
。
4
、空间大小
栈的空间大小远远小于堆的。
JVM加载class的机制:
加载 -》 验证 -》 准备 -》 解析 -》 初始化 -》 使用 -》 卸载
类的加载是指把类的
.class
文件中的数据
读入到内存中,通常是创建一个字节数组读入
.class
文件,然后产生与所加载类对应的
Class
对象。加载
完成后,
Class
对象还不完整,所以此时的类还不可用。当类被加 载后就进入连接阶段,这一阶段包括
验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步
骤。最后
JVM
对类进行初始化,包括:
1)
如果类存在直接的父类并 且这个类还没有被初始化,那么就先
初始化父类;
2)
如果类中存在初始化语句,就依次执行这些初始化语句。 类的加载是由类加载器完成
的,类加载器包括:根加载器(
BootStrap
)、扩展加载器 (
Extension
)、系统加载器(
System
)和
用户自定义类加载器(
java.lang.ClassLoader
的子类)。从
Java2
(
JDK 1.2
)开始,类加载过程采取了
父亲委托机制(
PDM
)。
PDM
更好的保证了
Java
平台的安全性,在该机制中,
JVM
自带的
Bootstrap
是
根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器
无能为力时才由其子类加载器自行加载。
JVM
不会向
Java
程序提供对
Bootstrap
的引用。下面是关于几个
类加载器的说明:
Bootstrap
:一般用本地代码实现,负责加载
JVM
基础核心类库(
rt.jar
);
Extension
:从
java.ext.dirs
系统属性所指定的目录中加载类库,它的父加载器是
Bootstrap
;
System
:又叫应用类加载器,其父类是
Extension
。它是应用最广泛的类加载器。它从环境变量
classpath
或者系统属性
java.class.path
所指定的目录中记载类,是用户自定义加载器的默认父加
载器。
java对象的创建过程:
对象的创建过程一般是从new指令(我说的是JVM的层面)开始的(具体请看图1),JVM首先对符号引用进行解析,如果找不到对应的符号引用,那么这个类还没有被加载,因此JVM便会进行类加载过程(具体加载过程可参见我的另一篇博文)。符号引用解析完毕之后,JVM会为对象在堆中分配内存,HotSpot虚拟机实现的JAVA对象包括三个部分:对象头、实例字段和对齐填充字段(具体内容请看图2),其中要注意的是,实例字段包括自身定义的和从父类继承下来的(即使父类的实例字段被子类覆盖或者被private修饰,都照样为其分配内存)。相信很多人在刚接触面向对象语言时,总把继承看成简单的“复制”,这其实是完全错误的。JAVA中的继承仅仅是类之间的一种逻辑关系,唯有创建对象时的实例字段,可以简单的看成“复制”。
为对象分配完堆内存之后,JVM会将该内存(除了对象头区域)进行零值初始化,这也就解释了为什么JAVA的属性字段无需显示初始化就可以被使用,而方法的局部变量却必须要显示初始化后才可以访问。最后,JVM会调用对象的构造函数,当然,调用顺序会一直上溯到Object类。
至此,一个对象就被创建完毕,此时,一般会有一个引用指向这个对象。在JAVA中,存在两种数据类型,一种就是诸如int、double等基本类型,另一种就是引用类型,比如类、接口、内部类、枚举类、数组类型的引用等。引用的实现方式一般有两种,具体请看图3。此处说一句题外话,经常用人拿C++中的引用和JAVA的引用作对比,其实他们两个只是“名称”一样,本质并没什么关系,C++中的引用只是给现存变量起了一个别名(引用变量只是一个符号引用而已,编译器并不会给引用分配新的内存),而JAVA中的引用变量却是真真正正的变量,具有自己的内存空间,只是不同的引用变量可以“指向”同一个对象而已。因此,如果要拿C++和JAVA引用对象的方式相对比,C++中的指针倒和JAVA中的引用如出一辙,毕竟,JAVA中的引用其实就是对指针的封装。
类的生命周期:
类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,
加载,查找并加载类的二进制数据,在
Java
堆中也创建一个
java.lang.Class
类的对象
连接,连接又包含三块内容:验证、准备、初始化。
1
)验证,文件格式、元数据、字节码、符号
引用验证;
2
)准备,为类的静态变量分配内存,并将其初始化为默认值;
3
)解析,把类中的符
号引用转换为直接引用
初始化,为类的静态变量赋予正确的初始值
使用,
new
出对象程序中使用
卸载,执行垃圾回收
Java
的对象结构
Java
对象由三个部分组成:对象头、实例数据、对齐填充。(上边那个图2有详细介绍)
如何判断对象可以被回收?
判断对象是否存活一般有两种方式:
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加
1
,引用释放时计数减
1
,计数
为
0
时可以回收。此方法简单,无法解决对象相互循环引用的问题。
可达性分析(
Reachability Analysis
):从
GC Roots
开始向下搜索,搜索所走过的路径称为引用
链。当一个对象到
GC Roots
没有任何引用链相连时,则证明此对象是不可用的,不可达对象。