这一张图呢就是整个java程序运行过程。
- java文件:也就是我们平时用编辑器编写的那些java文件。
- java源码编译器:在windows系统中安装的JDK中bin文件夹里面javac.exe就是我们的编译器,是专门讲java文件编译成.class文件。
- class文件:字节码文件。
- 类加载器:其实就是执行类加载的过程。是将.class文件加载到JVM中。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类的方法区内的数据
- 方法区:(Method Area)也就是我们生活中的仓库。线程之间共享的区域。主要用来存储运行时常量池、静态变量、类信息、JIT编译后的代码等数据。
- 虚拟机栈:(VM Stack)也叫栈内存,也就是我们生活中的工作区域。虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈。
每个java方法在执行时,会创建一个“栈帧(stack frame)”,栈帧的结构分为“局部变量表、操作数栈、动态链接、方法出口”几个部分(具体的作用会在字节码执行引擎章节中讲到,这里只需要了解栈帧是一个方法执行时所需要数据的结构)
方法调用时,创建栈帧,并压入虚拟机栈;方法执行完毕,栈帧出栈并被销毁,如下图所示:
- 若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError(栈溢出错误)。
JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,典型如一个无结束条件的递归函数调用,代码见下:
/**
* java栈溢出StackOverFlowError
* JVM参数:-Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = -1;
//通过递归调用造成StackOverFlowError
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("Stack length:" + oom.stackLength);
e.printStackTrace();
}
}
}
- 不同于StackOverflowError,OutOfMemoryError指的是当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出的异常。
JVM未提供设置整个虚拟机栈占用内存的配置参数。虚拟机栈的最大内存大致上等于“JVM进程能占用的最大内存(依赖于具体操作系统) - 最大堆内存 - 最大方法区内存 - 程序计数器内存(可以忽略不计) - JVM进程本身消耗内存”。当虚拟机栈能够使用的最大内存被耗尽后,便会抛出OutOfMemoryError,可以通过不断开启新的线程来模拟这种异常,代码如下:
**
* java栈溢出OutOfMemoryError
* JVM参数:-Xss2m
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
//通过不断的创建新的线程使Stack内存耗尽
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(() -> dontStop());
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new _03_JavaVMStackOOM();
oom.stackLeakByThread();
}
}
- 本地方法栈:
- 堆:也就是我们生活中的居住区。
- 程序计数器:
- 执行引擎:
- 本地库接口:
- 本地方法库:
类加载的过程又可以分为五个部分
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。
加载
加载.class文件肯定要找啊,在哪儿,从哪儿加载?
加载class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取出.class文件
- 将Java源文件动态编译为.class文件
- 类的加载的最终产品是位于内存中的Class对象
- 类加载器不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时预先加载它,如果与先加载过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误
- 如果这个类一直没被程序主动使用,那类加载器就不会报告错误
链接
- 将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去
- 将类与类之间的关系确定好,并且对字节码相关的处理、验证、校验等操作通过加载连接完成
校验
此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。
文件格式验证:基于字节流验证。
元数据验证:基于方法区的存储结构验证。
字节码验证:基于方法区的存储结构验证。
符号引用验证:基于方法区的存储结构验证。
准备
准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如以下示例,在准备阶段将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0.
public class Sample{
private static int a = 1;
public static long b;
static {
b = 2;
}
}
解析
把类型中的符号引用转换为直接引用。
符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。个人理解为:在编译的时候一个每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,多以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
直接引用 :直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在
主要有以下四种:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
- 在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:
- 在静态变量的声明处进行初始化
- 在静态代码块中进行初始化
- Java虚拟机按照初始化语句的类文件的先后顺序依次执行它们
例如以下代码,静态变量a和b都被显式初始化,而静态变量c没有被显式初始化,它将保持默认值0,按照先后顺序a最终将取值为4
public class Sample{
private static int a = 1;
public static long b;
public static long c;
static {
b = 2;
}
static {
a = 4;
}
}
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们
-
主动使用(七种)
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName("com.test.Test"))
- 初始化一个类的子类
- Java虚拟机启动时被表明为启动类的类(Java Test)
7.JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七中情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化
使用与卸载
pass
类加载器的分类
- Bootstrap ClassLoader
- 最顶层的加载类,主要加载核心类库,也就是我们环境变量下面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- 在启动JVM的时候可以通过-Xbootclasspath参数来指定Bootstrap ClassLoader的加载目录
- Extension ClassLoader
- 扩展类加载器,主要加载%JRE_HOME%\lib\ext目录下的jar包和class文件。
- Application ClassLoader
- 加载当前应用的classpath的所有类。
- User ClassLoader 自定义类加载器
- 第一种方式:遵守双亲委派模型:继承ClassLoader,重写findClass()方法。(通常的做法)
- 第二种方式:破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。
public class MyClassLoader extends ClassLoader {
//指定路径
private String path ;
public MyClassLoader(String classPath){
path=classPath;
}
/**
* 重写findClass方法
* @param name 是我们这个类的全路径
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class log = null;
// 获取该class文件字节码数组
byte[] classData = getData();
if (classData != null) {
// 将class的字节码数组转换成Class类的实例
log = defineClass(name, classData, 0, classData.length);
}
return log;
}
/**
* 将class文件转化为字节码数组
* @return
*/
private byte[] getData() {
File file = new File(path);
if (file.exists()){
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
}else{
return null;
}
}
}
接下来我们编写一个java文件然后用编译器编译成class文件
public class Log {
public static void main(String[] args) {
System.out.println("load Log class successfully");
}
}
接下来就是测试我们的classLoader是否起作用
public class ClassLoaderMain {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
//这个类class的路径
String classPath = "D:\\Log.class";
MyClassLoader myClassLoader = new MyClassLoader(classPath);
//类的全称
String packageNamePath = "com.example.demo.Log";
//加载Log这个class文件
Class<?> Log = myClassLoader.loadClass(packageNamePath);
System.out.println("类加载器是:" + Log.getClassLoader());
//利用反射获取main方法
Method method = Log.getDeclaredMethod("main", String[].class);
Object object = Log.newInstance();
String[] arg = {"ad"};
method.invoke(object, (Object) arg);
}
}
类加载的方式
-
通过命令行启动应用时由JVM初始化加载含有main()方法的主类。
-
通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。
-
通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。
类加载的顺序(双亲委派原则)
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。这个理解起来就简单了,比如说,另外一个人给小费,自己不会先去直接拿来塞自己钱包,我们先把钱给领导,领导再给领导,一直到公司老板,老板不想要了,再一级一级往下分。老板要是要这个钱,下面的领导和自己就一分钱没有了。(例子不好,理解就好)
采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:
可以避免重复加载,父类已经加载了,子类就不需要再次加载
更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。
参考:https://www.jianshu.com/p/3556a6cca7e5
参考:https://www.jianshu.com/p/7b25fb3e810d
网友评论