JVM
JVM 是 Java Virtual Machine( Java 虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
类装载器(ClassLoader)子系统,运行时数据区和执行引擎
引入 Java 语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java 语言使用 Java 虚拟机屏蔽了与具体平台相关的信息,使得Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
(百度百科:https://baike.baidu.com/item/JVM/2902369?fr=aladdin)
JVM内存
Gabage Collection 垃圾收集 ,java 提供垃圾 GC 很大程度上解决了 C++ 程序员管理内存的问题, 对于每个版本的 JDK , JVM 内存结构有些许不同之处 ,但是总体上还是分为下面几块:
1、方法区
线程共享的内存区域。用于存储被虚拟机加载的类的信息、常量、静态变量、即时编译器编译出的代码数据。 很多人也称该区域为永久代(并不等价)。不同于堆,它除了不需要连续的内存和可以选择固定大小或者可以扩展外,还可以不需要实现垃圾回收。
运行时常量池 是 方法区的一部分,class文件除了有类的版本字段和方法接口等描述外,还有一些信息是常量池用于存放编译期生成的各种字面量和符号引用,这些东西将在类被加载后放置到方法区的运行时常量池中。
2、虚拟机栈
线程私有,生命周期与线程相同。描述的是 Java 方法执行的内存模型:每个方 法在执行时会创建一个栈帧用于存储局部变量表(存放编译期可知的各种基本数据类型、对象引用类型)、操作数栈、动态链接、方法出口等信息。
3、本地方法栈
类似虚拟机栈,为虚拟机使用到的 native 方法服务,(有的虚拟机会把本地方法栈与虚拟机方法栈合二为一 例如 : Sun HotSpot )
4、堆
Java 堆 是虚拟机所管理内存中最大的一块儿。它被所有线程共享,再虚拟机启动的时候创建。 唯一目的是存放对象实例(不是很绝对)。
这个区域是垃圾收集器管理的主要区域,因此被叫做 “ GC 堆 ” 也是常见的。由于现在大的垃圾收集器基本都采用分代收集法,所以 java 堆还可以细分为 : 新生代和老年代;再细分的话就有 Eden 区和 From Survivor 空间和 To Survivor 空间 。
方法区中的常量池只能存放基本数据类型以及String类型
当前主流虚拟机通过 -Xmx 和 -Xms 控制堆的大小
5、程序计数器 (占用空间很小)
可以被看作当前线程所执行的字节码的行号指示器。
6、直接内存(本机直接内存)
它不是 JVM 运行时数据区的一部分,也不是虚拟机定义的内存区域。JDK 1.4 中新加入的 NIO 类 可以使用 Native 函数库直接分配到堆外内存。这避免了堆与 Native 堆来回复制数据。
类加载器、创建对象
创建对象的话,一般我们会先创建类。那么我们创建的类使如何被虚拟机加载到内存中的呢?
一个类从被加载到 JVM 内存开始,到被卸载结束为止,只是一个类的生命周期。会有七个阶段:
加载、(验证、准备、解析)【统称为 连接】、初始化、使用、卸载。
什么是类的加载: 通过一个类的完全限定名来获取描述此类的二进制字节流
什么是类的加载器: 实现加载过程的模块就叫做类的加载器
类的加载对于 JVM 来说只有两种不同的类加载器:
一种是 启动类加载器;另一种是:其他类加载器。然而绝大部分类的加载都会用到以下三种类加载器:
1、启动类加载器:
加载 Java/bin 路径下的或被 -XbootClassPath 参数指定的路径中并且被虚拟机识别的(文件)类库到 JVM 内存中。 该类加载器不能被 Java 程序直接引用,若要把加载请求委派给引导类加载器,就直接使用 null 代替即可。
2、扩展类加载器:
负责加载 <JAVA_HOME>/lib/ext 目录中或者被 Java.ext.dirs 系统变量指定的路径中的所有类库。 这个类加载器可以直接被开发者使用。
3、应用程序类加载器:
用于加载用户类路径(ClassPath) 上所指定的类库,可直接使用。如果程序上没有自定义的类加载器,一般情况下就是程序默认的加载器。
程序一般是由这三种类加载器相互配合进行加载的,如果有必要可以加入自定义的类加载器。
它们之间的关系使一层一层继承的,除了最上层启动类加载器无父类之外,其余的类加载器都存在父类,而且当一个类需要加载时,该类的类加载器自己不会执行类加载过程,而是向上给它的父类,所以一般加载类用的都是 启动类加载器,只有父类返回不能加载时,该加载器才会尝试去加载类。
破坏双亲委派模型有3种情况(这里不作具体说明)。
类的连接、解析阶段不做详细介绍。
遇到以下五种情况必须立即对其进行初始化:
1、 new getstatic 、putstatic 、invokestatic 关键字
2、使用 java.lang.reflect 包的方法对类进行反射调用
3、当一个类初始化的时候,发现它的父类还没有进行初始化,先对其父类进行初始化
4、JVM 启动时,指定一个要执行的主类,先初始化这个主类
5、如果 java.lang.invoke.methodHandle 实例最后的解析结果 REF_static REF_putstatic REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行初始化,需要先进行初始化。
在 Java 中创建对象我们一般都会使用 new 关键字。
当 JVM 遇到 new 指令的时候:
1、他会去检查这个类的符号是否被加载 、解析、初始化。
2、如果没有有一个加载类的过程。
3、分配内存
4、JVM 初始化内存空间 (若存在 TLAB , 则会提前到 TLAB 分配时同时操作,目的在于使对象不赋初值也能被使用(默认值))
5、对对象有必要的设置
6、执行< init > (加载对象) 操作
GC
Gabage Collection 垃圾收集
都知道它的作用,回收内存垃圾,但是哪些内存需要回收,什么时候回收,如何回收是基本问题。
1、思路
在堆里存放的几乎全是被实例化的对象,垃圾回收前第一件事就是判断这些对象中哪些还在被引用,哪些没有任何作用了。
2、引用计数器法
“该对象的计数器为0,被引用+1, 失效-1” 当任何时刻 计数器为0的化就被回收。理论上是可以,但是很难解决两个对象之间循环相互引用的问题。
3、可达性分析算法
这个算法在现在也是比较主流的。算法基本思想为:
通过一系列的成为 ” GC Roots “ 的对象作为起点,开始向下搜索,所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链的时候,证明对象不在被引用。 可作为 GC Roots 的对象的有 JVM 栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 Native 引用的对象。
4、引用
如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
引用在 JDK 1.2 之后分为 强引用(存在引用)、 软引用(还有用,但非必须,列入回收范围)、 弱引用(非必须对象 生存到下一次垃圾回收之前)、 虚引用(最弱的引用,无法通过虚引用获取一个对象的实例)4种,表示引用强度依次逐渐降低。
5、finalize()方法
真正被 GC 回收会进行两次标记过程:如果对象在进行可达性分析之后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,条件就是该对象有无必要执行 finalize() 方法 。当对象未覆盖 finalize() 方法 或者finalize() 方法已经被 JVM 调用过,虚拟机视这两种情况为“没必要执行”。
如果一个对象别判断有必要执行 finalize() 方法,那么这个对象就会被放置在一个 F-Queue 的队列中, JVM 会自动建立一个低级优先的 Finalizer 线程去执行 finalize() 方法,不一定等它执行结束,否则有可能造成 其他对象在 F-Queue 中永久等待。
finalize() 方法 是对象逃亡的最后一次机会,GC 会对 F-Queue 中的对象进行第二次标记,如果有对象在这个期间有被引用,或者赋值给某个变量,那么在第二次标记的时候会把它移除 F-Queue ,如果这个时候没有被用到,那么该对象就真的被回收了。
6、回收方法区(HotSpot 虚拟机的 永久代)
很多人认为方法区是没有垃圾收集的,在堆中,一次垃圾收集能回收70%~95% 的空间,而 方法区效率远低于此。
该区域回收两部分内容:
1、废弃的常量
回收方法与 Java 堆 回收对象类似
2、无用的类
判断类是否是无用的类,一般要满足下面三个条件,满足这三个条件的类可以被回收(是可以):
(1) 该类的所有实例都已经被回收,也就是 Java 堆中不存在该类的对象
(2) 加载该类的 ClassLoader 已经被回收
(3) 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射来访问该类的方法。
7、算法
(1) 标记清除
1、效率低
2、内存中产生大量不连续的碎片
(2)复制
将堆内存分为相等大小的两块,每次只对一块进行清理,清理后 复制到另外一块,再对第一块进行全部清理。每次只对半个堆内存进行回收。代价将内存变为原来的一半。
(3) 标记-整理算法(老年代)
标记-清除 算法后 ,让存活的对象都移动到内存的一边,可以有效地避免空间碎片。
(4)分代收集算法
根据对象的存活周期,将内存分为几块,一般是 新生代和老年代。
每次 GC 后都有大批对象死去,就用复制算法来复制少量存活的。
8、安全点 安全区域
类加载完成时,HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来,在 JIT 编译的过程中,也会在特定的位置记录下栈和寄存器中哪些位置的引用。这里的特定位置就是安全点。
程序执行时并非在所有的地方都能停顿来让 GC 进行,而是在到达安全点时才能暂停。
JVM 是如何判断一个对象是否持有被引用呢?
(查阅 Exact VM 对 Classic VM 的改进)
在 OopMap 的帮助, JVM 快速且准确的完成 GC Roots 枚举 ,安全点的选定不能太少以至于 GC 等待时间太长,也不能太频繁,以至于增大运行符合。这里有两个方案让所有线程都跑到最近的安全点停顿下来:
抢先式中断 和 主动式中断(常用)
如果在 GC 时, 有些安全点在短时间内可能进入 GC 回收范围,
安全区域 是指在一段代码中引用关系不会发生变化,这个区域任意地方开始 GC 都是安全的,相当于被扩展的安全点。
垃圾收集器
Serial 收集器
Parnew 收集器
Parallel Scavenge 收集
Serial Old 收集器
Parallel Old 收集器
CMS 收集器
G1 收集器 (广泛使用)
部分参考:《 深入理解 JAVA 虚拟机 》( 作者 : 周志明 )