标题1 问题
为什么要学习JVM?学习JVM是为了什么?
JVM屏蔽了不同操作系统之间的差异,这是Java语言能够Write Once,Run Anywhere的根本。
JDK:JRE:JVM三者之间的区别?
JDK=JRE+开发工集(例如:Javac编译工具等)
JRE=JVM+基础标准类库
Java程序运行的过程:.java->.class->加载到JVM
字节码文件的结构:参考字节码文件格式和虚拟机规范。
从.java转换到.class文件只不过是转换了一种形式而已。
另一种理解字节码文件的方式:javap反编译字节码文件。
JVM是跨语言的平台
字节码文件“装到”JVM的机制——类加载(的过程)
三个步骤:loading->linking->initializing
1.加载:通过类的全限定名来获得文件名,并查找导入磁盘的class文件
2.链接:
1)验证:保证字节码文件的格式正确(版本号、字节码、符号引用等)。
2)准备:为类的静态变量分配内存,并将其初始化为默认初始值。
3)解析:把类中的符号引用转化为直接引用。符号引用指的是由一组符号描述的目标,它可以是任何的字面量;直接引用是指直接指向目标的指针,或者说是真实的指针地址。
3.初始化:为类的静态变量赋值,然后执行类的初始化(static)语句。初始化的详细过程:
1)如果类还没有被加载和链接(即没有执行前两个过程),那就先进行加载和初始化;
2)如果类存在父类,并且父类还没有初始化,那就先初始化直接父类;
3)如果类中存在初始化语句,顺序执行初始化语句。
class初始化时机:
1.创建类的实例(四种方式);(哪四种方式:new一个对象;反射;序列化、反序列化;克隆)(说得再直白点,如果类的实例已经存在,那么相应的字节码文件一定也存在)
2.访问类中的某个静态变量,或者对静态变量进行赋值;
3.主动调用类的静态方法;
4.Class.forname("包类名");(完成了类的初始化)
5.完成子类的初始化,也完成对本类的初始化(接口例外);
6.该类是程序引导入口(main方法入口或者test入口)。
Demo实例:
//Demo.java
//F.java
public class F {
public static int FCount = 200;
static {
System.out.println(FCount);
}
}
//S.java
public class S extends F {
public static int SCount = 100;
static {
System.out.println(Scount);
}
}
//Test.java
public class Test {
public static void main(String[] args)
S.SCount = 1000; //问题:会先执行哪条打印语句
}
}
问题大难:先执行FCount的打印语句。原因:见初始化时机第5条。
类的加载的实现者:类加载器
JDK自带的Class Loader一共有三个,可以分成两大类:
1.Bootrap ClassLoader;
2.Extension ClassLoader;
3.Application ClassLoader
Bootrap ClassLoader介绍:
1.JVM自带的引导类加载器,由C/C++的语言实现,在Java中打印null;
2.加载Java的核心类库,$JAVA_HOME中jre/lib/rt.jar、resource.jar或Java程序运行指定的Xbootclasspath选项jar包;
3.指定加载java,javax,sun等开头的包类名。
如果自定义了一个类,这个类的包名为java.lang,那么new一个这个自定义类的对象就会报错,因为java开头的包类名不能自定义类!
Extension ClassLoader介绍:
1.Java语言编写的类加载器sun.misc.Launcher$ExtClassLoader(静态内部类)
2.指定Bootrap ClassLoader为Parent加载器-->getParent()可以获取Bootrap ClassLoader
3.负责架子啊java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext或-Djava.ext.dirs指定目录下的jar包(如果我们自定义的class需要交给Ext来加载可以放置到ext的目录下)
Application ClassLoader介绍:
1.Java语言编写的类加载器sun.misc.Launcher$APPClassLoader(静态内部类)
2.该加载器是Java程序默认的类加载器,Java应用的类都是该类加载器加载的
3.指定Extension ClassLoader为parent加载器-->getParent()可以获取Extension ClassLoader
4.负责加载环境变量classpath指定的目录,或者java.class.path指定的目录类库
双亲委派加载机制/模型(面试常考)
从源码分析可知,Extension ClassLoader和Application ClassLoader都是继承于ClassLoader这个抽象类。ClassLoader中的loadClass方法中实现了双亲委派:
loadClass源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
这段代码的逻辑是:
1.先调用方法findLoadedClass()来判断是否已经存在这个类;
2.若存在,则跳过不执行;否则,执行try语句块中的代码;
3.递归判断parent是否为空并调用parent的loadClass方法。由于刚开始调用时类加载器为Application ClassLoader,所以parent为Extension ClassLoader不是null,继续调用parent的loadClass方法,Extension ClassLoader的parent为Bootrap ClassLoader,这时parent为null(表示不可访问),就会执行try语句块中else部分语句findBootstrapClassOrNull方法,这个方法调用C/C++编写的native方法private native Class<?> findBootstrapClass(String name)来尝试加载这个类;接着也是一个递归过程。
4.如果Bootrap ClassLoader无法加载并且没有找到这个类就会交给子类Extension ClassLoader来尝试加载,如果它也无法加载并且没有找到对应的类,那么最终就会交给Application ClassLoader去加载,由Application ClassLoader实现类的加载过程。
双亲委派加载机制/模型的作用:
1.避免类的重复加载;
2.保护程序安全,放置核心的Java语言环境遭受破坏。
如何理解?例如:java.lang包下的类都是非常宝贵的,例如String类,如果我自己定义了一个也叫做String的类,那么如何保证核心类库下的String类能够被正常加载呢?当new一个String对象时,实际上是由Bootrap ClassLoader调用native的方法findBootstrapClass得到的一个final类,而不是自定义的类所对应的对象。
进一步,如果想要破坏这样一个逻辑,或者说想要new的对象是自定义的String类的对象,那么可以通过重写loadClass方法来实现;换句话说,这也是为什么Application ClassLoader、Extension ClassLoader以及Bootrap ClassLoader没有重写这个方法的原因,所以只要调用loadClass方法就一定会按照这个逻辑执行。
为什么要自定义类加载器?
1.适配数据源
2.环境隔离;
3.jar包共享;
4.防止源码泄露。
初始运行时数据区
在JVM中划分了几大区域来分别存储不同时期或者不同class文件产生的不同的数据,有些区域随着JVM的启动就创建了并且随着JVM的关闭而销毁;其他的数据区域则是跟线程相关,即随着线程的创建而创建,随着线程的死亡而销毁。主要包括(参考JVM结构第2.5小节-Runi-Time Data Areas):
1. The pc Register(程序计数器)
通俗的讲,就是指下一个命令的地址。它与线程同创建同销毁。
由于JVM支持多线程,每个线程都会有自己的程序计数器,线程执行方法的过程中都会记录这个程序计数器。例如,线程A在调用某个方法,执行到某个过程时由于某种原因失去CPU执行权,而线程B获得了CPU执行权,当线程B完成某个任务之后,线程A又获得了CPU执行权,那么JVM需要知道从哪里开始继续执行。简单而言,就是用来保存当前当前线程当中执行的位置。
2. JVM stacks(Java虚拟机栈)
指当前执行线程的独占空间。与线程同创建同销毁。
初学时,我们简单粗暴的将JVM分为栈和堆,其中的栈就是这个虚拟机栈。
它以栈的形式存在,即先进后出。
保存的是栈帧(frame),每个栈帧中又保存了一些数据结构,例如局部变量、动态链接、返回地址等;或线程的运行状态,或理解为调用的方法。
3. Heap(堆)
程序运行中最需要关注的内容。
是线程共享的。与虚拟机同创建同消亡。
存储类的实例即对象和数组。
分为老年代、新生代,新生代又分为存活区、伊甸区,思考这样分的原因?
4. Method Area(方法区)
是所有线程共享的区域。随着JVM启动而创建。
保存的是类已经加载的信息,例如常量池、属性和方法数据以及方法体和构造器中编译后的代码。这些数据基本不变,而经常发生变化的保存在堆中
如果方法区的内存无法满足申请的需求时,会报出OutOfMemoryError的错误。
JDK8以前叫做永久代(Perm Space),1.8叫做metaspace。
5. Run-Time Constant Pool(运行时常量池)
每个常量池由JVM中的方法区进行分配。
6. Native Method Stacks(本地方法栈)
native方法(C/C++编写的方法)的保存位置,不能用JVM栈保存
JVM执行引擎的组成
JVM本质上也是一个程序、一个进独立的进程,最终还是要交给操作系统去运行,这就涉及到执行引擎。
思考问题:Java是编译型语言还是解释型语言? Ans:解释+编译
解释器:把class字节码文件中的指令翻译成机器语言、机器能够马上执行的指令;
即时编译器:
垃圾回收器:对内存共享区域(例如堆、方法区)的自动处理。
总结
注意,图中把常量池合并到了方法区。