JVM学习之路
一)JDK、JRE和JVM对比
JVM,JRE,JDK 都是 java 语言的支柱,他们分工协作。但不同的是 Jdk 和 JRE 是真实存在的,而 JVM 是一个抽象的概念,并不真实存在。
1.1 JDK
JDK:Java Development Kit,是 Java 语言的软件开发工具包(SDK)。JDK 物理存在,是 programming tools、JRE 和 JVM 的一个集合。JDK本体也是Java程序,因此运行依赖于JRE,由于需要保持JDK的独立性与完整性,JDK的安装目录下通常也附有JRE。目前Oracle提供的Windows下的JDK安装工具会同时安装一个正常的JRE和隶属于JDK目录下的JRE。
1.2 JRE
JRE:Java Runtime Environment,即Java 运行时环境,JRE 是物理存在的,主要由Java API 和 JVM 组成,提供了用于执行 java 应用程序最低要求的环境。
1.3 JVM
JVM是一种用于计算设备的规范,它是一个虚构的计算机的软件实现,简单的说,JVM是运行byte code字节码程序的一个容器。
JVM是可运行Java代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收,堆,和一个存储方法域。
JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
JVM执行字节码时实际上还是要解释成具体操作平台的机器指令的。
通过JVM,Java实现了平台无关性,Java语言在不同平台运行时不需要重新编译,只需要在该平台上部署JVM就可以了。因而能实现一次编译多处运行。(就像是你的虚拟机也可以在任何安了VMWare的系统上运行)
二)JVM的特点
- 基于堆栈的虚拟机:最流行的计算机体系结构,如英特尔 X86 架构和 ARM 架构上运行基于寄存器。比如,安卓的 Davilk 虚拟机就是基于寄存器 结构,而 JVM 是基于栈结构的。
- 符号引用 :除了基本类型以外的数据(类和接口)都是通过符号来引用,而不是通过显式地使用内存地址来引用。
- 垃圾收集 :一个类的实例是由用户程序创建和垃圾回收自动销毁。
- 网络字节顺序 :Java class文件用网络字节码顺序来进行存储,保证了小端的Intel x86架构和大端的RISC系列的架构之间的无关性。
三) JVM字节码
JVM使用Java字节码的方式,作为Java 用户语言和机器语言之间的中间语言。实现一个通用的、 机器无关的执行平台。
四) JVM类加载机制
4.1 什么是类的加载
简而言之,类加载器就是用于把.class文件中的字节码信息转化为具体的java.lang.Class对象的过程的工具。
具体过程:
- 在实际类加载过程中,JVM会将所有的.class字节码文件中的二进制数据读入内存中,将其导入到运行时数据区的方法区中。
- 当一个类首次被主动加载或被动加载时,类加载器会对此类执行类加载的流程 – 加载、连接(验证、准备、解析)、初始化。
- 如果类加载成功,会在堆内存中会创建一个新的java.lang.Class对象,该Class对象封装了类在方法区内的数据结构。
说明:
- 类的加载的最终产品是位于堆区中的 Class对象
- Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
- 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
4.2 加载.class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件
4.3 类的生命周期
类的生命周期:
- 加载(Loading)
- 连接
2.1 验证(Verification)
2.2 准备(preparation)
2.3 解析(Resolution) - 初始化(Initialization)
- 使用(Use)
- 卸载(Unloading)
其中,加载、连接、初始化归属于类的加载过程,连接又分为验证、准备、解析三个子过程。
4.4 类加载过程
类加载的过程分为三个步骤(五个阶段) :加载 -> 连接(验证、准备、解析)-> 初始化。
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段可以在初始化阶段之后发生,也称为动态绑定或晚期绑定。
类加载过程一:加载
加载:查找并加载类的二进制数据的过程
- 加载的过程描述
- 通过类的全限定名定位.class文件,并获取其二进制字节流。
- 把字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在Java堆中生成一个此类的java.lang.Class对象,作为方法区中这些数据的访问入口。
类加载过程二:连接
连接:包括 验证、准备、解析 三个步骤
1) 验证
验证:确保被加载的类的正确性。验证是连接阶段的第一步,用于确保Class字节流中的信息是否符合虚拟机的要求。
- 具体验证形式
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2) 准备
准备:为类的静态变量分配内存,并将其初始化为默认值。准备过程通常分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口信息等。
- 具体行为
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
- 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式赋值。
3) 解析
解析:把类中对常量池内的符号引用转换为直接引用。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用进行。
类加载过程三:初始化
初始化:对类静态变量赋予正确的初始值 (注意和连接时的解析过程区分开)。
- 初始化的目标
- 实现对声明类静态变量时指定的初始值的初始化;
- 实现对使用静态代码块设置的初始值的初始化。
- 初始化的步骤
- 如果此类没被加载、连接,则先加载、连接此类
- 如果此类的直接父类还未被初始化,则先初始化其直接父类
- 如果类中有初始化语句,则按照顺序依次执行初始化语句
- 初始化的时机
- 创建类的实例(new关键字)
- java.lang.reflect包中的方法(如:Class.forName(“xxx”))
- 对类的静态变量进行访问或赋值
- 访问调用类的静态方法
- 初始化一个类的子类,父类本身也会被初始化
- 作为程序的启动入口,包含main方法(如:SpringBoot入口类)。
4.5 结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
- 执行了 System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
4.6 类的主动引用和被动引用
主动引用
主动引用:在类加载阶段,只执行加载、连接操作,不执行初始化操作。
- 主动引用的几种形式
- 创建类的实例(new关键字)
- java.lang.reflect包中的方法(如:Class.forName(“xxx”))
- 对类的静态变量进行访问或赋值
- 访问调用类的静态方法
- 初始化一个类的子类,父类本身也会被初始化
- 作为程序的启动入口,包含main方法(如:SpringBoot入口类)
被动引用
被动引用: 在类加载阶段,会执行加载、连接和初始化操作。
- 被动引用的几种形式:
- 通过子类引用父类的的静态字段,不会导致子类初始化
- 定义类的数组引用而不赋值,不会触发此类的初始化
- 访问类定义的常量,不会触发此类的初始化
4.7 三种类加载器
类加载器:类加载器负责加载程序中的类型(类和接口),并赋予唯一的名字予以标识。
类加载器分类
站在Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;所有其它的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类 java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
- 启动类加载器: BootstrapClassLoader,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器: ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器: ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1、在执行非置信代码之前,自动验证数字签名。
2、动态地创建符合用户特定需要的定制化构建类。
3、从特定的场所取得java class,例如数据库中和网络中。
类加载器的关系
- Bootstrap Classloader是在Java虚拟机启动后初始化的。
- Bootstrap Classloader负责加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrap Classloader
- Bootstrap Classloader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为ExtClassLoader。
类加载器的特点
- 层级结构:Java里的类装载器被组织成了有父子关系的层级结构。Bootstrap类装载器是所有装载器的父亲。
- 代理模式: 基于层级结构,类的代理可以在装载器之间进行代理。当装载器装载一个类时,首先会检查它在父装载器中是否进行了装载。如果上层装载器已经装载了这个类,这个类会被直接使用。反之,类装载器会请求装载这个类
- 可见性限制:一个子装载器可以查找父装载器中的类,但是一个父装载器不能查找子装载器里的类。
- 不允许卸载:类装载器可以装载一个类但是不可以卸载它,不过可以删除当前的类装载器,然后创建一个新的类装载器装载
JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
类加载器的隔离问题
每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name) 进行搜索来检测这个类是否已经被加载了。
JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个类不是由一个 ClassLoader 加载,是无法将一个类的实例强转为另外一个类的,这就是 ClassLoader 隔离性。
为了解决类加载器的隔离问题,JVM引入了双亲委托机制。
4.8 双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
核心思想:其一,自底向上检查类是否已加载;其二,自顶向下尝试加载类。
具体加载过程
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在%JAVA_HOME%/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 如果ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
双亲委派模型意义:
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行
4.9 类的动态加载
类加载方式
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
Class.forName()和ClassLoader.loadClass()区别
- Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
- ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
- Class.forName(name,initialize,loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。
4.10 对象初始化顺序
(七). 对象的初始化
静态变量/静态代码块 -> 普通代码块 -> 构造函数
- 父类静态变量和静态代码块(先声明的先执行)
- 子类静态变量和静态代码块(先声明的先执行)
- 父类普通成员变量和普通代码块(先声明的先执行)
- 父类的构造函数
- 子类普通成员变量和普通代码块(先声明的先执行)
- 子类的构造函数
4.11 自定义类加载器
五)JVM内存结构
JVM主要包括:程序计数器(Program Counter),Java堆(Heap),Java虚拟机栈(Stack),本地方法栈(Native Stack),方法区(Method Area)
JVM内存结构布局:
注:
- PermGen Space:Permanent Generation space,持久代,是指内存的永久保存区域,也叫方法区。这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中,它和存放类实例(Instance)的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的应用中有很多CLASS的话,就很可能出现PermGen space错误。这种错误常见在web服务器对JSP进行pre compile的时候。如果你的WEB APP下都用了大量的第三方jar, 其大小超过了jvm默认的大小(4M)那么就会产生此错误信息了。
- Young Generation:年轻代
- Old Generation:老年代,也叫Tenured Generation
JVM内存结构主要有三大块:堆内存、方法区和栈。
- 堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配。
- 方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆)。
- 栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。
JVM和系统调用之间的关系:
方法区和堆是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
下面详细介绍各区域的作用。
5.1 程序计数器(PC, Program Counter,线程私有)
- 程序计数器是一个寄存器,是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器,用于指示跳转下一条需要执行的命令。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
- PC存在于JVM Stack上。
- PC的内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
5.2 Java虚拟机栈(JVM Stacks,线程私有)
- 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。
- 虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表(Local Variables)、操作栈(Operand Stack)、动态链接(Current Class Constant Pool Reference)、方法出口(Return Value)等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 局部变量表负责存放以下内容:
- 编译期可知的8种基本数据类型(boolean、byte、char、short、int、float、long、double)
- 对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)
- returnAddress类型(指向了一条字节码指令的地址)。
- 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
- 操作栈在执行字节码指令时会被用到,这种方式类似于原生的CPU寄存器,大部分JVM把时间花费在操作栈的花费上,操作栈和局部变量数组会频繁的交换数据。
- 动态连接控制着运行时常量池和栈帧的连接。所有方法和类的引用都会被当作符号的引用存在常量池中。符号引用是实际上并不指向物理内存地址的逻辑引用。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。无论如何,JVM必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。
- 在Java虚拟机规范中,对JVM Stack这个区域规定了两种异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
5.3 本地方法栈(Native Method Stack,线程私有)
- 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
- 虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
- 与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
5.4 堆(Heap,线程共享)
- 对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆(Garbage Collection Heap)”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代(Young Generation)和老年代(Old Generation/Tenured Generation);再细致一点的有Eden Space、From Survivor Space、To Survivor Space等。
但无论哪个区域,如何划分,存储的都是Java对象实例,进一步的划分是为了更好的回收内存或快速的分配内存。 - 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
- 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
5.5 方法区(Method Area,线程共享)
- 方法区有时被称为持久代(PermGen),对于存在永久代(Permanent)概念的虚拟机(HotSpot)而言,方法区存在于永久代。
- 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
- 对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。
- Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
- 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
JVM内存模型总结
1)各区域存储的内容:
Heap | Method Area | Thread 1…N |
---|---|---|
所有线程共享 | 所有线程共享 | 私有的栈 |
存储所有创建的对象及数组 | 存储类结构信息(如属性、方法数据、构造函数、方法的代码等) | 持有堆对象的引用、存储局部变量(原始类型) |
所有的对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:伊甸区(Eden),幸存者区域(Survivor Sapce),老年代(Old Generation Space)。
方法的执行都是伴随着线程的。原始类型的本地变量以及引用都存放在线程栈中。而引用关联的对象比如String,都存在在堆中。
堆:
存储的是对象,每个对象都包含一个与之对应的class。
JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。
对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定
栈:
每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)。
每个栈中的数据(原始类型和对象引用)都是私有的。
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失。
方法区:
静态区,跟堆一样,被所有的线程共享。
方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
例:1
2
3
4
5
6
7
8
9
10
11
12
13
14import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.log4j.Logger;
public class HelloWorld {
private static Logger LOGGER = Logger.getLogger(HelloWorld.class.getName());
public void sayHello(String message) {
SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
String today = formatter.format(new Date());
LOGGER.info(today + ": " + message);
}
}
这段程序的数据在内存中的存放如下:
- Heap:
- -Object: HelloWorld
- -Object: SimpleDateFormat
- -Object: String
- -Object: LOGGER
- Method Area:
- -Class: SimpleDateFormat
- -…
- -Class: Logger
- -…
- -Class: HelloWorld
- -Method: sayHello()
- -…
- -Class: SimpleDateFormat
- Thread 1: main
- -参数引用: “message” to String对象
- -变量引用:
- formatter to SimpleDateFormat
- today to String
- -局部原始类型:行号
2)理解OutOfMemoryError1
Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space
- 原因:对象不能被分配到堆内存中
1 | Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space |
- 原因:类或者方法不能被加载到老年代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库
1 | Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit |
- 原因:创建的数组大于堆内存的空间
1 | Exception in thread “main”: java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space? |
- 原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。
1 | Exception in thread “main”: <reason> <stack trace>(Native method) |
- 原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现
六)Java垃圾回收
概述
JVM中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于java堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。
对象存活判断
判断对象是否存活一般有两种方式:
- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
- 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的不可达对象。
在Java语言中,GC Roots包括:
- 虚拟机栈中引用的对象。
- 方法区中类静态属性实体引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
垃圾收集算法
垃圾收集算法主要有 标记-清除算法、复制算法、标记-压缩算法、分代收集算法 等。
其中分代收集法是目前大部分JVM所采用的方法。
- 新生代:复制算法(Copying)
- 老年代:标记-整理算法(Mark-Compact)
1)标记-清除算法
“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2)复制算法
“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。
3)标记-整理算法
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4)分代收集算法
GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
垃圾收集器
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
垃圾收集器主要有 Serial收集器、Parallel收集器、Parallel Old 收集器、CMS收集器、G1收集器 等
七)JAVA 四中引用类型
将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:
强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)。软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用:在GC时一定会被GC回收。弱引用需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象 来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。
虚引用:由于虚引用只是用来得知对象是否被GC。虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
八)JVM线程与原生线程的关系
JVM允许一个程序使用多个并发线程,Hotspot JVM中Java的线程与原生操作系统的线程是直接映射关系。即当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。run() 返回时,被处理未捕获异常,原生线程将确认由于它的结束是否要终止 JVM 进程(比如这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和 Java 线程的所有资源。
九)JVM性能调优
阅读:
[1]jvm系列(四):jvm调优-命令篇
[2]jvm系列(五):Java GC 分析
[3]jvm系列(六):Java服务GC参数调优案例
[4]jvm系列(七):jvm调优-工具篇
[5]jvm系列(九):如何优化Java GC「译」
[6]jvm系列(十):教你如何成为Java的OOM Killer
十)Java8中JVM内存的变化
完全移除了持久代,用MetaSpace来代替它
阅读:jvm系列(十一):Java 8-从持久代到metaspace
参考资料
类加载部分:
[1]jvm系列(一):java类的加载机制
[2]JVM系列(五) - JVM类加载机制详解
JVM内存结构部分:
[1]jvm系列(二):JVM内存结构
[2]Java基础:Java虚拟机(JVM)
垃圾回收部分:
[1]jvm系列(三):GC算法 垃圾收集器
[2]Java基础:Java虚拟机(JVM)
[3]JAVA GC回收算法
JVM性能调优部分:
[1]jvm系列(四):jvm调优-命令篇
其他:
[1]关于Jvm知识看这一篇就够了
[2]jvm知识点总览
[3]JVM(一)史上最佳入门指南
[4]JVM 的 工作原理,层次结构 以及 GC工作原理
[5]学习JVM是如何从入门到放弃的?