文章目录
一:Java的跨平台性
Java程序是跨平台性的,它是通过JVM来实现的
JVM是Java Virtual Machine(Java虚拟机)
的缩写,是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的字节码(即.class文件),就可以在多种平台上不加修改地运行。
注意:
- Java程序可以依靠 JVM 进行跨平台,但是不是 JVM 跨平台。(原因是不同操作系统的 JVM 版本不同)
- java源文件 通过编译生成 .class字节码文件,再通过 JVM 将字节码文件翻译成特定平台下的机器码进行运行,而Java程序并没有发生任何改变。
- JVM 是通过C\C++开发的,是Java程序与不同操作系统之间的中间层。
二:JVM概念
JVM是Java的核心和基础,在Java编译器和OS平台之间的虚拟的处理器,是一种利用软件方法来实现的抽象的计算机的下层的操作系统和硬件平台,在其上面执行java的字节码程序。
JVM有自己的完善的硬件架构,如处理器,堆栈,寄存器,还有相应的指令系统,使用JVM可以实现开发和操作系统无关,实现跨平台性。
三:JDK/JVM/JRE的关系
JRE(Java Runtime Environment ,Java的运行环境)
,也就是Java平台,所有的Java程序都是要在JRE下才能运行。JDK(Java Development Kit,Java开发工具包)
,程序开发过程中用来编译、调试Java程序用的Java开发工具包,JDK的工具包也是Java程序,也是需要在JRE上运行,为了保证JDK的独立性和完整性,在JDK安装过程中,JRE也是安装的一部分 ,所在在JDK安装的过程中有一个jre的目录,主要存放的是JRE的文件。JVM(Java Virtual Machine,Java虚拟机)
,是JRE的一部分,是一个虚拟出来的计算机,通过实际的计算机来仿真模拟各种计算机的功能来实现的。
四:JVM生命周期
启动和消亡:
- JVM负责运行Java程序,启动一个Java程序时,一个虚拟机也就诞生了,当程序关闭时,JVM实例也就随之消亡。
JVM运行的起点:
- JVM实例是通过调用某个初始类的main()方法来运行一个java程序,而这个main()方法必须是共有的(public) 、静态的(static)、返回值为void,并且能够接受一个字符串数组作为参数,
(psvm)
任何拥有这样main方法作为java成的运行的起点。
JVM两种线程:
- 守护线程和非守护线程
只要有Java程序在继续运行,即非守护线程在运行,守护线程依赖于非守护线程,当程序运行结束退出之后,守护线程也会退出,继而JVM虚拟机实例也会自动退出。
五:JVM工作过程
JVM的运行过程涉及到三个子系统:
- 类加载子系统(Class Loader SubSystem)
- 运行时数据区(Runtime Data Areas)
- 执行引擎(Execution Engine)
1、类加载子系统
作用是将字节码文件加载到JVM中,在类第一次被使用时,是需要初始化类文件
-
装载:功能就是来加载类,使用到了三个类加载器,分别是
Bootstrap ClassLoader
、Extension ClassLoader
和Application ClassLoader
-
链接:
- (1)验证:验证 文件格式、元数据、字节码、符号引用,如果验证失败,将得到验证错误
- (2)准备:对于所有的静态变量进行分配内存,并给定默认值
- (3)解析: 将所有的符号内存引用替换为方法区的原始引用
-
初始化:静态变量将被赋予原始值,静态代码块将被执行
2、运行时数据区域
- 方法区:类级别数据、静态变量的应用,线程共享
- 堆:对象及其 实例变量和数据的存储位置,线程共享
- 虚拟机栈:程序运行过程中会使用栈区,线程私有
- 本地方法栈:保存本地方法的信息,调用JNI,线程私有
- 程序计数器:线程私有
3、执行引擎
将分配给运行时数据区域的字节码将有执行引擎来执行,执行引擎读取字节码并逐个进行执行
六:类加载器
1、类加载时机
- new对象,静态属性调用或赋值,静态方法调用
- People.class 获取Class对象
- 加载子类时需要提前加载父类
- 启动JVM,main()函数所在的类需要加载
注:Java类的加载是动态的,它并不会一次性将所有的类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到JVM中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
不会进行初始化的情况:
- 1.在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了。
- 2.在编译时能确定下来的静态变量(编译常量),不会对类进行初始化。比如
final
修饰的静态变量。
2、类加载的过程
关于类加载的详细过程,前面已经做过介绍,这里就不再进行过多介绍了。
详情请点击:类加载的详细过程
-
类在同一个JVM实例中只被加载一次,而类的使用可以多次使用。
-
创建型的设计模式(单例模式):在系统中(JVM实例)只存在一个对象实例
//单例模式 - 懒汉模式 (慢加载单例模式)
public class Singleton {
private static People people;//方法区
private Singleton(){
}
public static People getInstance(){
if(people == null){
people = new People();
}
return people;
}
}
//饿汉单例模式 快加载
public class Singleton {
private static People people = new People();//方法区
private Singleton(){
}
public static People getInstance(){
return people;
}
}
如何保证当前的类只被实例化一次,当前的这个类是保证只创建一个实例的?
当前的单例实现上将当前要实例的对象people
设置为私有的,通过一个public
方法getInstance
来获取当前对象的实例,而且当前的s对象和方法getInstance
都是定义为静态的
当第一次调用Singleton.getInstance()
方法时,该类在当前JVM中不存在,尝试来加载当前类Singleton
时候,就进行类的加载过程,在类的加载过程中,在准备阶段完成静态变量people
的分配内存并给定默认值null,当在初始化阶段,完成静态代码块的执行,即完成了people
的初始化,在整个加载过程中就完成了当前对象people
的实例化,当调用Singleton.getInstance()
方法返回时,这个阶段是在使用阶段已经完成了类的加载初始化,不管当前的方法多次调用,当前的类的Singleton
的初始化只进行一次,这个是类加载机制来保证。(JVM保证同一个类被加载一次)
3、双亲委派模型
关于双亲委派模型,前面已经做过介绍,这里就不再进行介绍了。
详情请点击:双亲委派模型
双亲委派模型的优点:
- 安全性,避免用户自己编写的类动态替换Java的一些核心类
如果不采用双亲委派模型的加载方式进行类的加载工作,那我们就可以随时使用自定义的类来动态替代Java核心API中定义的类。例如:如果黑客将“病毒代码”植入到自定义的String类当中,随后类加载器将自定义的String类加载到JVM上,那么此时就会对JVM产生意想不到“病毒攻击”。而双亲委派的这种加载方式就可以避免这种情况,因为String类已经在启动时就被引导类加载器进行了加载。
- 避免类的重复加载
因为JVM判定两个类是否是同一个类,不仅仅根据类名是否相同进行判定,还需要判断加载该类的类加载器是否是同一个类加载器,相同的class文件被不同的类加载器加载得到的结果就是两个不同的类。
七:Java内存模型
JVM内存划分:
- 方法区(线程共享):常量、静态变量、JIT(即时编译器)编译后的代码也在方法区存放。
- 堆(线程共享):垃圾回收的主要场地。
- 程序计数器(线程私有):当前线程执行的字节码的位置提示器。
- 虚拟机栈(线程私有):保存局部变量,基本数据类型以及堆内存中对象的引用变量。
- 本地方法栈(线程私有):为JVM提供使用native方法的服务。
1、程序计数器
程序计数器是一块较小内存空间,用来记录当前程序所执行的字节码的行号。字节码解释器工作时通过改变这个计数器的值来选取下一条要执行的字节码指令,分支,循环,跳转,异常处理等都需要依赖这个程序寄之前来完成。线程切换后也需要恢复到正确的执行位置,因此,每一个线程都有一个独立到的程序计数器,各个线程的计数器相互独立,互不影响,因此线程计数器是必须私有的。
作用:
- 字节码解释器通过改变程序计数器来依次读取执行,从而实现代码的流程控制。(如:顺序执行、选择、循环、异常处理等)
- 在多线程下,程序计数器用于记录当前线程执行的位置,方便在程序切换后能够继续正确执行。
注意:
-
程序计时器是
唯一一个不会抛出OutOfMemoryError的(OOM)内存区域
-
程序计数器的生命周期是和线程同步的,随着线程的创建而创建,随着线程的消亡而死亡
2、虚拟机栈
注意:
- 在程序执行过程中,可以理解为方法调用方法,每一个方法在虚拟机栈上对应就是一个栈帧,每一个栈帧包好信息:局部变量表、操作数栈、动态链接、方法出口信息。
- 局部变量表:主要存放编译器可知的各种数据类型(boolean、byte、char、short、int、float、double、long),对象的引用(Reference类型,它不是对象本身,是一个执行对象起始位置的引用指针,也可以是指向一个代表对象的句柄)
虚拟机栈会出现两种异常:StackOverflowError
和OutOfMemoryError
StackOverflowError
:若虚拟机栈的大小不允许动态扩容,当线程请求的栈深度超过当前虚拟机栈的最大深度的时候,就会出现StackOverflowError
的异常。OutOfMemoryError
:若虚拟机栈的大小允许动态扩容,当前的线程请求栈是内存用完了,无法再动态扩容了,此时抛出OutOfMemoryError
异常。
虚拟机栈也是线程私有的,每个线程都有各自的虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
3、本地方法栈
本地方法栈和虚拟机栈作用非常相似
区别:
- 虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的native方法服务
本地方法栈执行时候,也会为本地方法创建一个个栈帧,栈帧中也包含局部变量表、操作数栈、动态链接、出口信息等,方法执行完成后也会出栈并释放掉内存空间也会抛出StackOverflowError
和OutOfMemoryError
的异常。
4、堆
Java虚拟机所管理的内存区域最大的一个内存空间就是堆,是线程共享的一块内存区域,在虚拟机启动的时候创建
。内存区域用来存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。
GC区域即垃圾回收主要作用的就是对象实例,因此GC所作用的区域就是堆区域
从垃圾回收的角度,堆可以细分为新生代和老年代
新生代细分为:Eden空间
,From Survivor空间
和To Survivor空间
,更细的划分是为了更高的回收内存或更快的分配内存
永久代:
- 在JDK1.6及之前,常量池就存在“永久代”
- 在JDK1.7,有“永久代”,但是已经逐步失去作用
- 在JDK1.8之后,无”永久代“, 取而代之的是一个“元空间”(Metaspace)区域
(永久代是属于JVM堆中的内存空间,元空间使用的物理内存,直接受到本机的物理内存限制)
该内存区域也是会抛出OutOfMemoryError
异常
5、方法区
方法区和堆一样,是线程共享的区域,存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据都存放在方法区。
生命周期和堆相同,随着JVM的创建而创建随着JVM的消亡而消亡
也会抛出OutOfMemoryError
的异常