Java内存模型
JVM的组成
- 类加载器(classloader)
- 执行引擎(execution engine)
- 运行时数据区域(runtime data area)
对于Java程序员来说,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个new 操作去写对应的delete/free操作,不容易出现内存泄漏和内存溢出问题。
正是因为Java程序员把内存控制权利交给Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
类加载器
顾名思义,类加载器用来加载Java类到Java虚拟机中, Java源文件(.java) 经过Java编译器编译之后就被转换成 字节码文件(.class) ,类加载器就负责读取Java字节代码,并转换成一个 类的实例(java.lang.Class) ,每个这样的实例用来表示一个Java类,这个类由它的类加载器和这个类本身一同确立在Java虚拟机中的唯一性,通过此实例的newInstance()方法就可以创建出该类的一个对象。
类加载器的结构
Java中的类加载器大致可以分为两类,一类是系统提供的,另外一类是由开发人员编写的
- 根类加载器:也叫引导类加载器、启动类加载器。负责将存放在 ++<JAVA_HOME>\lib++ 目录中的,或者被 ++-Xbootclasspath++ 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。
- 扩展类加载器:负责加载 ++<JAVA_HOME>\lib\ext++ 目录下的,或者被 ++java.ext.dirs++ 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用类加载器:由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫 ++系统类加载器++ 。它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器。一般情况下这个就是程序中默认的类加载器。
这种类加载的层次关系,称为类加载器的双亲委派模型。
双亲委派模型
双亲委派模型是指当我们调用类加载器进行类加载时,该类加载器首先请求它的父类加载器进行加载,依次递归。
如果所有的父类加载器都加载失败,则当前类加载器自己进行加载操作。
以下用ClassLoader类的源码进行逻辑分析:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//进行类加载操作时首先要加锁,避免并发加载
synchronized (getClassLoadingLock(name)) {
//首先判断指定类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果当前类没有被加载且父类加载器不为null,则请求父类加载器进行加载操作
c = parent.loadClass(name, false);
} else {
//如果当前类没有被加载且父类加载器为null,则请求根类加载器进行加载操作
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
//如果父类加载器加载失败,则由当前类加载器进行加载,
c = findClass(name);
//进行一些统计操作
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//初始化该类
if (resolve) {
resolveClass(c);
}
return c;
}
}
这里有几个细节需要说明
- ClassLoader类是一个抽象类,但却没有包含任何抽象方法;
- 如果要实现自己的类加载器且不破坏双亲委派模型,只需要继承ClassLoader类并重写findClass方法;
- 如果要实现自己的类加载器且破坏双亲委派模型,则需要继承ClassLoader并重写findClass和loadClass方法;
为什么使用双亲委派模型
确保无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的类都是同一个类,保障了Java核心类库的安全问题。
相反,如果没有双亲委派模型,由各个类加载器自己加载的话,如果开发者自己写了一个称为java.lang.Object的类并放在classpath下那么系统将会出现很多个不同的Object类,Java类型体系中最基础的行为也就无法保证。
执行引擎
类加载器负责装载编译后的字节码,并加载到运行时数据区,执行引擎则会执行这些字节码。
执行引擎以指令为单位读取Java字节码,它像一个CPU一样,一条一条地执行机器指令,每个字节码指令都由1字节的操作码和附加的操作数来执行任务,完成后就继续执行下一条操作码。
虚拟机实现中,可能会有两种的执行方式:
- 解释执行(通过解释器执行)
- 编译执行(通过即时编译器产生本地代码)
有些虚拟机值采用一种执行方式,但是有点采用了两种,甚至有可能包含几个不同级别的编译器执行引擎。
所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件、处理过程是等效字节码解析过程,输出的是执行结果。
这里的坑太深,建议 ->详情请看这里
运行时数据区
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
线程共享的: 堆、方法区
线程私有的: (虚拟机)栈、本地方法栈、程序计数器
堆
- 堆可以被多个线程共享;
- 堆用来存放对象和数组(特殊的对象),几乎所有的对象实例都在这里分配,可以说堆只存放对象;
- 堆随着虚拟机启动而创建;
- 堆是垃圾回收机制管理的主要区域,也称GC堆;
- 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可
- 堆可以是可动态扩展内存的,如果堆内存剩余的内存不足以满足对象的创建,也不能扩展时,就会抛出OOM错误
方法区
- 方法区可以被多个线程共享;
- 方法区用来存放
- 每个类的信息(包括类名、方法信息、字段信息);
- 静态变量;
- 常量;
- 编译器编译后的代码;
- 可以选择不实现垃圾收集;
- 方法区还包含常量池,用来存储编译期间生成的字面量和符号引用;
- 方法区中还有一个重要的部分是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载进JVM后,对应的运行时常量池就被创建出来;
栈
- 属于线程私有,生命周期与线程相同;
- 每个方法执行的同时都会创建一个栈帧,里面包含:
- 局部变量表
- 操作数栈
- 动态链接
- 方法出口
- 等等
- 方法执行时入栈,执行完出栈,先进后出;
- 在栈中可能出现两种错误:
- StackOverflowError:出现在内存设置为固定值的时候,需要的内存超过设定的固定值时;
- OutOfMemoryError:出现在内存设置为动态增长的时候,申请内存大小超过可用内存时;
- 在方法中定义的基本类型变量和对象引用变量都会在栈内存中分配,当该变量退出其作用域后,Java会自动释放所占用的内存
本地方法栈
- 每个线程独有的;
- 与虚拟机栈类似,但是虚拟机栈服务的是Java方法,本地方法栈是为JVM所调用到的Native方法服务;
- 并不是由Java语言实现的栈;
程序计数器
- 用来记录当前线程正在执行的指令,线程私有的;
- 字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 如果当前正在执行的方法时本地方法,那么计数器的值为undefined;
- 这个区域是唯一一个不会抛出OOM错误的数据区