文章目录
- 环境配置篇
- 如何执行一个文件
- 配置JDK环境(简述)
- Java文件执行流程
- 编译
- 加载
- JVM环境准备
- BootStrapClassLoader
- sun.misc.laucher
- AppClassLoader
- 解释
- 执行
- 回收
- ClassLoader讲解
- 主要的三个ClassLoader
- 双亲委派模型
- loadClass方法讲解
- 自定义ClassLoader
- JVM内存管理
环境配置篇
如何执行一个文件
阶段一:手动+图形界面
在日常生活中,我们要运行一个exe文件,我们的做法是什么?双击这个文件就能让它运行起来,那我们把这个方法抽象出来就是:我们找到文件所在的位置,并在当前位置运行文件。在这个过程中,寻找文件位置的过程是我们手动完成的。例如我们要运行javac.exe文件,我们需要找到JDK\bin文件夹,然后找到javac.exe文件,双击它,这就执行了javac.exe文件
阶段二:手动+命令行
那么我们再通过命令行的方式执行javac.exe文件。打开命令窗口,一般情况下不会直接进入到JDK\bin文件夹中,所以我们要通过cd命令不断地进入到JDK\bin文件夹中,通过输入javac命令来代替双击文件,这样javac.exe文件就执行起来了
阶段三:自动+命令行
总是自己JDK\bin文件夹太麻烦了,而且这个文件夹位置就是固定的,我告诉计算机这个文件夹的位置,以后找到我要输入javac命令,计算机就帮我自行找到JDK\bin文件夹,然后执行javac文件行不行?当然是可以的了。下面就讲解如何配置JDK环境。
配置JDK环境(简述)
一般JDK环境配置都是这样子的
C:\Program Files\Java\jdk1.8.0_201\bin
JAVA_HOME:C:\Program Files\Java\jdk1.8.0_201
Path:%JAVA_HOME%\bin
至于为什么要把一个完整路径分成两部分呢?主要有如下两个原因
- 首先,当我们重新安装了JDK或者是JDK升级之后,JDK的安装路径都会发生变化。在这种情况下,如果我们配置了JAVA_HOME环境变量,我们就可以只修改JAVA_HOME当中JDK的安装路径,而Path和CLASSPATH中涉及到的JDK的安装路径因为用%JAVA_HOME%代替了,所以不需要做任何修改,这样就减少了工作量和出错的概率。
- 其次,某些集成开发环境(IDE)和Java Web服务器会以JAVA_HOME环境变量的值去寻找JDK的安装路径。所以,如果我们希望在实际开过程中减少遇到莫名其妙问题的概率,应该添加一个JAVA_HOME环境变量,并且在Path和CLASSPATH中用%JAVA_HOME%去代替JDK安装的路径。
然而我没有配置CLASSPATH环境变量,这里就总结一下它的作用,具体讲解点击这里
-
结论1:在没有配置CLASSPATH环境变量时,java命令在找class文件时是默认在当前目录下寻找的。
-
结论2:配置过CLASSPATH环境后,java命令是按照CLASSPATH变量中的路径来的寻找class文件的,这就是为什么CLASSPATH变量中配置没有当前目录时,即使当前目录中有class文件,java命令仍然不能正常运行的原因。
Java文件执行流程
这不是一个简简单单的Java文件执行流程介绍,里面包含了很多细节,但是这是原创文章,只是个人总结,所以难免会发生错误,大佬轻点喷,重谢!!!
编译
这一步没什么好说的,就是调用javac.exe,将.java文件编译成.class文件
加载
下面的讲解,一切都发生在java -jar xxx.jar
等java命令之后,即运行之后。
JVM环境准备
当你在调用java -jar xxx.jar
的时候,操作系统会在path下载你的java.exe程序,java.exe就通过下面一个过程来确定JVM的路径和相关的参数配置了。
首先查找jre路径,Java是通过GetApplicationHome api来获得当前的Java.exe绝对路径,c:\j2sdk1.4.2_09\bin\java.exe
,那么它会截取到绝对路径c:\j2sdk1.4.2_09\
,并作为jre路径
然后通过c:\j2sdk1.4.2_09\
找到jvm.cfg,根据jvm.cfg配置文件找到jvm.dll。Java通过LoadJavaVM来装入JVM.dll文件.装入工作很简单就是调用Windows API函数:
LoadLibrary装载JVM.dll动态连接库.然后把JVM.dll中的导出函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs挂接到InvocationFunctions变量的CreateJavaVM和GetDefaultJavaVMInitArgs函数指针变量上。JVM.dll的装载工作宣告完成。
这样就可以在Java中调用JVM的函数了.调用InvocationFunctions->CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。
BootStrapClassLoader
首先说明的是,BootStrapClassLoader是由C/C++编写的,它本身就是虚拟机的一部分,所以它并不是JAVA类,也就是说无法在Java代码中获取它的引用。
在JVM启动之后,就会唤醒的BootStrapClassLoader,BootStrapClassLoader就会把JDK\JRE\lib下的jar包全部加载进JVM,其中有一个rt.jar包中,有sun.misc.laucher类。这个类非常重要
sun.misc.laucher
看一下sun.misc.laucher部分源码
- 加载了ExtClassLoader
- 加载了AppClassLoader
public class Launcher {
private ClassLoader loader
public Launcher() {
ClassLoader extcl;
extcl = ExtClassLoader.getExtClassLoader();
loader = AppClassLoader.getAppClassLoader(extcl);
}
}
AppClassLoader
AppClassLoader源码如下,总的来说,就是根据java.class.path
来加载.class文件。这个java.class.path
就是我们在环境变量中配置的CLASSPATH变量的值。看吧,这里是不是就把这些知识串起来了。
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
}
将.class加载到JVM后, 在Java堆中生成一个代表这个类的java.lang.Class对象,作为这个对象的访问入口。
解释
解释阶段是在代码执行的时候来触发的,当我们尝试执行一个类的方法时,首先会通过这个类的对象作为入口,找到对应方法的字节码的信息,然后解析器会把字节码信息解释成系统能识别的指令码。
解释阶段会有两种方式把字节码信息解释成机器指令码,一个是字节码解释器、一个是即时编译器JIT,一般来说当我们运行某个代码的时候会默认使用字节码解释器进行指令解析,只有当某个方法成为热点方法后,即时编译器就会把热点方法的指令码保存起来,下次方法执行的时候就无需重复的进行解析,所以JIT是对解析过程中的一种优化手段。
执行
操作系统把解释器解析出来的指令码,调用系统的硬件执行最终的程序指令
回收
GC如何判断对象可以被回收
- 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收
- 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象
可达性分析算法
- 可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会:第一次是经过可达性分析发生没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法
- 当对象编程(GC Roots)不可达时,GC会判断该对象是否执行过finalize方法,若执行了则直接回收。否则,若对象未执行过finalized方法,将其放入F-Queue队列,由低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则对象复活。
- 每个对象只能触发一次finalize()方法
- 由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用
四种JVM的垃圾回收算法
标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
ClassLoader讲解
终于来到了心心念念的ClassLoader了
主要的三个ClassLoader
BootStrapClassLoader
:在规定的sun.mic.boot.class
路径加载类
ExtClassLoader
:在规定的java.ext.dirs
路径加载类
AppClassLoader
:在规定的java.class.path
路径加载类
双亲委派模型
类加载过程
AppClassLoader
查找类时,先看看缓存是否有,缓存有则从缓存中获取,否则委托给父加载器ExtClassLoader
ExtClassLoader
查找类时,先看看缓存是否有,缓存有则从缓存中获取,否则委托给父加载器BootStrapClassLoader
ExtClassLoader
查找类时,先看看缓存是否有,缓存有则从缓存中获取,否则sun.mic.boot.class
路径查找,查找成功则加载类,否则让子类BootStrapClassLoader
自己加载BootStrapClassLoader
在java.ext.dirs
路径查找,查找成功则加载类,否则让子类AppClassLoader
自己加载AppClassLoader
在java.class.path
路径查找,如果找到就加载类,否则就抛出异常。
类加载器ClassLoader规则
- 一个ClassLoader创建时直接指定一个ClassLoader为parent,如果没有指定parent,那么它的parent默认就是AppClassLoader
- 一个ClassLoader的父加载器为null,则JVM内置的加载器去替代,也就是BootStrapClassLoader
双亲委派模型的好处
- 避免重复加载
- 防止核心API库被随意篡改
loadClass方法讲解
在loadClass()方法里面,一般执行以下步骤
- 执行findLoaderedClass(String)去检测这个Class是不是已经加载过了
- 执行父加载器的loadClass()方法
- 如果向上委托父加载器没有加载成功,则通过findClass(String)查找
这样完全符合双亲委派模型
自定义ClassLoader
一般不会直接重写整个loadClass()方法,只会重写findClass()方法
1. 自定义一个Test.java文件,存放在D:\lib
package com.frank.test;
public class Test {
public void say(){
System.out.println("Say Hello");
}
}
2. 重写findClass()方法
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class DiskClassLoader extends ClassLoader {
private String mLibPath;
public DiskClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = getFileName(name);
File file = new File(mLibPath,fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
3. 测试
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClassLoaderTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建自定义classloader对象。
DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
try {
//加载class文件
Class c = diskLoader.loadClass("com.frank.test.Test");
if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("say",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
JVM内存管理
既然看到这里了,那就把内存管理也看完吧
- 对象内存管理介绍
- JVM为java程序提供并管理所需要的内存空间;
- JVM内存分为“堆”、“方法区”、“栈”、“本地方法栈”、“程序计数器”,其中堆和方法区是线程共有的;栈、本地方法栈和程序计数器是线程私有的。
- 堆
- 这部分空间用于存储使用new关键字所创建的对象。
- 访问对象需要依靠引用变量,当一个对象没有任何引用时,被视为废弃的对象,属于被回收的范围,该对象中所有成员变量也随之被回收。所以成员变量的生命周期为:从对象在堆中创建开始到对象从堆中被回收结束
- 栈
- 这部分空间用于存储程序运行时在方法中声明的所有局部变量。例如:main()方法中有如下代码
- 一个运行的Java程序从开始创建到结束会有多次方法的调用;JVM会为每一个方法的调用在栈中分配一个对应空间,这个空间称为该方法的栈帧;一个栈帧对应一个正在调用中的方法,栈帧中存储了该方法的参数、局部变量等数据,当某个方法调用完成后,其对应的栈帧将被清楚,局部变量失效。
- 方法区
- 存放类的所有信息(方法和变量)。java程序运行时,首先会通过类加载器载入类文件的字节码信息,经过解析后将其装入方法区。
- 当类的信息被加载到方法区时,除了类的类型信息外,同时类内的方法定义也被加载到方法区,类在实例化对象时,多个对象会拥有各自在堆中的空间,但所有实例对象是共用在方法区中一份方法定义的,同时也是同用一个方法区的Class。
- 本地方法栈
- 本地方法栈与虚拟机的作用相似,不同之处在于虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则为虚拟机使用到的Native方法。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。
- 会抛出stackOverflowError和OutOfMemoryError异常