Java虚拟机

先来一个JVM物理结构图,后面基本是围绕这个图来说明各个部分。

image

1. JVM五大区

image

程序计数器

说明:当前线程所执行的字节码的行号指示器。

1)线程私有;

2)线程执行Java方法时,记录其正在执行的虚拟机字节码指令地址,线程执行Native方法时,计数器记录为空(Undefined);

3)唯一在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域;

虚拟机栈

说明:存放方法执行时所需的数据,其中每个方法为一个栈帧,存储了局部变量表(编译期间可以知道的各种基本数据类型和对象引用(不是对象本身))、操作数栈、动态链接等信息,方法的调用就对应了栈帧在虚拟机栈中的入栈和出栈的过程。

1)线程私有;

2)线程请求的栈深度如果大于虚拟机所允许的栈深度,将会抛出StackOverflowError异常;

3)如果虚拟机栈在动态扩展内存后依然不能申请到足够的内存,会抛出OutOfMemoryError异常;

本地方法栈

说明:作用基本同本地方法栈,只是虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务的,而本地方法栈却是为虚拟机使用到的Native(使用的编程语言不固定)方法服务的。

1)线程私有;

2)线程请求的栈深度如果大于虚拟机所允许的栈深度,将会抛出StackOverflowError异常;

3)如果虚拟机栈在动态扩展内存后依然不能申请到足够的内存,会抛出OutOfMemoryError异常;

Java堆

说明:存放对象的实例。

1) 所有线程共享;

2)Java堆是Java虚拟机所管理的内存中最大的一块;

3)垃圾收集器管理的主要区域,也称为GC堆;

4)如果在堆中内存不够无法完成实例的分配,并且堆无法在扩展时,就会抛出OutOfMemoryError异常;

方法区 (包括其中的运行时常量池)

说明:用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

1)所有线程共享;

2)当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池:

说明: Class文件中的常量池(不是运行时常量池),用来存放编译期生成的各种字面量和符号引用,而这部分内容将会在类加载之后进入方法区的运行时常量池中存放。

注意: 运行时常量池不一定就一定要从字节码常量池中拿取常量,可能在程序运行期间将新的常量放入池中,比如String.intern()方法,这个方法的作用就是:先从方法区的运行时常量池中查找看是否有该值,如果有,则返回该值的引用,如果没有,那么就会将该值加入运行时常量池中。

参考:Java垃圾收集算法

2. 对象标记算法

垃圾回收器在对堆内存进行回收前,第一件事情就是要确定哪些对象还”存活”中,哪些对象已经”死去”。

引用计数法

原理:给对象中添加一个引用计数器,每当有一个地方引用到它,计算器的值就加1,当引用失效的时候,计数器就减1,任何时刻计数器为0的对象就是没有被使用的对象,表示可以回收。

说明:这种方法在主流的虚拟机里面没有被采用,原因是它很难解决对象之间循环引用的问题。

可达性分析算法

原理:通过一系列称为”GC Roots”的对象作为起始起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象没有被使用,可以被回收。

四中引用 (参考:Java的四种引用方式

强引用

说明:是指创建一个对象并把这个对象赋给一个引用变量。

软引用(SoftReference)

说明:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

弱引用(WeakReference)

说明:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

虚引用

说明:它并不影响对象的生命周期,无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

3. 垃圾回收算法

标记清除算法

思路:

第一步:使用可达性分析算法将无用的对象标记出来;

第二步:将第一步标记的对象进行回收清除;

缺点:

1)效率问题:标记和清除的效率都不高;

2)空间问题:清除后会产生大量不连续的内存碎片。

复制算法(新生代)

思路:

第一步:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块;

第二步:一块的内存用完了,就将还存活着的对象复制到另外一块上面;

第三步:把已使用的内存空间一次清理掉;

优点:

1)实现简单
2)运行高效
3)不容易产生碎片

缺点:

对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

标记-整理算法(老年代)

思想: 在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

分代收集算法

思路: 根据对象存活的生命周期将内存划分为若干个不同的区域。

image

新生代: 包含有Enden (80%)、form survivor space (10%)、to survivor space(10%)三个区,绝大多数最新被创建的对象会被分配到这里,大部分对象在创建之后会变得很快不可达,在该区域发生的垃圾收集被称为Minor GC。

老年代: 从新生代存活下来的对象会被拷贝到这里,它的空间比新生代要大,所以在老年代上发生的GC要比新生代少得多。在该区域发生的垃圾收集被称为Major GC / Full GC。

持久代: 也被称为方法区,用来存放类常量和字符串常量,这个区域不是用来存储那些从老年代存活下来的对象。它也会发生GC操作。

内存分配与回收的策略

1)对象优先在Enden上分配

