JVM
类加载机制
JVM处于机器的哪个位置?
JVM运行在操作系统之上,实际运行在OS之上的就是我们常说的JRE,而JRE中就包含了JVM。
OS运行在硬件体系之上,
JVM体系结构
Java Stack 和 Native Method Stack 和 PC 不会存在垃圾回收
类加载器:加载、链接、初始化
JVM调优几乎是在堆及方法区实现的
类加载器
用于加载 Class 文件
加载器包括:
- JVM自带的加载器
- 引导(启动)类(根)加载器(在JRE中lib的rt.jar包中)
- 扩展类加载器(在JRE中lib的ext中的jar包中)
- 系统类加载器(应用程序加载器)
- 用户自定义的类
双亲委派机制
保证安全性
当加载器要将.class文件加载类到JVM时,采用双亲委派机制,即
-
类加载器收到类加载请求后,不会先自己进行加载,而是把该请求委托给父类加载器进行加载,如果父类加载器还存在加载器,再委托给其父类加载器,当达到最顶层的类加载器时,如果该加载器可以完成类加载,则成功返回,否则该类加载器的子类加载器尝试进行类加载。
-
如果父类已经加载过该类,则直接返回,无需重复加载
沙箱安全机制
保证JVM的安全
沙箱是一个限制程序运行的环境。
沙箱机制就是将Java代码程序限制在JVM特定的运行环境中。
Java将执行程序分为本地代码和远程代码,默认本地代码是安全的,而远程代码不一定是安全的。
Java沙箱的基本组件
-
字节码校验器
-
类加载器
-
存取控制器
-
安全管理器
-
安全软件包
沙箱的元素
- 权限
- 代码源
- 保护域
- 策略文件
- 密钥库
内存结构
本地方法栈
native
native
- 使用该关键字的函数都是Java无法触及的范围,需要调用底层C/C++的库
- 使用该关键字会进入本地方法栈,调用JNI
- JNI:Java本地方法接口
- 扩展Java,使得不同语言可以在Java中使用
- JNI需要去调用本地方法库
- JNI:Java本地方法接口
本地方法栈
-
用于管理本地方法的调用
-
登记 native 方法,在执行引擎执行时加载本地方法库
方法区
方法区
- 方法区是线程共享的,当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待;
- 方法区的大小是不固定的,可以动态调整
- 方法区存在GC
- 保存的信息:
- 类信息
- 类型信息
- 静态变量
- 常量
- 类型的常量池
- 每一个Class文件中,都维护着一个常量池(这个保存在类文件里面,不要与方法区的运行时常量池搞混),里面存放着编译时期生成的各种字面值和符号引用;这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 ;
- 类信息
程序计数器
程序计数器
-
程序计数器是记录着当前线程所执行的字节码的行号指示器。
-
每个线程都有一个程序计数器,是线程私有的
-
PC的值对应该线程执行字节码指令的地址(可以认为PC是一个指针)
-
程序计数器占用内存空间很小,几乎可以忽略不计
Java栈
Java栈
运行原理:栈帧
Java栈的相关概念
- Java栈是线程私有的,生命周期与线程相同
- Java栈描述的是Java方法执行的内存模型
- 每个方法执行时会创建一个栈帧存入栈中,其中栈内存,即局部变量表是需要关注的重点
栈帧
-
栈帧是用于JVM进行方法调用与执行的数据结构,是虚拟机运行时数据区中的Java栈的栈元素
-
栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等
-
局部变量表存储了八大基本类型、对象引用及returnAddress类型(值为指向了一条字节码指令的地址
堆
堆 Heap
一个Java虚拟机只存在一个堆,是线程共享的,且堆内存是可动态调节的
默认情况下,JVM分配的最大内存是宿主机的1/4,初始内存是宿主机的1/64
JDK1.8以前,堆包括:
- 新生带(年轻代)
- 老年代(养老区)
- 永久代
JDK1.8以后,堆包括:
- 新生带(年轻代)
- 老年代(养老区)
JDK 1.8 以后,永久代被元空间取消了,而元空间采用的是本地内存,而不是虚拟内存
新生带
该区域存放的内存对象是新创建的对象或者刚创建不久的对象,新生带分为:
-
伊甸区
大多数新建对象处于 Eden 区,该区域被填充时,执行 Minor GC,并将没有被GC的对象移动到幸存区中
-
幸存0区
-
幸存1区
默认情况下,三者的比例为:
8
:
1
:
1
8:1:1
8:1:1,可以通过 -XX:SurvivorRatio
配置
新生带在填充的时候,会执行垃圾回收,称为Minor GC,在 Eden 区执行 Minor GC 时,会将幸存的对象移动到另一个幸存者空间,故总有一个幸存区是空的
多次 Minor GC 仍然存活下来的对象会被移动到老年带
老年代
该区域存放着一些“顽固分子”,通常在此区域的垃圾回收都是在此区域内存满的时候进行,该区域的GC称为 Major GC(主GC),耗费时间相比Minor GC更长
需要大量连续内存空间的对象(大对象)一般不进入新生带,因为Eden区和两个幸存区之间存在内存拷贝
永久代(元空间)
只有在 HotSpot 中才有永久代的概念说法
该区域也称为非堆*(Non-Heap),当JVM关闭时,该区域的内存才会被释放
元空间:使用的是计算机物理内存(本地内存),非使用JVM的内存
堆内存调优
通过 IDE 设置 JVM 的起始堆内存和最大堆内存,参数分别为:-Xms
和 -Xmx
-Xms
:设置初始堆内存大小;-Xmx
:设置最大堆内存大小
这两个参数的值需要带单位
通过 -XX+PrintGCDetails
可以查看GC详情
TLAB
TLAB:Thread Local Allocation Buffer
- 对 Eden 区域进行划分,JVM为每个线程分配一个私有缓存区域在 Eden 区重
- 使用 TLAB 可以保证多线程问题时的线程安全,且提高内存分配的吞吐量(通常情况下对象是分配在堆上的,因为堆是线程共享的,所以同一时间可能会有很多线程申请空间分配,在这种情况下一般要加锁处理,加锁便会造成分配效率下降。而TLAB是每个线程独有的,它可以避免这种开销,直接分配空间,效率等同于C语言)
- OpenJDK 衍生的 JVM 提供了 TLAB 设计
通过 -XX:UseTLAB
可以设置 TLAB 的开启,通过 -XX:TLABWasteTargetPercent
设置 TLAB 空间所占伊甸园区的大小比例
逃逸分析
逃逸分析:JVM中的一种优化技术
- 当一个对象在方法中定义后,不会被外部方法引用,则认为不会发生逃逸
- 当一个对象在方法中定义后,被外部方法引用,则认为发生逃逸
HotSpot 在 JDK7 后就默认开启了逃逸分析,也可以通过 -XX:DoEscapeAnalysis
开启
逃逸分析开启后,编译器会做如下代码优化:
-
栈上分配
不会发生逃逸则使用栈上分配
-
同步省略
对象值能从一个线程被访问到则使用同步省略
-
分离对象/标量替换
垃圾回收机制
对象回收的判断
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,也就是需要回收的对象。
存在问题:循环引用问题会导致对象无法被回收,故JVM 不采用引用计数算法
可达性分析算法
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
在Java语言中,可以作为GC Roots的对象包括以下几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即本地方法接口)引用的对象
如果对象在进行可达性分析之后被发现没有与 GC Roots 相连的引用链,那么它将会被第一次标记,并且进行一次筛选,筛选的条件就是此对象是否有必要执行 finalize()
方法
两次标记过程:
- 第一次标记: 如果对象在进行可达性分析后发现没有 GC Roots 相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行
finalize()
方法。当对象没有覆盖finalize()
方法,或者finalize()
方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行,此时会直接回收该对象;如果对象被判定为有必要执行,则会被放到一个 F-Queue 队列。 - 第二次标记:
finalize()
方法是对象跳脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中对象进行第二次小规模标记,如果对象要在finalize()中重新拯救自己:只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时她将被移出即将回收的集合。
垃圾回收算法
复制
作用场景:存放存活率低,占用内存空间小的对象的区域
按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
JVM 中如何使用?
作用区域:年轻代中的 Eden 区和幸存区
目前的商用 JVM 普遍采用复制回收算法来回收年轻代,但并不是将年轻代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。
缺点:内存使用率较低
标记-清除
该算法分为两个阶段,标记和清除,标记无用对象,然后进行清除回收
缺点:效率不高,内存碎片严重化
标记-整理
记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存
缺点:效率不高
分代收集
分代收集算法,根据对象的存活周期将内存划分为几块,按照块来采用不同的 GC 算法。
一般包括年轻代、老年代。
根据对象存活周期的不同将内存划分为几块,一般是年轻代和老年代,新生代基本采用复制算法,老年代采用标记-清除/标记-整理算法。
当前商业虚拟机都采用分代收集的垃圾收集算法。