一、Java代码是如何运行起来的
在我们编写Java代码时,每个类的文件格式后缀都为".java",而当我们通过编译器进行编译了以后:".java"文件----->>>>>".class"文件。这样,不同操作系统上的JVM就可以将这个字节码文件".class"进行加载,解释成电脑可以识别的机器码,实现了平台无关性,下面简单介绍一下".class"文件再进入JVM之前都经过了哪些处理步骤!!!
1.类加载
通过类加载器,可以将编译好的那些class字节码文件给加载到JVM中,具体有哪些类加载器在后面会说
2.链接
在类被加载后,需要通过链接过程将它合并到JVM的运行状态之中,然后才可以开始使用。链接包括三个步骤
2.1 验证:这一步验证class文件是否符合了JVM规范,符合才能交给JVM运行
2.2 准备:准备过程中,会为类分配内存空间,给内部的类变置分配内存空间并设置默认的初始值,在准备过程中不会执行代码
2.3 解析:解析的过程要确保这些被引用的类能被正确的找到,这个过程可能会导致其它的Java类被加载
3.初始化
3.1初始化过程的主要操作是执行静态代码块和初始化静态变量,按照源代码中从上到下的顺序依次执行
3.2一个类被初始化之前,它的直接父类也需要被初始化
3.3一个接口的初始化,不会引起其父接口的初始化
在经过上述步骤后,class文件就会进入到JVM中进行下一步的操作啦
二、JVM的类加载机制和双亲委派机制
上个问题我们说到,通过类加载器,把编译好的.class字节码文件,加载到JVM中进行执行。接下来我们介绍一下几种类加载器
Java中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的
系统提供的类加载器主要有以下三个:
启动类加载器(bootstrap class loader)、扩展类加载器(extensions class loader)、系统类加载器(AppClassLoader),三者之间是相互继承的关系,即系统类继承扩展类,扩展类继承启动类
1.启动类加载器:(核心类)
1.1 主要负责加载我们在机器上安装的Java目录下的核心类
1.2 在Java安装目录下有一个lib目录,这里有Java最核心的一些类库,支撑Java系统的运行
1.3所以一旦JVM启动,那么首先就会依托启动类加载器,去加载Java安装目录下的lib目录中的核心类库 (rt.jar)
2.扩展类加载器:(扩展类)
Java安装目录下有一个lib/ext的目录,通过扩展类加载器来加载此目录下的文件,支持Java系统的运行
3.应用程序类加载器:(程序类)
3.1主要负责加载ClassPath环境变量所指定的路径中的类
3.2大致可以理解为去加载写好的Java代码,把写好的那些类到内存里
接下来,我们来说说双亲委派机制
定义:在收到类加载的请求时,类加载器首先会把这个请求委托给父类加载器去完成,如果父类加载器无法完成,才会由自己尝试去加载。举一个栗子
JVM现在需要加载一个User类
应用程序类加载器(Application ClassLoader)会问问自己的爸爸扩展类加载器(ExtensionsClassLoader),你能加载到这个类吗?
然后扩展类加载器直接问自己的爸爸启动类加载器(BootstrapClassLoader),你能加载到这个类吗?
启动类加载器发现在Java安装目录下没找到这个类,就下推加载权利给扩展类加载器这个儿子
扩展类加载器在自己负责的目录中,也没有找到这个类,下推加载权利给儿子应用程序类加载器
应用程序类加载器在自己负责的范围内,比如写好的那个系统打包成的jar包吧,一下子发现就在这里,然后把这个类进行加载
那么,双亲委派机制的优点是什么呢?
1.Java 虚拟机不仅要看类的全名是否相同,还要看加载这个类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的
2.双亲委托机制使得Java类随着它的类加载器,具备了一种带有优先级的层次关系
3.例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。但如果没有双亲委派机制,假设用户自己编写一个称之为java.lang.Object的类,并将其放在程序的ClassPath中,由自身的程序类加载器自行去加载,这样就很容易导致一片混乱
因此,相同名称的类可以并存在 Java 虚拟机中,只需要不同的类加载器来加载它们即可。
不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,这种技术在许多框架中都被用到。
三、JDK8后虚拟机内存包含了几部分?各部分的功能是什么
我们先来看看JDK8之前的JVM架构
下面来讲讲各区域的主要功能(注意区分哪些是线程私有,哪些是线程公有)
堆:
1.堆是JVM内存管理的最大的一块区域,主要目的是存放对象的实例
2.所有新创建的对象实例和数组都会在堆上为其分配内存空间
3.线程共享
4.是垃圾收集器的主要管理区域,堆内存中可以存在物理上不连续的空间,只要逻辑上是连续的即可
5.如果在堆中没有内存完成实例分配,将抛出OutOfMemoryError
栈:
1.Java栈也称作虚拟机栈(Java Vitual Machine Stack),是线程私有的
2.Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括:
·局部变量表(Local Variables)
·操作数栈(Operand Stack)
·动态链接(调用另外一个方法)
·方法返回地址(Return Address)和一些额外的附加信息
本地方法栈:
1.专门为Native本地方法来实现的,线程私有
2.Java语言不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言(如C和C++)来实现对底层的访问
3.简而言之,一个本地方法就是一个Java调用非Java代码的接口
程序计数器:
1.它存储着当前线程所执行的字节码的行号
2.字节码解释器工作时,就是通过改变这个计数器的值,来让线程知道接下来需要执行哪条字节码指令
3.线程私有,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,每个线程之间的计数器互不影响
下面来看看JDK8后的架构
改进:
将方法区(永久代)进行了移除,新增了元空间概念,元空间是放置在JVM内存空间之外的直接内存中。这里说的直接内存直接受操作系统管理,而不是虚拟机,
这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响,关于垃圾回收的问题后续会提到。
下面来说下JDK8之后的JVM主要的变化为:
1.移除了永久代(PermGen),替换为元空间(Metaspace);
2.永久代中的类信息转移到了本地内存中的元空间中;
3.永久代中的 字符串常量池(interned Strings) 和静态变量( class static variables) 转移到了堆中;
4.永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)
四、对于堆内存,虚拟机如何进行分代管理?
JVM根据对象存活时间的长短,将堆内存分为新生代和老年代。对于刚创建的对象,我们将其放入新生代,长期存活的对象将进入老年代,默认是15岁,通过设置MaxTenuringThreshold来指定,但也不是永远要求年龄必须达到了MaxTenuringThreshold才能晋级老年代
新生代:新生代由三部分组成:Eden+(S0+S1)Survivor
大部分情况下,对象优先分配在Eden区,如果对象实在太大,新生代都放不下,会直接放到老年代;当Eden区满时,JVM将会触发一次Minor GC;Minor GC主要是利用新生代的三块区域进行的一次标记-整理算法,有关垃圾回收算法的内容将在后续提及
老年代:
1.主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以Full GC不会频繁执行,Full GC会清理整个堆空间
2.在进行Full GC前一般都先进行了一次Minor GC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发
3.当无法找到足够大的连续空间分配给新创建的较大对象时,也会提前触发一次Full GC进行垃圾回收腾出空间
4.Full GC会导致Stop The World问题,即终止应用程序的运行