JVM概述
1.1 JVM作用:
Java 虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对
应平台上的机器码指令执行,每一条 java 指令,java 虚拟机中都有详细定义,如怎么取操
作数,怎么处理操作数,处理结果放在哪儿。
1.2 JVM特点:
-
一次编译到处运行
-
自动内存管理
-
自动垃圾回收功能
1.3 JVM组成:
-
类加载器(ClassLoader)
-
运行时数据区(Runtime Data Area)
-
执行引擎(Execution Engine)
-
本地库接口(Native Interface)
简图:
1.4 组成部分的执行顺序:
程序在执行之前先要把 java 代码转换成字节码(class 文件),jvm 首先需 要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运 行时数据区(Runtime Data Area),而字节码文件是 jvm 的一套指令集规范, 并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎 Execution Engine) 将字节码翻译成底层系统指令再交由 CPU 去执行,而 这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现 整个程序的功能,这就是这 4 个主要组成部分的职责与功能。而我们通常所说的 JVM 组成指的是运行时数据区(Runtime Data Area),因 为通常需要程序员调试分析的区域就是“运行时数据区”,或者更具体的来说就 是“运行时数据区”里面的 Heap(堆)模块。
二、JVM结构-类加载
类加载器子系统负责从文件系统或者网络中加载 class 文件, class 文件在文 件开头有特定的文件标识(字节码文件都以 CA FE BA BE 标识开头)。 classLoader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。加载的类信息存放于一块称为方法区的内存空间。除了类的信息 外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量 (这部分常量信息是 class 文件中常量池部分的内存映射).
2.1 类加载时ClassLoader的角色:
-
class file 存在于硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板 在执行的时候是要加载 JVM 当中来,根据这个模板实例化出 n 个一模一样的实 例.
-
class file 加载到 JVM 中,被称为 DNA 元数据模板,放在方法区中.
-
在.class–>JVM–>最终称为元数据模板,此过程就要有一个运输工具(类加 载器 Class Loader),扮演一个快递员的角色.
2.2 初始化:
类什么时候初始化?
1 )创建类的实例,也就是 new 一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“”))
5)初始化一个类的子类(会首先初始化子类的父类)
类的初始化顺序
对 static 修饰的变量或语句块进行赋值. 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
顺序是:父类 static –> 子类 static –> 父类构造方法- -> 子类构造方法
2.3 加载器分类:
引导类加载器(启动类加载器 BootStrap ClassLoader)
这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类 库.
并不集成于 java.lang.ClassLoader 没有父加载器.
负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器.
出于安全考虑,引用类加载器只加载包名为 java,javax,sun 等开头的类.
扩展类加载器(Extension ClassLoader)
Java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现.
派生于 ClassLoader 类.
上层类加载器为引用类加载器.
从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 系统安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库.如果用户创建的 jar 放在此目录下,也 会自动由扩展类加载器加载.
应用程序类加载器(系统类加载器 Application ClassLoader)
Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现.
派生于 ClassLoader 类.
上层类加载器为扩展类加载器.
加载我们自己定义的类.
该类加载器是程序中默认的类加载器.
通过 类名.class.getClassLoader(),ClassLoader.getSystemClassLoader()来获得
2.4 双亲委派机制:
Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件 时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式.
工作原理:
-
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请 求委托给父类的加载器去执行.
-
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终 将到达顶层的启动类加载器.
-
如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完 成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制.
如果均加载失败,就会抛出 ClassNotFoundException 异常。
双亲委派优点?
1 安全,可避免用户自己编写的类动态替换 Java 的核心类,如 java.lang.String
2 避免全限定命名的类重复加载(使用了 findLoadClass()判断当前类是否已加载
2.5 沙箱安全机制:
作用:防止恶意代码污染 java 源代码
比如上面我们定义了一个类名为 String 所在包也命名为 java.lang,因为这个类 本来是属于 jdk 的,如果没有沙箱安全机制的话,这个类将会污染到系统中的 String,但是由于沙箱安全机制,所以就委托顶层的引导类加载器查找这个类,如 果没有的话就委托给扩展类加载器,再没有就委托到系统类加载器.但是由于 String 就是 jdk 的源代码,所以在引导类加载器那里就加载到了,先找到先使用, 所以就使用引导类加载器里面的 String,后面的一概不能使用,这就保证了不被 恶意代码污染.
2.6 面试题:
在 jvm 中如何判断两个对象是属于同一个类?
-
类的全类名(地址)完全一致.
-
类的加载器必须相同.
三、JVM运行时数据区
3.1 程序计数器(Program Counter Register):
程序计数器(Program Counter Register)是一块较小的内存空间,它可以 看作是当前线程所执行的字节码的行号指示器
3.2 Java 虚拟机栈(Java Virtual Machine Stacks):
描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个 线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出 口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟 机栈中入栈到出栈的过程。
3.3 本地方法栈(Native Method Stack):
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地 方法栈是为虚拟机调用 Native 方法服务的。
3.4 Java 堆(Java Heap):
是 Java 虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时 候创建,Java 堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这 里分配内存,随着 JIT 编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、 标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐 渐变得不那么“绝对”了。
3.5 方法区(Methed Area) :
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等 数据。 内存区域是很重要的系统资源,是硬盘和 CPU 的中间桥梁,承载着操作系统和应 用程序的实时运行.JVM 内存布局规定了 Java 在运行过程中内存申请,分配,管理 的策略,保证了 JVM 的高效稳定运行.不同的 JVM 对于内存的划分方式和管理机 制存在着部分差异,
3.6 程序计数器的作用:
程序计数器用来存储下一条指令的地址,也即将要执行的指令代码.由执行引擎读 取下一条指令.
-
它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域.
-
在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与 线程生命周期保持一致.
-
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法.程序计数器会存储当前线程正在执行的Java方法的JVM指令地址.如果是在执行native 方法,则是未指定值(undefined).
-
它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需 要依赖这个计数器来完成.
-
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的 字节码指令.
-
它是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.
作用位置:
3.7 面试题:
1. 使用程序计数器存储字节码指令地址有什么用?为什么使用程序计数器记录当前线程的执行地址呢?
因为 CPU 需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪儿 开始继续执行. JVM 的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什 么样的字节码指令.
2.程序计数器为什么被设定为线程私有的.
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的 方法,CPU 会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢? 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然 是为每一个线程都分配一个程序计数器,这样一来各个线程之间便可以独立计算, 从而不相互干扰.
3.8 栈的特点:
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器. JVM 直接对 java 栈的操作只有两个:调用方法,进栈. 执行结束后出栈.对于栈来说不存在垃圾回收问题.
3.9 栈中存储什么?
每个线程都有自己的栈,栈中的数据都以栈帧为单位存储. 在这个线程上正在执行的每个方法都各自对应一个栈帧. 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息.
3.10 栈的运行原理:
JVM 直接对 java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循”先进后 出”/后进先出的原则.
1.在一条活动的线程中,一个时间点上,只会有一个活动栈.即只有当前在执行的方 法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈(Current Frame),与当前栈 帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前 类(Current Class).
2.执行引擎运行的所有字节码指令只针对当前栈帧进行操作.
3.如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶 端,成为新的当前栈帧
不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中 引用另一个线程的栈帧(方法).
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧.
Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是 抛出异常.不管哪种方式,都会导致栈帧被弹出
3.11 栈内部结构:
3.12 面试题:
什么情况下会出现栈溢出(StackOverflowError)?
栈溢出就是方法执行时创建的栈帧超过了栈的深度。那么最有可能的就是方法递归调
用产生这种结果。
通过调整栈大小,就能保证不出现溢出吗?
不能
分配的栈内存越大越好吗?
并不是的,只能延缓这种现象的出现,可能会影响其他内存空间
垃圾回收机制是否会涉及到虚拟机栈?
不会
3.13 堆内存概述:
一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域. Java 堆区在 JVM 启动时的时候即被创建,其空间大小也就确定了,是 JVM 管理 的最大一块内存空间.
堆内存的大小是可以调节.
例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小)
一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重 新分配大小的次数,提高效率.
《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它 应该被视为连续的.
所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区.《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分 配在堆上.
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.
堆,是 GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.
四 、垃圾回收相关算法
6.1 引用计数算法
1.引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型 的引用计数器属性。用于记录对象被引用的情况。
2.对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1; 当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,即表示 对象 A 不可能再被使用,可进行回收。
3.优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
4.缺点:
1.它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2.每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3.引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命 缺陷,导致在.Java 的垃圾回收器中没有使用这类算法。
6.2 可达性分析算法
可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集
1.相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效 等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题, 防止内存泄漏的发生。
2.相较于引用计数算法,这里的可达性分析就是 Java、C#选择的。这种类型的 垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)
可达性分析实现思路
所谓"GCRoots”根集合就是一组必须活跃的引用
其基本思路如下:
1.可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式
搜索被根对象集合所连接的目标对象是否可达。
2.使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接 着,搜索所走过的路径称为引用链(Reference Chain)3.如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡, 可以标记为垃圾对象。
4.在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存 活对象
总结
1.简单一句话就是,除了堆空间的周边,比如:虚拟机栈、本地方法栈、方法区、 字符串常量池等地方对堆空间进行引用的,都可以作为 GC Roots 进行可达性分 析。
2.除了这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前 回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整 GC Roots 集合。比如:分代收集和局部回收。
小技巧
由于 Root 采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里 面的对象,但是自己又不存放在堆内存里面,那它就是一个 Root。
注意
的对象才是存 活对象
总结
1.简单一句话就是,除了堆空间的周边,比如:虚拟机栈、本地方法栈、方法区、 字符串常量池等地方对堆空间进行引用的,都可以作为 GC Roots 进行可达性分 析。
2.除了这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前 回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整 GC Roots 集合。比如:分代收集和局部回收。
小技巧
由于 Root 采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里 面的对象,但是自己又不存放在堆内存里面,那它就是一个 Root。
注意
如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能 保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。 这点也是导致 GC 进行时必须“Stop The World”的一个重要原因。即使是号 称(几乎)不会发生停顿的 CMS 收集器中,枚举根节点时也是必须要停顿的。