1.什么是JVM
是一个通过在实际的计算机上模拟各种计算机功能的虚拟计算机,也是JAVA语言“Write Once ,Run Anywhere~”的核心技术。
1.1 JDK、JRE和JVM 三者是什么关系?
官方图
很显然,JDK是JRE的超集,除了包含JRE以外,还包含一些编译调试程序和应用的工具。(包括java、javac、JAVA API)
JRE是JVM的超集,包含JVM,并且还包含一些JAVA核心类库、运行程序和应用的其他组件。(JAVA SE API子集 + JVM)
JVM主要的工作是解释字节码指令,并映射到本地的CPU指令集和OS的系统调用,使之与系统无关,实现跨平台。(跨平台的核心)
1.2 JVM架构图
1.3 JVM 内存模型
程序计数器
线程私有
是一块较小的内存空间。
当前线程所执行的字节码的行号指数器(取指,执行)。
它指定下一条待执行的指令是哪个。
Java虚拟机栈
线程私有(生命周期与栈同步,线程结束,栈内存也就释放了,所以不存在垃圾回收的问题)
用来描述Java方法执行的线程内存模型,也就是通常所讲的栈。
每个方法被执行,就创建一个栈帧(Stack Frame,是栈里的元素),用来存储:
- 局部变量表
- 基本数据类型(boolean、byte、char、short、int、long、float、double)
- 对象引用(只是引用,类似于指针,但不是对象本身)
- 操作数栈
- 动态连接
- 方法出口(returnAddress,方法会压栈出栈,肯定要记录出口)
- 等。。。
Java 虚拟机栈会出现两种错误:StackOverFlowError
和 OutOfMemoryError
。
StackOverFlowError
: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。OutOfMemoryError
: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常。
本地方法栈
线程私有
类似于上面的Java虚拟机栈,但是是为了虚拟机使用到的本地(Native)方法服务
(虚拟机栈为虚拟机执行Java方法服务,也就是字节码)
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError
和 OutOfMemoryError
两种错误。
native关键字
说明Java 的作用范围达不到了,回去调用底层C语言的库了。
进入本地方法栈,调用本地方法接口 JNI。
JNI作用:扩展Java 的使用范围,融合不同的编程语言为Java所用。
在内存中开辟一块空间,即本地方法栈,在这里登记native方法,在最终执行的时候,通过JNI加载本地方法库中的方法
Java堆
也称GC堆,虚拟机所管理的最大的一块线程共享的内存区域
目的就一个:存放实例对象
。
堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:
OutOfMemoryError: GC Overhead Limit Exceeded
: 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space
错误。(和本机物理内存无关,和你配置的内存大小有关!)- …
方法区
线程共享
用于存储已被虚拟机加载的:
- 类型信息(构造方法、接口定义) Class
- 静态变量 static
- 常量 final
- 运行时常量池
- 即时编译器编译后的代码缓存等数据
别名:非堆(Non-Heap)
里面存的很少被GC回收,之前常被人称为永久代(JDK8后成为元空间)(并不等同)。
方法区是《Java虚拟机规范》里规定的东西,是接口,并没有规定具体怎么实现
而永久代是HotSpot的具体实现,是实现类。
元空间
在JDK8之后,元空间取代的是永久代,而不是方法区
使用的是本地内存,不是JVM的内存(脱缰的野马)
**好处:**不再受JVM的内存的限制,受本机可用内存的限制,可以能溢出,但几率小多了。
**注意:**在JDK1.8中,使用元空间代替永久代来实现方法区,但是方法区并没有改变,所谓"Your father will always be your father",变动的只是方法区中内容的物理存放位置。
正如上面所说,类型信息(元数据信息)等其他信息被移动到了元空间中;
但是运行时常量池和字符串常量池被移动到了堆中。
但是不论它们物理上如何存放,逻辑上还是属于方法区的。
- JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
- JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。
- JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
- JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
- JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。
- JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)
2. Java对象揭秘
如何创建对象
new
,是没错,细节呢?
-
类加载检查
检查类是否被加载、解析、初始化过,没有就加载类
-
分配内存
两种分配方式,根据Java堆是否规整决定(整整齐齐,还是乱七八糟)
-
指针碰撞
整整齐齐,挪动空闲与非空闲内存中间的指针就行了。
-
空闲列表
乱七八糟,那只能维护一个列表,来标明哪些空闲,哪些被占用。
-
-
初始化内存空间(所有字段置为0)
-
设置对象头
- 是哪个类的实例?如何找到类的元数据信息?对象的哈希码?对象的GC分代年龄等信息(对象头部信息)
-
执行构造函数
new指令后,会接着执行<init>()方法
对象的内存布局
包含三部分:
-
对象头
-
对象自身运行时的数据
哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
-
类型指针
确定该对象 是哪个类的实例
-
数组长度(如果是个数组对象)
-
-
实例数据
对象存储的真正有效信息,父类变量优先于子类
-
对齐填充
对象起始地址默认是8个字节的整数倍,所以对象本身的大小也要是8字节的整数倍。
对象头部是8字节的整数倍,若实例数据没有对其,就通过对其对齐来补齐它。
对象的定位访问
通过栈上的reference来操作堆上的具体对象。
主流的方式:
-
使用句柄(间接访问实例数据)
好处:稳定(reference始终不变)
reference只存稳定的句柄地址,在对象移动时,只需要修改实例数据指针,reference不需要被修改
-
直接访问(直接访问实例数据)
好处:速度快(定位|查找)
省去了一次指针定位的时间开销。
3. 内存泄漏
3.1 什么是内存泄漏
不再会被使用的对象的内存却因为某种原因,不能被回收,就是内存泄露。
- 首先,这些对象是可达的,即在可达性分析的过程中,有通路的
- 其次,这些对象是无用的,程序以后再也不会使用这些对象了
所以就是,没用,还没法被回收。
3.2 例子
(1) 新生代的引用挂在老年代上
比如说单例模式。
新生代本来可能很快就要被回收的,但是依靠着老年代,一直存活了下来,但是它本身就没啥用,就被搁置了。
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代码
}
}
如何解决:
- 缩小作用域
- 养成对失效对象的引用赋值为null 的好习惯。
(2) 容器使用时的内存泄漏
一个对象被放在了一个容器里,这的对象本身以后可能不会再被使用到了,但是容器在一直被使用,就可能导致内存泄漏。
void method(){
Vector vector = new Vector();
for (int i = 1; i<100; i++){
Object object = new Object();
vector.add(object);
object = null;
}
//...对vector的操作
//...与vector无关的其他操作
}
如何解决:
如果不用容器了,就把容器的引用赋值为null
void method(){
Vector vector = new Vector();
for (int i = 1; i<100; i++){
Object object = new Object();
vector.add(object);
object = null;
}
//...对v的操作
vector = null;
//...与v无关的其他操作
}
(3) 各种提供close()方法的对象
我们用完之后要显示调用close()方法,才能释放资源。
但是万一操作的过程中,发生了异常,没有正常的close()释放资源,就会导致内存的泄漏。
读写流,Hibernate使用时创建的session
Session session=sessionFactory.openSession();
session.close();
如何解决
用try catch finally包起来
try{
session=sessionFactory.openSession();
//...其他操作
}finally{
session.close();
}