JVM基础

本文首先介绍一下Java虚拟机的生存周期,然后大致介绍JVM的体系结构,最后对体系结构中的各个部分进行详细介绍。
首先这里澄清两个概念:JVM实例和JVM执行引擎实例,JVM实例对应了一个独立运行的java程序,而JVM执行引擎实例则对应了属于用户运行程序的线程;也就是JVM实例是进程级别,而执行引擎是线程级别的。

一、 JVM的生命周期

JVM实例的诞生: 当启动一个java程序时,一个JVM实例就诞生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点,既然如此,那么JVM如何知道是运行class A的main而不是运 的由来,如java classA hello world,这里java是告诉os运行Sun java 2 SDK的java虚拟机,而classA则指出了运行JVM所需要的类名。

JVM实例的运行:main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程, 守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。

JVM实例的消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。

二、JVM的体系结构

粗略分来,JVM的内部体系结构分为三部分,分别是:类装载器(ClassLoader)子系统,运行时数据区,和执行引擎, 本地方法接口和垃圾收集模块。 下面将先介绍类装载器,然后是执行引擎,最后是运行时数据区

1. 类装载器

就是用来装载.class文件的。JVM的两种类装载器包括:启动类装载器和用户自定义类装载器,启动类装载器是JVM实现的一部分,用户自定义类装载器则是Java程序的一部分,必须是ClassLoader类的子类。
启动类装载器: 只在系统类(java API的类文件)的安装路径查找要装入的类
用户自定义类装载器:
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

加载.class文件的方式

	从本地系统中直接加载   
	通过网络下载.class文件  
	从zip,jar等归档文件中加载.class文件  
	从专有数据库中提取.class文件  
	将Java源文件动态编译为.class文件  
2.类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

什么时候触发类加载?

  1. 使用new关键字实例化对象
  2. 读取或者设置一个类的静态变量的时候
  3. 调用类的静态方法的时候;
  4. 对类进行反射调用的时候;
  5. 初始化子类时,父类会先被初始化;
  6. 对类使用动态代理的时候需要先被初始化;

3.类加载过程

类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

Java中的绑定:绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对java来说,绑定分为静态绑定和动态绑定:  
静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对java,简单的可以理解为程序编译期的绑定。java当中的方法只有final,static,private和构造方法是前期绑定的。  
动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。在java中,几乎所有的方法都是后期绑定的。  

4. 加载

1.加载过程

加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

 1.通过一个类的全限定名来获取其定义的二进制字节流,是可控性最强的阶段(Class文件、Jar包、网络中Applet、JSP)。
 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
 3.在Java堆中生成一个代表这个类的java.lang.Class对象(Class对象比较特殊,存放在方法区),作为对方法区中这些数据的访问入口  

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。

2、连接
2.1验证
验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

1、文件格式验证
验证class文件格式规范,例如: class文件是否已魔术0xCAFEBABE(咖啡宝贝)开头 , 主、次版本号是否在当前虚拟机处理范围之内等

2、元数据验证
这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求。验证点可能包括:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)、这个类是否继承了不允许被继承的类(被final修饰的)、如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法。

3、字节码验证
进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如:保证访法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型、保证跳转命令不会跳转到方法体以外的字节码命令上。

4、符号引用验证

2.2准备

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

这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值。

2.3解析

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

符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。

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

解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。

3、初始化

类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器< clinit >()方法的过程。
在以下四种情况下初始化过程会被触发执行:

1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。
2、使用java.lang.reflect包的方法对类进行反射调用的时候
3、当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化
4、jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类在类的初始化里,有一个著名的单例延迟初始化

由于类的初始化是线程安全的,并且仅被执行一次,因此程序可以确保多线程环境下有且仅有一个Singleton实例。

4.2 类加载机制

全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

4.3 类加载器

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。

(1)启动类加载器:
负责加载JAVA_HOME\lib目录中并且能够被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。存放在JRE的lib目录下jar包中的类(以及由虚拟机参数-Xbootclasspath指定的类)