2)大对象可以直接进入老年代 (设置标签-XX:PretenureSizeThreshold = n)

3)长期存活的对象将进入老年代

注意:虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Enden出生并且经过第一次Minor GC仍然存活,并且能够被Survivor空间容纳,进入移动到Survivor空间,并且设置对象年龄为1,对象在Survivor区每熬过一个Minor GC,年龄就增加1岁,当它的年龄到达一定的程度(默认为15岁),就会被移动到老年代,这个年龄阀值可以通过-XX:MaxTenuringThreshold设置。

动态对象年龄判断

说明:虚拟机并不是永远要求对象的年龄达到MaxTenuringThreshold才移动到老年代,如果Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象也被移动到老年代。

空间分配担保

说明:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

4. 垃圾收集器

参考:Java HotSpot虚拟机的内存管理(垃圾收集)
参考:7种垃圾收集器:主要特点 应用场景 设置参数 基本运行原理

image

新生代收集器: Serial、ParNew、Parallel Scavenge;

老年代收集器: Serial Old、Parallel Old、CMS;

整堆收集器: G1;

说明: 两个收集器间有连线,表明它们可以搭配使用。

Serial 收集器

Serial是最基本的收集器,历史悠久。是一个单线程收集器,而且它在进行垃圾回收的时候,必须暂停其他所有的工作线程,直到收集结束。这就意味着,每次进行垃圾收集都必须停掉用户正常工作的线程。

线程:单线程;

算法:复制算法;

ParNew 收集器(新生代)

ParNew是Serial的多线程版本。仍要停顿

线程:多线程;

算法:复制算法;

Parallel Scavenge 收集器(新生代)

Parallel Scavenge是一个新生代收集器,使用多线程和复制算法。相比其他收集器,只有这个收集器是针对系统吞吐量进行改进,适用于后台运算并且交互不多的程序。其他收集器则更关注改善收集时的停顿时间,适用于用户交互的程序。

吞吐量:

用于运行用户代码的时间与CPU总消耗时间的比值;

即吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间);

线程:多线程;

算法:复制算法;

注意: 通常,平均响应时间越短,系统吞吐量越大;平均响应时间越长,系统吞吐量越小。但是,系统吞吐量越大,未必平均响应时间越短。因为在某些情况(例如,不增加任何硬件配置)吞吐量的增大,有时会把平均响应时间作为牺牲,来换取一段时间处理更多的请求。

Serial Old 收集器(老年代)

Serial Old是Serial的老年代版本,专门用于收集老年代,采用“标记整理算法”。

线程:单线程;

算法:标记整理算法;

Parallel Old 收集器(老年代)

Parallel Old是Parallel Scavenge的老年代版本,使用多线程和“标记整理算法”。

线程:多线程;

算法:标记整理算法;

CMS 收集器(老年代)

说明: CMS收集器是一种以获取最短回收停顿时间为目标的收集器,给用用户带来较好的体验,可称之为并发低停顿收集器。基于标记-清除算法实现。可用于新、老代收集,一般用于老年代收集,然后搭配一个新生代收集器(可以是Serial和ParNew)

缺点:

1)对CPU资源敏感(因为是并发)。在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程(即CPU资源)而导致用户应用程序变慢,总吞吐量降低。

2)无法处理浮动垃圾。因为是边收集,边产生垃圾。

3)标记-清除算法会产生空间碎片。需要额外碎片整理过程,停顿时间变长。

线程:多线程;

算法:标记清除算法;

G1 收集器

说明: G1收集器是当前收集器技术发展最前沿的成果。基于标记-整理算法,可以精确控制停顿。基本不牺牲吞吐量的前提下完成低停顿的内存回收。这是由于它将新生代、老年代划分为多个区域,并维护一个每个区域收集的优先列表,保证了在有限的时间内可以获得最高的收集效率。

缺点:没有经过实际应用的考验,缺少测试。

应用场景总结:

用户交互:ParNew、CMS

高吞吐量:Parallel Scavenge

5. fullGC和MinorGC区别

Minor GC(新生代GC):发生在新生代的垃圾收集动作,因为Java对象大多数都具备朝生夕灭的特点,所以Minor GC 非常频繁,通常回收速度比较快。

Major GC / Full GC(老年代GC):指发生在老年代的GC,速度一般会比Minor GC慢10倍以上。

触发条件:

Minor GC:当Eden区满时,触发Minor GC。

Major / Full GC:

1)调用System.gc时,系统建议执行Full GC,但是不必然执行

2)老年代空间不足

3)方法区空间不足

说明:

1)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

2)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。


6. 类加载机制

说明: Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。

类从被加载到虚拟机内存中到卸载出内存为止,它的整个生命周期如下:

image

加载: 查找和导入Class文件;

连接: 把类的二进制数据合并到JRE中;

