JVM的内部结构与详述
JVM
前言
- 该文章是本人在学习java时,总结的笔记心得。基本来源于各大网络文章或博客。现在不记得出处了,所以如有雷同,望请海涵哈。
概念
-
JVM(Java Virtual Machine):Java
虚拟机,简称JVM
,是运行所有Java
程序的假想计算机,是Java
程序的运行环境,是Java
最具吸引力的特性之一。Java
代码,都运行在JVM
之上。 -
JRE(Java Runtime Enviroment):
是Java
程序的运行时环境,包含**JVM
和运行时所需要的核心类库**。 -
JDK(Java Development Kit):
是Java
程序开发工具包,包含**JRE
和开发人员使用的工具**。 - 总结:
JDK > JRE > JVM
- 我们想要运行一个已有的
Java
程序,那么只需安装JRE
。 - 我们想要开发一个全新的
Java
程序,那么必须安装JDK
。
- 我们想要运行一个已有的
JVM运行原理
-
JVM
是java
的核心和基础,在java
编译器和os
平台之间的虚拟处理器。java
编译器只需面向JVM
,生成JVM
能理解的代码或字节码文件。 -
JVM
执行程序的过程:1. 加载.class
文件;2. 管理并分配内存;3. 执行垃圾回收机制。 - 一段
Java
代码的在JVM
中的执行流程如下:
JVM基本结构
-
类加载器
(ClassLoader):
在JVM
启动时或者在类运行时将需要的class
加载到JVM
中 -
执行引擎:负责执行
.class
文件中包含的字节码指令 -
内存空间:是在
JVM
运行的时候操作所分配的内存区。运行时内存主要可以划分为以下几个区域:-
方法区
(Method Area)
:是各个线程共享的区域,存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然JVM
规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap
(非堆)。方法区还包含一个运行时常量池。 -
Java
堆(Heap)
:也是**线程共享的区域,存储Java
实例或者对象的地方。**这里GC
的主要区域。 -
Java
栈(Stack)
:每个线程私有的区域,它的生命周期与线程相同。每执行一个方法就会往栈中压入一个元素,这个元素叫栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java
栈中入栈到出栈的过程。 -
本地方法栈
(Native Method Stack)
:和Java
栈类似,只不过是为JVM
使用到的native
方法服务的。 -
程序寄数器
(PC Register)
:每个线程私有的区域,用于保存当前线程执行的内存地址。由于JVM
程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方。还有程序该怎么执行,哪个方法先执行,哪个方法后执行,这些指令执行的顺序就是程序寄数器在管,它的作用就是控制程序指令的执行顺序。 执行引擎就是根据程序寄数器调配的指令顺序,依次执行程序指令。
-
方法区
-
本地方法接口:连接本地方法库和
Java
栈。 - 垃圾收集器:进行垃圾回收机制
JVM内存分配
-
Java
的内存分配原理与C/C++
不同,C/C++
每次申请内存时都要malloc
进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这需要一定的开销,而**Java
虚拟机是先一次性分配一块较大的空间,然后每次new
时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销**,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC
机制有关了。 -
Java
一般内存申请有两种:静态内存和动态内存。- **静态内存:**编译时就能够确定的内存就是静态内存。即内存是固定的,系统一次性分配,如
int
类型变量 - **动态内存:**程序在执行时才知道要分配的存储空间大小,比如
java
对象的内存空间。 -
Java
栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。 - 但是
Java
堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态。
- **静态内存:**编译时就能够确定的内存就是静态内存。即内存是固定的,系统一次性分配,如
垃圾回收算法
引用计数法
- 引用计数法顾名思义,就是对一个对象被引用的次数进行计数,当增加一个引用计数就加
1
,减少一个引用计数就减1
。当引用计数为0
时,对象的内存空间将会被回收掉。引用计数算法原理非常简单,是最原始的回收算法,但是java
中没有使用这种算法。原因是因为:- 频繁的计数影响性能;
- 无法处理循环引用的问题。
标记-清除法
- 算法原理如名,先进行标记,再进行清除。
-
标记:遍历所有的
GC Roots
,并将从GC Roots
可达的对象设置为存活对象; - 清除:遍历堆中的所有对象。将没有被标记可达的对象清除。
-
标记:遍历所有的
-
不足
- 标记 - 清除算法在执行时,
Java
程序将被暂停,产生stop the world
。 - 因为涉及大量的内存遍历工作,所以执行性能较低,从而效率低,这也会导致
Java
程序暂停时间较长。 - 对象被清除之后,被清除的对象留下内存的空缺位置,造成内存不连续,空间浪费。
- 标记 - 清除算法在执行时,
标记-压缩法
- 在进行完标记清除之后,对内存空间进行压缩,节省内存空间,解决了标记清除算法内存不连续的问题。
- 标记压缩算法也会产生
stop the world
,不能和Java
程序并发执行。在压缩过程中一些对象内存地址会发生改变,Java
程序只能等待压缩完成后才能继续。
###分代收集算法
-
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
-
堆内存按对象的生命周期的不同划分为:年轻代
(Young Generation)
、年老代(Old Generation)
、持久代(Permanent Generation)
。其中持久代主要存放的是类信息,所以与Java
对象的回收关系不大,与回收息息相关的是年轻代和年老代。-
年轻代:被分为
3
个部分,Enden
区和两个Survivor
区(From和to)
。当Eden
区被对象填满时,就会执行Minor GC
。并把所有存活下来的对象转移到其中一个survivor
区(假设为from
区)。Minor GC
同样会检查存活下来的对象,并把它们转移到另一个survivor
区(假设为to
区)。这样在一段时间内,总会有一个空的survivor
区。**经过多次GC
周期后,仍然存活下来的对象会被转移到年老代内存空间。**通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor
的两个区是对称的,没先后关系,from
和to
是相对的。
-
年轻代:被分为
-
年老代:年轻代中经历了
N
次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,通常会在老年代内存被占满时将会触发Full GC
,回收整个堆内存。 -
持久代:用于存放静态文件,比如
Java
类、方法等。持久代对垃圾回收没有显著的影响。
垃圾回收器
串行收集器
- 串行收集器就是使用单线程进行垃圾回收。对新生代的回收使用复制算法,对老年代使用标记压缩算法。
- 串行收集器是最古老最稳定的收集器,尽管它是串行回收,回收时间较长,但其稳定性是优于其他回收器的,综合来说是一个不错的选择。
-
执行垃圾回收时,应用程序线程暂停,
GC
线程开始(开始垃圾回收),垃圾回收完成后,应用程序线程继续执行。
并行收集器
-
ParNew
回收器- **这个回收器只针对新生代进行并发回收,老年代依然使用串行回收。**回收算法依然和串行回收一样,新生代使用复制算法,老年代使用标记压缩算法。在多核条件下,它的性能显然优于串行回收器。
- 在进行垃圾回收时应用程序线程依然被暂停,
GC
线程并行开始执行垃圾回收,垃圾回收完成后,应用程序线程继续执行。
-
Parallel
回收器- 依然是并行回收器,但这种回收器有两种配置。
- 一种类似于
ParNew
:新生代使用并行回收、老年代使用串行回收。它与ParNew
的不同在于它在设计目标上更重视吞吐量,可以认为在相同的条件下它比ParNew
更优。 -
Parallel
回收器另外一种配置则不同于ParNew
,对于新生代和老年代均适应并行回收。
- 一种类似于
-
Parallel
回收器的流程和ParNew
的流程是一致的。在进行回收时,应用程序暂停,GC
使用多线程并发回收,回收完成后应用程 序线程继续运行。
- 依然是并行回收器,但这种回收器有两种配置。
CMS回收器
-
CMS
回收器:Concurrent Mark Sweep
,并发标记清除。 -
并发表示它可以与应用程序并发执行、交替执行。 标记清除表示这种回收器不是使用的是标记压缩算法,这和前面介绍的串行回收器和并发回收器有所不同。需要注意的是**
CMS
回收器是一种针对老年代的回收器,不对新生代产生作用**。 - 这种回收器优点在于减少了应用程序停顿的时间,因为它不需要应用程序完成暂定等待垃圾回收,而是与垃圾回收并发执行。
- 主要工作流程
-
初始标记:标记从
GC Root
可以直接可达的对象; -
并发标记
(和应用程序线程一起)
:主要标记过程,标记全部对象; - 重新标记:由于并发标记时,用户线程依然运行,因此在正式清理前,再依次重新标记,进行修正。
-
并发清除
(和用户线程一起)
:基于标记结果,直接清理对象。
-
初始标记:标记从
-
CMS
的优点显而易见,就是减少了应用程序的停顿时间,让回收线程和应用程序线程可以并发执行。 - 但它也不是完美的,从他的运行机制可以看出,因为它不像其他回收器一样集中一段时间对垃圾进行回收,并且在回收时应用程序还是运行,因此它的回收并不彻底。这也导致了
CMS
回收的频率相较其他回收器要高,频繁的回收将影响应用程序的吞吐量。
G1回收器
- 不同于其他的回收器、
G1
将堆空间划分成了互相独立的区块。每块区域既有可能属于老年代、也有可能是新生代,并且每类区域空间可以是不连续的(对比CMS
的老年代和新生代都必须是连续的)。这种将老年代区划分成多块的理念源于:当并发后台线程寻找可回收的对象时、有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时**G1
仍然需要暂停应用线程、但可以用相对较少的时间优先回收包含垃圾最多区块。**这也是为什么G1
命名为Garbage First
的原因:第一时间处理垃圾最多的区块。 -
G1
相对CMS
回收器来说优点在于:- 因为划分了很多区块,回收时减小了内存碎片的产生;
-
G1
适用于新生代和老年代,而CMS
只适用于老年代。
类加载器
类加载流程
-
加载:加载是类装载的第一步,首先通过
.class
文件的路径读取到二进制流,解析二进制流将里面的**元数据(类型、常量等)**载入到方法区,在Java
堆中生成对应的java.lang.Class
对象。 -
连接:连接又分为三步,验证、准备、解析。
- 验证:主要的目的就是判断
.class
文件的合法性。除此之外,还会对元数据、字节码进行验证。 - 准备:就是分配内存,给类的一些字段设置初始值。
-
解析:解析过程就是将符号引用替换为直接引用。例如某个类继承
java.lang.Object
,原来的符号引用记录的是"java.lang.Object"
这个符号,凭借这个符号并不能找到java.lang.Object
这个对象在哪里,而直接引用就是要找到java.lang.Object
所在的内存地址,建立直接引用关系,这样就方便查询到具体对象。
- 验证:主要的目的就是判断
-
初始化:初始化过程,主要包括执行类构造方法、
static
变量赋值语句,static{}
语句块,需要注意的是如果一个子类进行初始化,那么它会事先初始化其父类,保证父类在子类之前被初始化。所以,在Java
中初始化一个类,必然初始化了java.lang.Object
。
类加载器种类
- 类加载器
ClassLoader
,它是一个抽象类,ClassLoader
负责把具体实例的Java
字节码读取到JVM
当中,ClassLoader
还可以定制以满足不同字节码流的加载方式,比如从网络加载、从文件加载。ClassLoader
的负责整个类装载流程中的加载阶段。 - 系统中的
ClassLoader
-
BootStrap ClassLoader(启动ClassLoader)
–C/C++
写的,看不到源码也获取不到该类的对象。 -
Extension ClassLoader(扩展ClassLoader)
– 加载位置:jre\lib\ext
中 - **
App ClassLoader(应用 ClassLoader)
** – 加载位置:classpath
中 -
Custom ClassLoader(自定义ClassLoader)
– 必须继承ClassLoader
-
- 每个
ClassLoader
都有另外一个ClassLoader
作为父ClassLoader
,BootStrap Classloader
除外,它没有父ClassLoader
。注意:父ClassLoader
不代表是它的父类。 - 加载方式,采用双亲委托机制:自下而上的检查类是否被加载,自上而下的尝试加载类。需要注意的是,即使两个类来源于相同的
.class
文件,如果使用不同的类加载器加载,加载后的对象是完全不同的。