(2) 扩展类加载器:
该加载器主要是负责加载JAVA_HOME\lib\,该加载器可以被开发者直接使用。

其父类加载器是启动类加载器,负责加载相对次要,但又通用的类,比如存放在JRE的lib/ext目录下jar包中的类(以及由系统变量java.ext.dirs指定的类)

(3)应用类加载器
该类加载器也叫系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接可以该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

(4)自定义类加载器(必须继承ClassLoader)

2. 运行时数据区

JVM内存模型又叫运行时数据区,根据JVM规范,JVM内存共分为堆,虚拟机栈,方法区,本地方法栈,和程序计数器五个部分。

1、线程私有:虚拟机栈,本地方法栈,程序计数器
2、线程共享:堆,方法区,直接内存

堆是Java虚拟机所管理的内存最大的一块。堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一的目的存放对象实例。几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此也被称为 GC堆(Garbage Collected Heap)。

1.从内存回收的角度看,由于现在垃圾收集器基本都采用分代垃圾收集算法,所以Java堆又分为:新生代和老年代。其中新生代又分为:Eden区,From Survivor,To Survivor区。进一步划分的目的是更好的回收内存,或更快的分配内存。分代回收是基于:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。

2.从内存分配的角度看,线程共享的Java堆中可能会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

PS:可以通过-Xms,-Xmx 分别控制堆初始化时最小堆内存和最大堆内存的大小。

Java虚拟机栈

Java虚拟机栈是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型。Java虚拟机栈是由一个个栈帧组成,线程在执行一个方法时,便会向栈中放入一个栈帧,每个栈帧中都包括局部变量表,操作数栈,动态链接,方法出口等信息。方法的执行就对应着栈帧在虚拟机中入栈和出栈的过程;栈里存放着各种基本类型和对象的引用。

局部变量表主要存放了各种基本数据类型(boolean,byte,char,short,int,float,long,double)和对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

Java虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError

1.StackOverFlowError :若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。

2.OutOfMemoryError:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。 即使有足够的物理内存可用,只要达到堆空间设置的大小限制,此异常仍然会被触发。

方法区(JDK 1.8后该区域被抛弃)

方法区与Java堆一样,是各个线程所共享的,它用来存储已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。

方法区是JVM提出的规范,而永久代就是方法区的具体实现。Java虚拟机对方法区的限制非常宽松,可以像堆一样不需要连续的内存,还可以选择不实现垃圾收集,所以垃圾收集行为在方法区是比较少出现的。

在方法区会报出 永久代内存溢出的错误。JDK1.8为了解决这个问题,提出了meta space(元空间)的概念,就是为了解决永久代内存溢出的情况,一般情况下,在不指定元空间大小的情况下,虚拟机方法区内存大小就是宿主主机的内存大小。

程序计数器

程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等功能都需要依赖程序计数器来完成。

Java虚拟机的多线程是通过线程轮流切换并分配CPU的时间片的方式实现的,因此在任何时刻一个处理器(如果是多核处理器,则只是一个核)都只会处理一个线程,为了线程切换后能恢复正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,因此这类内存区域为“线程私有”的内存。

程序计数器的作用:

1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行,选择,循环,异常处理
2.在多线程的情况下,程序计数器用于记录当前现场执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪了。

PS:程序计数器是不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

本地方法栈

与虚拟机栈发挥的作用类似,它们之间的区别是:虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机使用到的native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常。

Java虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建,唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
java堆是垃圾收集器管理的主要区域,因此也被称作GC堆。从垃圾回收的角度,采用分代垃圾算法,java堆还可以细分为:新生代和老年代,再细致一点有:Eden空间、FromSurvivor、To Survivor空间等。进一步划分的目的是更好的回收内存,或者更快的分配内存。
大部分情况,对象会在Eden区域分配,在一次新生代垃圾回收之后,如果对象还活着,则会进入S0 或S1,并且对象的年龄会加1(Eden区 -> Survivor区域后对象的初试年龄变为1) 当它的年龄增加到一定程度之后,(默认15岁)就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 --XX:MaxTenuringThreshold来设置。