1)验证:检查载入Class文件数据的正确性;

2)准备:给类的静态变量分配存储空间;

3)解析:将常量池符号引用转成直接引用;

初始化: 对类的静态变量,静态代码块执行初始化操作;

注意: 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这个顺序来按部就班地开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段后再开始。

说明: Java程序可以动态扩展是由运行期动态加载和动态链接实现的。比如:如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现(多态),解析过程有时候还可以在初始化之后执行;比如:动态绑定(多态)。

结束生命周期:

在以下情况的时候,Java虚拟机会结束生命周期
1. 执行了System.exit()方法;
2. 程序正常执行结束;
3. 程序在执行过程中遇到了异常或错误而异常终止;
4. 由于操作系统出现错误而导致Java虚拟机进程终止;

下面展开分析:

加载

说明:
1)将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内。

2)然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

3)类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

image

注意: 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它。

加载class文件方式:

1)从本地系统中直接加载

2)通过网络下载.class文件

3)从zip,jar等归档文件中加载.class文件

4)从专有数据库中提取.class文件

5)将Java源文件动态编译为.class文件

加载阶段,虚拟机完成工作:

1)通过一个类的全限定名称来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

验证

说明: 验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证元数据的验证字节码验证符号引用验证

1)文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。

2)元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。

3)字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。

4) 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身。

准备

说明: 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。

1)这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

2)这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

解析

说明: 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

1)符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。

2)直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

解析分析:

1)类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。

2)字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。

3)类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。

4)接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。

初始化

说明: 类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了加载(Loading)阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

初始化: 为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

1)声明类变量时指定初始值;

2)使用静态代码块为类变量指定初始值;

类初始化的触发条件: 只有当对类的主动使用的时候才会导致类的初始化。

1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

说明: 只有上述四种情况会触发初始化,也称为对一个类进行主动引用,除此以外,所有其他方式都不会触发初始化,称为被动引用。

通俗的解释对应下面的六种:

1)创建类的实例,也就是new的方式;

2)或者对该静态变量赋值;

3)调用类的静态方法;

4)反射(如Class.forName(“com.shengsiyuan.Test”));

5)初始化某个类的子类,则其父类也会被初始化;

6)Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类;

7. 类加载器与双亲委派模型

类加载器

说明: 在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器。这一动作是放在Java虚拟机外部去实现的,以便让应用程序自己决定如何获取所需的类。

注意:

1)虚拟机规范并没有指明二进制字节流要从一个Class文件获取,或者说根本没有指明从哪里获取、怎样获取。

2)JVM中两个类是否“相等”,首先就必须是同一个类加载器加载的,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要类加载器不同,那么这两个类必定是不相等的。

类加载器分类

说明:从Java虚拟机的角度来说,只存在两种不同的类加载器:一种是(Bootstrap ClassLoader),这个类加载器使,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都有Java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。

JVM虚拟机角度:

1)启动类加载器(用C++语言实现(HotSpot虚拟机中),是虚拟机自身的一部分)

2)其他的类加载器(由Java语言实现,独立于虚拟机外部,继承自java.lang.ClassLoader类。)

开发者的角度:

1)启动(Bootstrap)类加载器(将Java_Home/lib下面的类库加载到内存中(比如rt.jar),开发者无法直接引用)

2)扩展(Extension)类加载器(将Java_Home/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用)

3)应用程序(Application)类加载器(将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用,由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统(System)加载器。)

双亲委派模型

说明: 该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。

image

protectedsynchronized Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException {
    //首先判断该类型是否已经被加载
    Class c = findLoadedClass(name);
    if (c ==null) {
        //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
        try {
            //如果存在父类加载器,就委派给父类加载器加载
            if (parent !=null) {
                c = parent.loadClass(name,false);
            //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
            }else {
                c = findBootstrapClass0(name);
            }
        }catch (ClassNotFoundException e) {
            //如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
            c = findClass(name);
            }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

执行过程: 某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

注意: 双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。大多数的类加载器都遵循这个模型,但是JDK中也有较大规模破坏双亲模型的情况,例如线程上下文类加载器(Thread Context ClassLoader)的出现。


7. 与垃圾回收相关的JVM参数

-Xms / -Xmx :堆的初始大小 / 堆的最大大小
-Xmn :堆中年轻代的大小
-XX:-DisableExplicitGC :让System.gc()不产生任何作用
-XX:+PrintGCDetails :打印GC的细节
-XX:+PrintGCDateStamps :打印GC操作的时间戳
-XX:NewSize / XX:MaxNewSize : 设置新生代大小/新生代最大大小
-XX:NewRatio :可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution: 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
-XX:TargetSurvivorRatio:设置幸存区的目标使用率

为什么新生代内存需要有两个Survivor区

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值