# JVM
# 1. JVM 在系统中的位置
# 2. JVM 体系结构图
其中 Java 栈、本地方法栈、程序计数器一定不会涉及垃圾回收;垃圾回收一般在方法区和堆中,所以所谓的 JVM 的调优,其实就是在调优这两个区域,而且大多数情况下是调优堆
# 3. 类加载器 ClassLoader
在如下几种情况下,Java 虚拟机将结束生命周期:
(1)执行了 System.exit() 方法
(2)程序正常执行结束
(3)程序在执行过程中遇到了异常或者错误而异常终止
(4)由于操作系统出现错误而导致 Java 虚拟机进行终止
类的加载、连接与初始化
在 Java 代码中, Class 的加载、连接与初始化过程都是在程序运行期间完成的,即 Runtime
- 加载:查找并加载类的二进制数据
- 连接:
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转换为直接引用。在编译的时候每个 Java 类都会被编译成一个 class 文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在这个解析阶段就是为了把符号引用转换成为真正的地址的阶段
- 初始化:为类的静态变量赋予正确地初始值
class Test {
/**
* 1. 加载阶段
* 编译文件为 .class 文件,然后通过类加载,加载到 JVM
*
* 2. 连接阶段
* (1) 验证:确保 class 类文件没问题
* (2) 准备:先初始化为 a = 0,因为 int 类型的初始值为 0
* (3) 解析:将引用转换为直接引用
*
* 3. 初始化阶段
* 通过此解析阶段,把 1 赋值给变量 a
*/
public static int a = 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
类的加载
类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区内的方法区内,然后在内存中创建一个 java.lang.class
对象用来封装类在方法区内的数据结构
ClassLoader 分类
有两种类型的类加载器
- Java 虚拟机自带的加载器
- 根类加载器(BootStrap、BootClassLoader)
sun.boot.class.path
:加载系统的包,包含 JDK 核心库里的类 - 扩展类加载器(Extension、ExtClassLoader)
java.ext.dirs
:加载扩展 jar 包中的类 - 系统(应用)类加载器(System、AppClassLoader)
java.class.path
:加载你编写的类,编译后的类
- 用户自定义的类加载器
java.long.ClassLoader
的子类可以定制类的加载方式
双亲委派机制
双亲委派机制的工作原理:一层一层的让父类去加载,最顶层的父类没有就往下依次加载
(1)类加载器收到类加载的请求
(2)把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器
(3)启动器加载器检查能不能加载(使用 findClass() 方法),能就加载,然后结束;否则抛出异常,通知子加载器进行加载
沙箱安全机制
Java 安全模型的核心就是 Java 沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的ava程序运行都可以指定沙箱,可以定制安全策略。
在 Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的 Java 实现中,安全依赖于沙箱(Sandbox)机制。如下图所示JDK1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示 JDK1.1 安全模型
在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示 JDK1.2 安全模型
当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示最新的安全模型(jdk1.6)
# 4. Native 方法
/**
* native:凡是带了 native 关键字的,说明 Java 的作用范围达不到了,会去调用底层 C 语言* 的库,会进入本地方法栈,调用本地方法本地接口(JNI)
* JNI 的作用:扩展 Java 的使用,融合不同的编程语言为 Java 所用
* 它在内存区域中专门开辟了一块标记区域:Native Method Stack,登记 native 方法,在最终执行的时候,加载本地方法库中方法通过 JNI
*/
private native void start0();
2
3
4
5
6
# 5. 程序计数器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成,是一个非常小的内存空间,几乎可以忽略不计
# 6. 方法区
Method Area 方法区是 Java 虚拟机规范中定义的运行时数据区域之一,它与堆一样在线程之间共享
Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来
JDK7 之前(永久代)用于存储已被虚拟机加载的类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据,每当一个类初次被加载的时候,它的元数据都会被放到永久代中,永久代大小有限制,如果加载的类太多,很可能导致永久代内存溢出,即 java.lang.OutOfMemoryError:PermGen
JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据迁移至 Java Heap(堆)或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)
元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
如果 Metaspace 的空间占用达到了设定的最大值,那么就会触发 GC 来收集死亡对象和类的加载器
# 7. 栈
栈:先进后出
队列:先进先出
栈:栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题
栈存放 8 大基本类型 + 对象引用 + 实例的方法
栈的优势是:存取速度比堆要快,仅次于寄存器,栈数据可以共享
栈的运行原理
Java 栈的组成元素 - 栈帧
栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构,它是独立于线程的,一个线程有自己的一个栈帧,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息
一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,A 方法又调用了 B 方法,于是产生了栈帧 F2 也被压入栈中,B 方法又调用了 C 方法,于是产生栈帧 F3 也被压入栈中,执行完毕后,先弹出 F3,然后弹出 F2,再弹出 F1
# 8. 堆
Java7 之前,一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
- 新生区
- 养老区
- 永久区
堆内存逻辑上分为三部分:新生、养老、永久(JDK8 以后叫元空间)
GC 垃圾回收主要是在新生区和养老区,又分为轻 GC 和重 GC,如果内存不够,或者存在死循环,就会导致 java.lang.OutOfMemoryError: Java heap space
,即 OOM
新生区
新生区是类诞生、成长、消亡的区域,一个类在这里产生、应用,最后被垃圾回收器收集,结束生命
新生区又分为两部分:伊甸区和幸存者区,所有的类都是在伊甸区被 new 出来的,幸存区有两个:0 区和 1 区,当伊甸区的空间用完时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸区中幸存下来的对象移动到幸存 0 区,若幸存 0 区也满了,再对该区进行垃圾回收,然后移动到 1 区,如果 1 区也满了,再移动到养老区,若养老区也满了,那么这个时候将产生 MajorGC(Full GC),进行养老区的内存清理,若养老区执行了 Full GC 后发现依然无法进行对象的保存,就会产生 OOM 异常
如果出现 OOM 异常,说明 Java 虚拟机的堆内存不够,原因如下:
- Java 虚拟机的堆内存设置不够,可以通过参数
-Xms(初始值大小)
、-Xmx(最大大小)
来调整 - 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环
HotSpot内存管理
分代管理,不同的区域使用不同的算法:
经过研究,不同对象的生命周期不同,在 Java 中很多的对象都是临时对象
永久区(Perm)
永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class、Interface 的元数据,也就是说它存储的是运行环境必须得类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存
如果出现 java.lang.OutOfMemoryError: PermGen space
,说明是 Java 虚拟机对永久代 Perm 内存设置不够,一般出现这种情况,都是因为程序启动需要加载大量的第三方 jar 包,例如:在一个 Tomcat 下部署了太多的应用,或者大量动态反射生成的类不断被加载,最终导致 Perm 区被占满
注意:
- JDK1.6 之前:有永久代,常量池在方法区
- JDK1.7:有永久代,但是已经逐步去永久代,常量池在堆
- JDK1.8 及以后:无永久代,常量池在元空间
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息 + 普通常量 + 静态变量 + 编译器编译后的代码,虽然 JVM 规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名,叫做 Non-Heap(非堆),目的就是要和堆分开
对于 HotSpot 虚拟机,很多开发者习惯将方法区称之为永久代(Parmanent Gen),但严格本质上说两者不同,或者说使用永久代实现方法区而已,永久代是方法区的一个实现,JDK1.7 的版本中,已经将原本放在永久代的字符串常量池移走
常量池(Constant Pool)是方法区的一部分,Class 文件除了有类的版本、字段、方法、接口描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放
# 9. Dump 内存快照
在运行 Java 程序的时候,有时候想测试运行时占用内存的情况,这时候就需要使用测试工具查看了,在 Eclipse 里面有 MAT 插件可以测试,而在 IDEA 中也有这么一个插件时 JProfiler,一款性能瓶颈分析工具
作用:
- 分析 Dump 文件,快速定位内存泄漏
- 获得堆中对象的统计数据
- 获得对象相互引用的关系
- 采用树形展现对象间相互引用的情况
- ......
# 10. GC
JVM 在进行 GC 时,大部分时候回收的是新生区,因此 GC 按照回收的区域又分为两种类型,一种是普通的 GC(minor GC),一种是全局 GC(major GC、Full GC)
普通 GC:只针对新生区域的 GC
全局 GC:针对养老区的 GC,偶尔伴随对新生代的 GC 以及对永久代的 GC
- GC 四大算法:引用计数法
每个对象有一个引用计数器,当对象被引用一次则计数器加 1,当对象引用失效一次,则计数器减 1,对于计数器为 0 的对象意味着是垃圾对象,可以被 GC 回收
缺点:每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗,JVM 的实现一般不采用这种方式
- 复制算法
新生区中的 Minor GC,采用的就是复制算法
Minor GC 会把伊甸区中的所有活的对象都移到幸存区中,如果幸存区中放不下,那么剩下的活的对象就被移动到养老区,也就是说,一旦 GC 后,伊甸区就是变成空的了
这里把幸存 0 区和幸存 1 区看作是幸存 from 区和幸存 to 区,在 GC 开始的时候,对象只会在伊甸区和幸存 from 区,幸存 to 区是空的,紧接着进行 GC,伊甸区中所有存活的对象都会被复制到 to 区,而在 from 区中仍存活的对象会根据它们的年龄值来决定去向,年龄达到一定值的对象会被移动到老年代中,没有达到阈值的对象会被复制到 to 区,经过这次 GC 后,伊甸区和 from 区已经被清空,这个时候 from 和 to 区会交换它们的角色,即新的 to 区是 GC 前的 from 区,新的 from 就是上次 GC 前的 to 区,不管怎样,都会保证名为 to 的区是空的,Minor GC 会一直重复这样的过程,直到 from 区被填满,from 区被填满之后,会将所有的对象移动到老年代中
当对象在伊甸区和 from 区,经过一次 Minor GC 后,如果对象还存活,并且 to 区有足够的内存空间来存储伊甸区和 from 区中存活的对象,则使用复制算法将这些仍然存活的对象复制到 to 区,然后清理使用过的伊甸区和 from 区,并且将这些对象的年龄设置为 1,然后 to 区变成 from 区,from 区变为 to 区,保证 to 区是空的,以后对象在幸存区每熬过一次 Minor GC,就将这个对象的年龄加 1,当这个对象的年龄达到某一个值时(默认是 15 岁,通过 -XX:MaxTenuringThreshold 设定参数
),这些对象就会成为老年代
复制算法劣势:
- 浪费了一半的内存
- 如果对象的存活率很高,假设是 100% 存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍,复制这一工作所花费的时间,在对象存活率达到一定程度时将会变得不可忽视,所以复制算法要想使用,最起码对象的存活率要非常低才行,而且要克服 50% 的内存浪费
- 标记清除算法
标记清除算法指在回收时对需要存活的对象进行标记,不被标记的对象将被回收
- 标记压缩算法
在整理压缩阶段,不再对标记的对象做回收,而是通过所有存活对象都向一端移动,然后直接清除边界以外的内存,可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉,如此一来,当我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可
- 标记清除压缩算法
标记清除压缩算法是标记清除算法和标记压缩算法的结合,当经过多次标记清除算法后会有大量内存碎片时才进行压缩
- 总结
内存效率:复制算法 > 标记清除算法 > 标记压缩算法
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
可以看出没有最优的算法,只有合适的算法:分代收集算法
年轻代:年轻代特点是区域相对老年代较小,对象存活率低,所以使用复制算法
老年代:老年代的特点是区域较大,对象存活率高,所以可以使用标记清除压缩算法
# 11. JMM
JMM:Java Memory Model 的缩写