jdk 1.8之前可以通过下面参数来调节方法区大小

-XX:PerSize = N //方法区 初试大小
-XX:MaxPermSize = N //方法区最大大小。超过这个值会抛出OOM

JDK 1.8的时候,方法区被彻底移除,取而代之的是元空间,元空间使用的是直接内存。下面是一些常用参数:

-XX:MetaspaceSize =N   //设置Metaspace的初试(和最小大小)
-XX:MaxMetaspaceSize=N  //设置Metaspace的最大大小

【JVM】强引用,弱引用,软引用,虚引用之间的区别

Java中提供这四种引用类型主要有两个目的:

第一是可以让程序员通过代码的方式决定某些对象的生命周期
第二是有利于JVM进行垃圾回收

强引用

强引用是最普遍的引用,如果一个对象具有强引用,垃圾回收器不会回收该对象,当内存不足时,JVM宁愿抛出OutOfMemoryError异常也不会回收这种对象,只有当这个对象没有被引用时,才有可能会被回收。

下面代码中object和str都是强引用:

Object object = new Object();  
String str = "hello";   

软引用

软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。
如果一个对象只具有软引用,则:

  • 1.当内存空间足够,垃圾回收器就不会回收它
  • 2.当内存空间不足了,就会回收该对象,JVM会优先回收长时间闲置不用的软引用的对象,对那些刚刚构建的或刚刚使用过的“新”软引用对象尽可能会保留
  • 3.如果回收完还没有足够的内存,才会抛出内存溢出异常。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用适合用来实现缓存(比如浏览器的“后退”按钮使用的缓存),内存空间充足的时候将数据缓存在内存中,如果空间不足了就将其回收掉。可以用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,java虚拟机会把这个软引用加入到与之关联的引用队列中。

package org.fool.reference;

import java.lang.ref.SoftReference;

public class SoftRefereneceTest {
public static void main(String[] args) {
/**
 * 软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。
 * 对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,
 * 并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
 * 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
 */
SoftReference<String> sr = new SoftReference<>(new String("hello"));

System.out.println(sr.get());   // hello

System.gc();

System.out.println(sr.get());   // hello
}
}

弱引用

弱引用也是用来描述非必须对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期,它只能生存到下一次垃圾收集之前。当垃圾回收器扫描到只具有弱引用的对象时,无论当前内存空间是否足够,都会回收它。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用也可以和一个引用队列(ReferenceQueue)联合使用。

使用场景:一个对象只是偶尔使用,希望在使用时能随时获取,但也不想影响对该对象的垃圾收集,则可以考虑使用弱引用来指向该对象。

package org.fool.reference;

import java.lang.ref.WeakReference;

public class WeakReferenceTest {
public static void main(String[] args) {
/**
 * 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
 * 在java中,用java.lang.ref.WeakReference类来表示。
 */
WeakReference<String> wr = new WeakReference<>(new String("hello"));

System.out.println(wr.get());   // hello

System.gc();

System.out.println(wr.get());   // null
/**
 * 第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。
 * 不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,
 * 如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。
 * 弱引用可以和一个引用队列(ReferenceQueue)联合使用,
 * 如果弱引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
 */
}
}

虚引用

虚引用和前面的软引用,弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

public static void main(String[] args) {
/**
 * 虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。
 * 如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
 * 要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,
 * 如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。
 * 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
 * 如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
 */

ReferenceQueue<String> queue = new ReferenceQueue<>();

PhantomReference<String> pr = new PhantomReference<>(new String("hello"), queue);

System.out.println(pr.get());   // null

System.gc();

System.out.println(pr.get());   // null
}

3. 执行引擎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值