JVM原理及JVM内存
之前看了许多JVM原理的文章、写作的大牛们都讲的很透彻,但是私下觉得:写得详细难免复杂,写得简单难免遗漏。所以我就记下这一篇学习记录。
概念这么说
JVM是Java Virual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,他是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。我们划重点去理解:
- JVM是虚构计算机 ,可以理解为JVM是java程序的操作系统,而这个操作系统可运行的文件是.class文件;
- 虚拟机组成 ,一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域;
- 屏蔽具体操作系统平台, 这也就是平时说的java平台无关,一次编译,随处运行。;
1.JVM的基本过程
先看一张图:
我们可以看到,
- 编译过程是由Java源码编译器来完成,这个编译器将代码通过词法分析器、语法分析器、语义分析器和字节码生成器的解析最终生成字节码文件。
- 接下来已经有了字节码文件了,这时候需要就需要去解释这些字节码是干什么的,于是有了Java解释器,用来解释执行Java编译器编译后的程序。
- 解释器的作用,与C/C++ 的编译不同。C编译器编译生成代码时,每段代码为特定硬件平台运行而产生。在编译过程中,编译程序就要通过查表将所有对符号的引用转换为特定的内存偏移量,以保证程序运行。Java编译器则不对变量和方法的引用编译为数值引用,也不确定程序执行过程中的内存布局,而是将这些符号引用信息保留在字节码中,由解释器在运行过程中创立内存布局,然后再通过查表来确定一个方法所在的地址。这就是JAVA的可移植性。
这里的java解释器,也是JVM中的一部分,这里开始存现了JVM、经过一句“一次编译,到处运行”,我们引出了JVM的内容。我们先介绍这里的“解释”。
2.JVM的中的“解释”原理,三个重要机制
前面我们在概念中得知,JVM的组成:一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域;
先看看图理解一下:
这就是JVM的原理图,前面我们讲过,一个字节码文件要解释才能真正作用在硬件上,那么,上图就是这个解释的原理了。
这里可以清晰的看到java字节码文件的解释过程。关于里面用到的内容,我们要说明Java代码编译和执行的整个过程包含了以下三个重要的机制:
·Java源码编译机制
·类加载机制
·类执行机制
可以理解这三个机制就是Java源码到执行的全过程
1、首先讲第一个机制,Java源码编译机制是将java源代码经过java编译器生成java字节码的过程,具体过程如下:
2、而类加载机制和类执行机制这两个机制,则是在JVM中的两大重要机制,要解释这一点,我们就要先看看JVM的体系结构。
3.JVM的体系结构
首先是JVM的体系结构图:
由上到下,我们可以观察到,字节码文件经过类加载器->运行时数据区->执行引擎,这三个部分的内容分别为:
类加载器:加载class文件;
运行时数据区:包括方法区、堆、Java栈、PC寄存器、本地方法栈
执行引擎:执行字节码或者执行本地方法
下图为详细的JVM的体系结构图:
所以可以看到,.class文件首先通过类加载器,这就是三大机制中的“类加载
机制”开始了。那么类加载器怎么将字节码文件加载的呢?请看上面的内存区域,可以看到,JVM有两类存储区:常量缓冲池和方法区。常量缓冲池用于存储类名称、方法和字段名称以及串常量。方法区则用于存储Java方法的字节码。对于这两种存 储区域具体实现方式在JVM规格中没有明确规定。这使得Java应用程序的存储布局必须在运行过程中确定,依赖于具体平台的实现方式。
一旦java字节码文件被类加载器应用,首先进入方法区,执行引擎读取方法区的字节码自适应解析,然后pc寄存器指向了main函数所在位置,虚拟机开始为main函数在java栈中预留一个栈帧(每个方法都对应一个栈帧),然后开始跑main函数,main函数里的代码被执行引擎映射成本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,然后运行本地方法,调用操作系统APIi等等。
上面这个理解的链接–>原文链接:
1、类加载机制:加载 --> 验证 --> 准备 --> 解析 --> 初始化(其中验证、准备、解析统称为类的连接);
加载:通过一个类的全限定名来获取定义此类的二进制字节流(Class文件);将这个二进制字节流所代表的静态存储结果转化为方法区的运行时数据结构;在内存中生成一个java.lang.Class对象,注意:存放在方法区!
验证:验证目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;使用纯粹的Java代码无法做到诸如访问数组边界意外的数据、将一个对象转型为它未实现的类型、跳转到不存在的代码之类的事情,如果这样做了,编译器将拒绝编译!
准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。首先这时候进行内存分配的仅包括类变量(static修饰的变量),而不是实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
解析:解析阶段是虚拟机将class常量池内的符号引用替换为直接引用的过程。
这里解释一下:{
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可;
直接引用:是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。有了直接引用,那引用的目标必定已经在内存中存在。
}
初始化:类初始化阶段是类加载过程的最后一步;在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源:初始化阶段是执行类构造器< clinit > ( )方法的过程。
< clinit >( )方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static { }块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
上面这段理解—>原文链接
总结:类加载机制,
加载阶段(获取二进制字节流的静态存储结果转化为方法区的运行时数据结构)
验证阶段(验证加载阶段的字节流是否符合虚拟机规范)
准备阶段(分配变量在方法区所使用内存)
解析阶段(class常量池内的符号引用替换为直接引用的过程)
初始化阶段(执行类构造器< clinit > ( )方法的过程)
讲完了类是怎么加载的,我们来看看,加载完之后,是怎么执行的。这是三大机制中的类执行机制。
2、类执行机制
JVM主要在程序第一次运行时在主动使用类的时候,才会立即去加载,加载完毕就会生成一个java.lang.Class对象,并且存放在方法区。并不是在运行时就会把所有使用到的类都加载到内存中,这很好理解,全部进内存的话会影响性能,只有在非加载不可时,才加载进来,而且只加载一次,初始化类构造器()方法也只执行一次,所以static{} 块,类变量赋值语句也就只执行一次,只生成一个java.lang.Class对象!过程:输入字节码文件,字节码解析,输出执行完的结果!(原文链接)
说完三大机制,那么我们来看看运行时数据区。
4.运行时数据区
前面或多或少的看到,三大机制就是在整个JVM系统中的规则。他们要工作,就得在JVM运行时数据区进行协调。我们来看看数据区的内容有什么。
以上可以看见,运行时的数据区有这么些内容:
a.程序寄存器:线程隔离,用于存储每个线程下一步将执行的JVM指令,程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,是线程隔离的;
b.栈:线程隔离,每个线程创建的同时都会创建的同时都会创建JVM栈,JVM栈中存放当前线程中的局部基本类型的变量;它的生命周期与线程相同,一个线程对应一个java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”,而栈帧中包括了方法中的局部变量、用于存放中间状态值的操作栈。如果java栈空间不足了,程序会抛出StackOverflowError异常。
每个帧代表一个方法,Java方法有两种返回方式,return和抛出异常,两种方式都会导致该方法对应的帧出栈和释放内存。
c.堆:线程共享的区域。它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。原则上讲,所有的对象都在堆区上分配内存。
d.方法区:线程共享的区域,存放了所加载的类信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息;通过class对象中的getName等方法来获取信息时,实际这些数据是来源于方法区,方法区是全局共享的。
e.运行时常量池:存放类中固定的常量信息、方法和Field的引用信息等,其空间是从方法区中分配。
f.本地方法栈:线程隔离,JVM采用本地方法栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。
想仔细的了解,可以去看看
大致是这样的,JVM初始运行的时候都会分配好 Method Area(方法区) 和Heap(堆) ,而JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , VM Stack(虚拟机栈)和Native Method Stack (本地方法栈), 当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。
了解到方法区的各个部件的功能后,我们来看看什么是JVM垃圾回收。
JVM垃圾回收
Java类的实例所需的存储空间是在堆上分配的。前面在类加载的时候说过的解释器具体承担为类实例分配空间的工作。解释器在为一个实例分配完存储空间后,便开始记录对该实例所占用的内存区域的使用。一旦对象使用完毕,便将其回收到堆中。在Java语言中,除了new语句外没有其他方法为一对象申请和释放内存。对内存进行释放和回收的工作是由Java运行系统承担的。这允许Java运行系统的设计者自己决定碎片回收的方法。在SUN公司开发的Java解释器和Hot Java环境中,碎片回收用后台线程的方式来执行。这不但为运行系统提供了良好的性能,而且使程序设计人员摆脱了自己控制内存使用的风险。
JVM垃圾回收的一些功能 Garbage Collection(GC)
对新生代的对象的收集称为minor GC;
对老年代的对象的收集称为full GC;
程序中主动调用System.gc()强制执行的GC为full GC;
强引用:默认情况下,对象采用的均为强引用;
软引用:适用于缓存场景(只有在内存不够用的情况下才会被回收)
弱引用:在GC时一定会被GC回收
虚引用:用于判断对象是否被GC
那么JVM是如何判断对象是否要回收的呢?
这里用到:可达性分析法
通过一系列**“GC Roots”对象**作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复,GC不会回收它的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)。
这里我们可以看到,可达性分析法的起点是GC Roots”对象,那么怎样才能将对象认为是root对象呢,遵循以下原则:
(1) 虚拟机栈(栈帧中本地变量表)中引用的对象
(2) 方法区中静态属性引用的对象
(3) 方法区中常量引用的对象
(4) 本地方法栈中Native方法引用的对象
相关原文链接:
其他算法还有:
复制算法
把内存区域分为两块,每次使用一块,GC的时候把一块中的内容移动到另一块中,原始内存中的对象就可以被回收了,解决了内存碎片问题,但是内存利用率低。
标记压缩算法
和标记回收差不多,有两点不足,一个效率问题,标记和清除过程都效率不高;一个是空间问题,标记清除后会产生大量不连续的内存碎片;
分代算法
将内存区域分代,对不同的代使用不同的回收算法,通常分为新生代,老年代,和永久代。
新生代:每次GC时都会有大量对象死去,少量存活,使用复制算法;新生代又分为Eden区、Survivor(Survivor from、Survivor to)大小比例默认为8:1:1;JVM给每个对象定义一个对象年龄计数器,乳沟对象在Eden出生并经过第一个Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1.每熬过一次Minor GC,年龄就加1,当它的年龄到了一定程度(默认15岁,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代;如果Survivor相同年龄所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。
老年代:老年代中的对象存活率高、没有额外空间进行分配,就是用标记—清除或标记—整理算法;大对象可以直接进入老年代,JVM可以配置对象达到阈值后进入老年代的大小。
原文链接:https://www.jianshu.com/p/dcfe84c50811
在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。
永久代垃圾回收主要两部分内容:废弃的常量和无用的类。
判断废弃常量:一般是判断没有该常量的引用。
判断无用的类:要以下三个条件都满足
该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
加载该类的 ClassLoader 已经被回收
该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法
原文链接:https://blog.csdn.net/qq_41701956/article/details/81664921
垃圾收集器:
Serial收集器:是最基本、历史最久的收集器,单线程,并且在收集是必须暂停所有的工作线程,主要针对针对新生代,什么都不配置的话JVM默认的收集器,采用复制算法。
ParNew收集器:是Serial收集器的多线程版本,主要 针对新生代, Serial的多线程版本;
Parallel Scavenge收集器:新生代收集器,并行的多线程收集器。它的目标是达到一个可控的吞吐量,这样可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算;
Serial Old收集器:Serial 收集器的老年代版本,单线程,主要是标记—整理算法来收集垃圾;
Parallel Old收集器:Parallel Scavenge的老年代版本,多线程,主要是标记—整理算法来收集垃圾;Parallel Old 和 Serial Old 不能同时搭配使用,后者性能较差发挥不出前者的作用;
CMS收集器:收集器是一种以获取最短回收停顿时间为目标的收集器;基于标记清除算法,并发收集、低停顿、运作过程复杂(初始标记、并发标记、重新标记、并发清除)。CMS收集器有3个缺点:1。对CPU资源非常敏感(占用资源);2。无法处理浮动垃圾(在并发清除时,用户线程新产生的垃圾叫浮动垃圾),可能出现“Concurrent Mode Failure”失败;3。产生大量内存碎片
G1收集器:
特点:
分代收集,G1可以不需要其他GC收集器的配合就能独立管理整个堆,采用不同的方式处理新生对象和已经存活一段时间的对象;
空间整合:采用标记整理算法,局部采用复制算法(Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC;
可预测的停顿:能够让使用者明确指定一个时间片段内,消耗在垃圾收集上的时间不超过时间范围内;
原文链接https://www.cnblogs.com/hujinshui/p/10398958.html
以上就是本人收集的关于JVM的资料,第一次写,内容不足,认识短浅,总之,学无止境,以后多多学习。