Java内存模型介绍&类的加载机制
-
JAVA技术体系:
jdk(Java Development Kit ):Java开发工具。是程序开发者用来编译,调试Java程序的工具包,JDK也是Java程序,需要jre才能运行。为了保持jdk的独立性和完整性,在jdk安装的过程中,jre也是安装的一部分。
jre:(Java Runtime Environment)Java运行环境。所有的Java程序都要在jre上才能运行。
jvm(Java Virtual Machine):Java虚拟机,是jre的一部分,是一个虚拟的机器,通过在真实的机器上仿真模拟各种计算机功能的实现。JVM有自己的一套硬件架构,处理器,堆栈,寄存器等,还有相应的指令系统。其特点是跨平台性。 -
JVM介绍
-
JVM的生命周期:
JVM负责运行JAVA程序,当启动JAVA程序时,一个jvm实例也就产生了,当程序关闭退出时,jvm也就随之消亡。 -
jvm运行的起点:
jvm实例通过调用某个初始类的main()来运行一个JAVA程序,main()必须是public static void 的,且参数为字符串数组,任何拥有main()的类,都可以作为jvm的起点。
- JVM的工作过程:
(1)类加载子系统(Class Loader System)
Java类的加载由类加载系统完成,它可以加载、连接、初始化类文件。
- 加载:使用类加载器来加载类。类的加载器负责查找并加载类的二进制文件,并在Java堆上创建一个Java.long.Class类的对象。
三种类加载器:
Bootstrap ClassLoader():负责加载 $ JAVA_HOME中jre/lib/rt.jar里所有的class,即加载Java基础类库中的class。
Extension ClassLoader():负责加载$ JAVA_HOME中jre/lib/*.jar(除rt.jar)或-Djava.ext.dirs指定目录下的jar包,即一些Java平台扩展功能的jar包。
Application ClassLoader():classpath目录下的class文件,即运行当前程序所引入的依赖或者当前程序源代码编译所产生的class。
加载过程(双亲委派模型):
当要加载一个类时,先从Application ClassLoader开始,但不自己尝试加载,而是请求委派给父类加载器Extension ClassLoader,Extension ClassLoader也不自己加载,而是委派给Bootstrap ClassLoader。Bootstrap ClassLoader先尝试加载,如果加载失败则Extension ClassLoader加载,如果Extension ClassLoader加载失败则Application ClassLoader加载,如果加载失败则抛出ClassNotFoundException异常。
双亲委派模型的好处:
安全性高:如果黑客自定义的类里面含有病毒,但这个类的class在classpath中,顶多破坏classpath目录下的内容,而不会破坏jvm原有的内容。
保证类的单一性:一个类只能被一个加载器加载一次,以后就存放在这个类加载器加载的目录下,不会出现两个类加载器加载的目录下都有该class文件。
可见性:子类加载器可以看到父类加载器加载得类。 - 连接
连接阶段包括验证、准备、解析三个部分。
验证:验证加载文件的文件格式、元数据、字节码、符号引用;
准备:为加载的类的静态变量分配内存,并将其初始化为默认值;
解析:把类中的符号引用转化为直接引用(所引用的数据实际在内存中的地址)。 - 初始化:为类的静态变量赋予正确的初始值。
- 使用和卸载:等上面类的加载过程(加载-连接-初始化)都完成,就可以使用该类,使用完就可以卸载。
总结:加载–>连接(验证–>准备–>解析)–>初始化–>使用–>卸载
(2)运行时数据区(Runtime Data Area)–>JMM(Java Memory Model)Java内存模型
包括堆、方法区、虚拟机栈、本地方法栈、程序计数器
- 方法区:存储类信息、静态变量、常量。方法区中有一块区域叫做运行时常量池,存放类的版本、字段、方法和接口等的描述信息。注意,这里只存放描述信息,方法存放在堆的对象中。线程共有。会抛出OutOfMemoryError。
- 堆:存储对象实例和数组。JVM管理的最大的内存区域。垃圾回收机制主要作用于堆。线程共有。在虚拟机启动时创建。会抛出OutOfMemoryError。
- 虚拟机栈:存储局部变量表(八大基本类型+对象的引用+returnAddress(本方法结束后下一部执行的地址))、栈帧数据、操作数栈、动态链接(一些插件的地址信息)。线程私有。生命周期与线程的生命周期同步。会抛出StackOverFlowError(虚拟机栈不允许动态扩容时,当前线程请求的栈深度大于最大深度时会抛出此error,可开启自动扩容机制解决)和OutOfMemoryError(虚拟机栈允许动态扩容,当前线程请求栈深度大于最大深度,无法扩容时会抛出此error,可优化代码来解决)。
- 本地方法栈:存储信息和虚拟机栈类似,只存储native修饰的方法的相关信息。线程私有。生命周期与线程的生命周期同步。也会抛出StackOverFlowError和OutOfMemoryError。
- 程序计数器:如果当前方法(正在执行的方法)是本地方法,程序计数器的值为undefined;如果不是本地方法,则指向当前执行的指令。JMM中唯一不会抛出OutOfMemoryError的内存区域。线程私有。程序计数器的生命周期与线程的生命周期同步。
(3)执行引擎:将运行时数据区的字节码交由执行引擎执行。
- 类什么时候加载(加载-连接-初始化)
- 创建对象实例,即new对象时,该类还未初始化,就要加载
- 反射时加载:通过class文件来反射创建该类的对象时
- 调用类的静态属性或者给静态属性赋值
- 调用类的静态方法
- 子类初始化时,会先初始化父类
- JVM启动时标记为启动类的类(含main()的类)会被加载
Java类的加载是动态的,并不是一次性将所有的类加载到jvm中,而是按需加载(懒加载),即需要用到那个类时就加载那个类。这样可以节省内存开销。
- 初始化的顺序:
通过代码实验得知:
class Test{
public Test(){
System.out.println("父类成员变量");
}
}
class TestStatic{
public TestStatic(){
System.out.println("父类静态变量");
}
}
class Test2{
public Test2(){
System.out.println("子类成员变量");
}
}
class Test2Static{
public Test2Static(){
System.out.println("子类静态变量");
}
}
class Father{
protected Test a=new Test();
protected static TestStatic b=new TestStatic();
static{
System.out.println("父类静态块");
}
{
System.out.println("父类实例块");
}
public Father(){
System.out.println("父类构造方法");
}
}
class Son extends Father{
private Test2 c=new Test2();
private static Test2Static d=new Test2Static();
static{
System.out.println("子类静态块");
}
{
System.out.println("子类实例块");
}
public Son(){
System.out.println("子类构造方法");
}
}
public class TestDemo {
public static void main(String[] args) {
Son son=new Son();
}
}
大体顺序就是上面的打印结果,值得注意的是:
- 将上面代码的静态块和静态变量顺序改变会改变其执行顺序,意味着静态变量和静态块的初始化顺序是其在代码中的顺序。
- 当给上面代码加入普通方法,但并未调用时,不会初始化。印证了类的加载是懒加载。