参考教程:B站狂神说Java
JAVA程序运行过程
一个java项目从完成、运行到结束一共经过一下几步
-
.java文件通过javac编译为.class文件
-
.class文件通过类加载器加载为Class对象并放在方法区中
(一个class文件即一个类只有一个对应的Class对象,这个Class对象包含了关于这个类的描述信息,相当于该类的模板)
-
开启主线程(即运行main方法),并将main方法压进java栈中
-
执行java栈顶的方法(栈帧)
- 调用了其他方法,将该方法压到栈顶,执行栈顶的方法
- new了一个对象,在堆中的开辟一片空间存放该对象,并在栈帧中存放该对象在堆中的地址进行引用
- 方法结束,将栈帧弹出java栈
-
程序运行结束
当然,这只是一个简单的过程,以便于简单理解JVM的内存结构,下面将详细对每一个区域进行分析
类加载器
类加载器负责将编译好的class文件加载为Class对象,每个不同的类只有唯一一个Class对象,该对象包含了该类的所有信息,如类中所包含的方法、所实现的接口、该类的类加载器等,相当于一个模板
类加载器一共分为3个,分别为:根类加载器、扩展类加载器和应用程序类加载器
根加载器
顾名思义,是最底层、最根本的类加载器,用于加载rt.jar包下的类,该包下为java中最基本的类,如:java.lang.String、java.util.List等
该加载器用C/C++编写的在打印一下代码时结果为null
System.out.println(String.class.getClassLoader());
//输出
//null
扩展类加载器
该加载器为根加载器的下一级,用于加载jre目录下/lib/ext中所存放的jar包中的类
一下为其中一个jar包下的类,代码输出结果如下
System.out.println(AWTEventMonitor.class.getClassLoader());
System.out.println(AWTEventMonitor.class.getClassLoader().getParent());
//输出
//sun.misc.Launcher$ExtClassLoader@6d6f6e28
//null
应用程序类加载器
该类加载器为扩展类加载器的下一级,是3个类加载器中最下级的一个,用于加载其他所有的自定义类
System.out.println(Test.class.getClassLoader());
System.out.println(Test.class.getClassLoader().getParent());
System.out.println(Test.class.getClassLoader().getParent().getParent());
//输出
//sun.misc.Launcher$AppClassLoader@18b4aac2
//sun.misc.Launcher$ExtClassLoader@4554617c
//null
方法区
方法区中存放了类中所定义的静态变量、常量、Class对象以及运行时常量池
方法区只是一种规范,而他的实现为永久区/元空间
堆
堆中存放的为每个线程在执行中所产生的对象实例,该对象的方法、变量等都会存放在堆中
堆中包含新生区、养老区、永久区,而新生区可细分为新生区、存活区form、存活区to
- 新生区:对象被新建出来时放在新生区
- 存活区:在进行一次垃圾回收(GC)后,在新生区中没有被回收的对象,即存活的对象会移到存活区
- 养老区:在经历15次(JVM默认值)GC后,依然没有被回收的对象会从存活区移到养老区
- 永久区:方法区的一种实现,存放方法区所要存放的东西
- jdk1.6及以前:永久区存在,数据存放在堆中,内存与新生区、养老区相连,常量池放在永久区中
- jdk1.7:永久区逐渐退化,常量池存放在堆中
- jdk1.8及以后:元空间代替永久区,元空间不再放在堆中,而是独立的内存空间,此时常量池在元空间
垃圾回收算法(GC)
判断算法:
-
引用计数法
对每个对象添加一个引用计数的属性,记录该对象正在被引用的计数,当计数为0,即该对象没有被引用时进行回收
-
可到达算法
从一系列"GC Roots"对象作为起点,搜索该对象的所有引用链,如果一个对象在每个GC Roots对象的所有引用链中都没有达到,即判断为垃圾进行回收
回收算法:
-
复制算法
把经过判断算法后判断所存活下来的对象进行复制,将这些对象复制到一个新的空间进行保存,并把原来的对象与垃圾对象一起回收,释放内存空间
优点:时间复杂度低,没有内存碎片
缺点:浪费内存,需要有一片内存为空的内存以便存放复制的对象
-
标记清除算法
对存活的对象进行标记,然后回收清除没有被标记的对象
优点:时间复杂度低
缺点:容易产生内存碎片
-
标记压缩算法
对存活的对象进行标记,然后把这些对象移动到相邻的一片内存中
优点:没有内存碎片
缺点:时间复杂度高
总体垃圾回收:
-
GC(轻GC):在新生区内存满了的时候执行
由于新生区中的对象存活率低,进行GC后可用内存空间大,使用复制算法,将新生区和幸存区form中存活的对象转移到幸存区to,然后将空了的幸存区form改为幸存区to,相对的幸存区to改为幸存区form,保证幸存区to的内存一直为空,以便执行复制算法
-
Full GC(重GC):在新生区及存活区form内存都满了的时候执行
Full GC会在GC的基础上对养老区的对象进行GC;如果某个幸存区中的对象在多次GC中(默认15次)依然存活下来,将会把该对象转移到养老区
由于养老区中的对象存活率高,进行GC后可用内存空间小,且Full GC使用执行频率较低,使用标记清除算法和标记压缩算法,先将未标记的对象进行回收清除,然后将存活的对象移动到相邻的一篇内存中,保证没有内存碎片
内存溢出(OOM):
当新生区以及养老区的内存都满了,且Full GC已经无法再清理垃圾时,会爆出OOM内存溢出错误,此时程序将会直接崩溃
线程:
一个线程包含3个区域:java栈、本地方法栈以及程序计数器,这3个区域是每个线程所私有的,而方法区以及堆是被每个线程所共享的
java栈
在java栈中存放了程序所需执行的方法,每个方法为一个栈帧,当程序调用了一个方法时,这个方法的栈帧会入栈,当这个方法执行完毕后,该栈帧会出栈,当栈中没有栈帧时线程运行结束死亡
栈帧包括:
-
本方法的索引:本方法在栈中的地址
-
局部变量:方法中定义的临时变量,为基本数据类型
-
对象引用:存放对象实例在堆中的地址以进行引用
在进行new操作时,会先在方法区中找到对应的Class对象,并新建一个变量名,然后会通过该对象创建一个实例,该实例放在堆中,而栈帧中存放该实例在堆中的地址
-
。。。
栈溢出:
当不断调用方法导致栈满时会爆出栈溢出错误,如:递归调用方法而不能退出递归时
本地方法栈
作用与java栈相同,不过该栈中执行的方法为本地方法,而非java方法
java通过native关键字调用本地方法接口(JNI),以调用本地方法库执行其他语言所写的代码,如:Thread类中的start0方法
public synchronized void start() {
if (this.threadStatus != 0) {
throw new IllegalThreadStateException();
} else {
this.group.add(this);
boolean var1 = false;
try {
this.start0();
var1 = true;
} finally {
try {
if (!var1) {
this.group.threadStartFailed(this);
}
} catch (Throwable var8) {
}
}
}
}
private native void start0();
线程启动调用start方法,而start方法的本质是调用使用native关键字修饰的start0方法,该方法调用C/C++的代码对硬件进行操作,从而开启一个新的线程
java中还有很多类似的本地方法,如Object中的hashCode方法
程序计数器
为保证线程在cpu中进行切换后,执行进度能与上次在cpu执行后的进度保持一致,使用程序计数器记录上次所执行命令的位置