jvm运行时内存的划分
JVM运行时的内存区域划分
- 程序计数器: 较小的内存空间,可以看到做当前线程所执行的字节码的信号指示器。线程私有,如果一个线程正在执行的是本地(Native)方法,这个计数器值则为空(Undefined)。唯一一个不会出现OutOfMemoryError的区域。
- java虚拟机栈: 线程私有,生命周期与线程相同
- 本地方法栈: 线程私有,为虚拟机使用到的本地(Native)方法服务
- java堆: 共享区域,虚拟机所管理内存最大的一块。java几乎所有的对象实例都在这里分配内存。
方法区: 共享区域,用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等
ASM,Unsafe,CAS的简单了解
ASM你指的是那个字节码工程包吗?是的话那就是一个提供了字节码抽象的工具,允许用Java代码来生成或者更改字节码。JDK里也会用到ASM,用来生成一些适配器什么的。我印象中代码覆盖工具JaCoCo也是用ASM来实现的。
Unsafe就是一些不被虚拟机控制的内存操作的合集。
CAS可以理解为原子性的写操作,这个概念来自于底层CPU指令。Unsafe提供了一些cas的Java接口,在即时编译器中我们会将对这些接口的调用替换成具体的CPU指令。垃圾收集器与内存分配策略
什么是垃圾
简单一句话就是没有被引用的对象就可以判定为垃圾
判定垃圾的过程需要经历两次标记过程,第一次是根可达分析完之后,发现没有GC Roots相连接的引用链,就会被标记一次,然后进行一次筛选出有必要执行finalize()方法的对象,将其放置在一个名为F-Queue的队列之中,并在稍后由一条续集你自动建立的、地调度优先级的Finalizer线程中取执行它们的finalize()方法。finialize()方法是对象逃脱死亡的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模标记,如果此时该对象重新有了引用,就可以逃脱,否则就要真正被回收了。判定垃圾的方式
引用计数算法: java主流虚拟机不使用该方法,缺点是 比如A,B相互引用,计数永远为1,这种的就无法被判定为垃圾。
可达性分析算法: GC roots,根据引用关系向下搜索,搜索过程所走的路径称为“引用链”
GC Root 的root指什么?
在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NPE、OOM)等,还有系统类加载器
- 所有被同步锁(synchronized关键字)持有的对象
- 反映虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
经典垃圾收集器
- Serial收集器 : 作用于新生代,采用复制算法,缺点: 暂停所有用户线程(Stop The World (STW)), 客户端模式、微服务可以考虑使用,停顿时间可以控制在十几、几十毫秒。
- ParNew收集器 : 作用于新生代
- Paraller Scavenge收集器: 作用于新生代,jdk8默认收集器
又经常被称作为”吞吐量优先收集器”,基于标记-复制算法实现的收集器,主要适合在后台运算而不需要太多交互的分析任务。
吞吐量 = 运行用户代码时间/(运行代码用户时间+运行垃圾收集时间)
提供了两个参数用户精确控制吞吐量:
- -XX: MaxGCpauseMillis : 控制最大垃圾收集停顿时间
- -XX: GCTimeRatio: 直接设置吞吐量大小
另外提供了一个参数 -XX:+UseAdaptiveSizePolicy,当设置该参数时,垃圾收集会采用自适应的调节策略(GC Ergonomics),如果手工优化存在困难,是一个不错的选择。使用该参数时,就不需要指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX: PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量
自适应的调节策略也是与ParNew的一个重要特性。
- Serial Old收集器: 是Serial的老年代版单线程收集器,使用标记-整理算法。
- Parrllel Old收集器: 是Parallel Scavenge收集器的老年代版本, 支持多线程并发收集,基于标记-整理算法实现。 jdk8默认收集器
CMS收集器: 承上启下的收集器,基本被Oracle抛弃了,可以忽略不看
Full GC 和 内存碎片整理停顿时间长还能二选一,不完美。
Garbage First 收集器 : 简称G1。开创了收集器面向局部收集的设计思路路和基于Region的内存布局形式。
- Shenandoah收集器
- ZGC收集器
并行(Parallel)和并发(Concurrent)
并行: 描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
并发: 描述的是垃圾收集器线程与用户之间的关系,说明统一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未冻结,所以程序仍然能相应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。内存分配与回收策略
1、对象优先在Eden分配
-Xms20M -Xmx20M java堆的大小20M
-Xmn10M 新生代10M 老年代10M
-XX:SurvivorRatio=8 新生代Eden与一个Survivor区的空间比例8:1
2、大对象直接进入老年代
-XX:PretenureSizeThreshold=3145728 大于这个设置值的对象直接在老年代分配
目的:避免在Eden区和两个Survivor区直接发生大量的内存复制
3、长期存活的对象将进入老年代
-XX:MaxTenuringThreshold=1 对象晋升老年代的阈值(默认为15)
4、动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
5、空间分配担保
极限情况下Eden区的对象都存活,且Survivor无法装配下,这就需要老年代保证有一块连续的内存空间才可进行一次Minor GC。
调优
基础故障处理工具
- jps [options] [hostid] 虚拟机进程状况工具,用于监视虚拟机各种运行状态信息的命令行工具。
- jstat
通过此命令,可以看系统Full GC的情况和次数,发生过多Full GC以及耗时较长,就不能忍了
- jinfo:Java配置信息工具,试试查看和调整虚拟机各项参数
- jmap: 命令用于生成堆转储快照(dump文件)
jmap -histo pid | head -10(行数) 得到哪个类占用内存过高,基本可以定位方法
JVM大内存分析,不推荐jmap+jhat,推荐JProfiler
- jhat: 虚拟机堆转储快照分析工具
与jmap命令搭配使用,来分析jamp生成的堆转储快照
- jstack: Java堆栈跟踪工具
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合, 生成线程快照的目的通常是定位线程出现长时间停顿的原因, 如线程间死锁、 死循环、 请求外部资源导致的长时间挂起等, 都是导致线程长时间停顿的常见原因。 线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情, 或者等待着什么资源
top分析占用内存高的进程,得到pid
jmap -histo pid | head -10(行数) 得到哪个类占用内存过高,基本可以定位方法
dump 文件分析OOM
虚拟机执行子系统
魔数与Class文件的版本
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
紧接着魔数的4个字节存储的是Class文件的版本号: 第5和第6个字节是次版本号(MinorVersion) , 第7和第8个字节是主版本号(Major Version)
不仅是Class文件, 很多文件格式标准中都有使用魔数来进行身份识别的习惯, 譬如图片格式, 如GIF或者JPEG等在文件头中都存有魔数。 使用魔数而不是扩展名来进行识别主要是基于安全考虑, 因为文件扩展名可以随意改动。
虚拟机类加载机制
类的生命周期
- 加载
- 通过一个类的全限定名来获取此定义类的二进制字节流
- 将这个字节流所代表的的静态存储结构转化为方法区是运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 验证
- 文件格式验证: 是否符合Class文件格式的规范。比如是否以魔数0xCAFEBABE开头,主次版本号,常量池等
- 元数据验证: 对类的元数据信息进行语义校验。比如是否有父类,父类是否继承了不允许被继承的类,是否实现了抽象类或者接口中需要实现的所有方法,子父类字段,方法是否产生矛盾
- 字节码验证: 最复杂的阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证
- 准备
正式为类中定义的变量(即静态变量)分配内存并设置类变量的初始值的阶段
- 解析
- 类或者接口的解析
- 字段解析
- 方法解析
- 接口方法解析
- 初始化
- 使用
- 卸载
类加载器-双亲委派模型
双亲委派模型
双亲委派模型的工作过程是: 如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此, 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类) 时, 子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织类加载器之间的关系, 一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。 例如类java.lang.Object, 它存放在rt.jar之中, 无论哪一个类加载器要加载这个类, 最终都是委派给处于模型最顶端的启动类加载器进行加载, 因此Object类在程序的各种类加载器环境中都能够保证是同一个类。 反之, 如果没有使用双亲委派模型, 都由各个类加载器自行去加载的话, 如果用户自己也编写了一个名为java.lang.Object的类, 并放在程序的ClassPath中, 那系统中就会出现多个不同的Object类, Java类型体系中最基础的行为也就无从保证, 应用程序将会变得一片混乱。类立即初始化-有且只有六种
- 遇到new、 getstatic、 putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化, 则需要先触发其初始化阶段。 能够生成这四条指令的典型Java代码场景有:
- 使用new关键字实例化对象的时候。
- 读取或设置一个类型的静态字段(被final修饰、 已在编译期把结果放入常量池的静态字段除外)的时候。
- 调用一个类型的静态方法的时候。
- 使用java.lang.reflect包的方法对类型进行反射调用的时候, 如果类型没有进行过初始化, 则需要先触发其初始化。
- 当初始化类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化。
- 当虚拟机启动时, 用户需要指定一个要执行的主类( 包含main()方法的那个类) , 虚拟机会先初始化这个主类。
- 当使用JDK 7新加入的动态语言支持时, 如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、 REF_putStatic、 REF_invokeStatic、 REF_newInvokeSpecial四种类型的方法句柄, 并且这个方法句柄对应的类没有进行过初始化, 则需要先触发其初始化。
- 当一个接口中定义了JDK 8新加入的默认方法( 被default关键字修饰的接口方法) 时, 如果有这个接口的实现类发生了初始化, 那该接口要在其之前被初始化。
线程安全与锁优化
java内存模型
注意 java运行时内存和java内存模型的区别volatile - 两项特性
- 保证此变量对所有线程的可见性(保证不了一致性),但不能保证绝对的现成安全
因为java里的运算操作符并发原子操作,volatile变量的运算在并发下是不安全的。例子如下,每次运行结果都不一致:
public class VolatileStudy {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
for (int i = 0; i < THREADS_COUNT; i++) {
new Thread(()->{
for (int j = 0; j < 10000; j++) {
increase();
}
}).start();
}
//等待所有累加线程都结束
while (Thread.activeCount() > 1){
Thread.yield();
}
System.out.println(race);
}
}
由于volatile变量只能保证可见性, 在不符合以下两条规则的运算场景中, 我们仍然要通过加锁(使用synchronized、 java.util.concurrent中的锁或原子类) 来保证原子性
- 运算结果并不依赖变量的当前值, 或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束
- 禁止指令重排优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
例子: 双锁检测(Double Check Lock,DCL)
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
原子性、可见性与有序性
线程与进程
线程是比进程更轻量级的调度执行单位, 线程的引入, 可以把一个进程的资源分配和执行调度分开, 各个线程既可以共享进程资源(内存地址、 文件I/O等) , 又可以独立调度。 目前线程是Java里面进行处理器资源调度的最基本单位, 不过如果日后Loom项目能成功为Java引入纤程(Fiber) 的话, 可能就会改变这一点。
线程状态
线程安全的实现方法
- 互斥同步 :
基本手段 synchronized,ReetrantLock(重入锁)
ReetrantLock比synchronized主要增加了三个高级功能:
等待可中断:
公平锁: 性能急剧下降, 会明显影响吞吐量
锁绑定多个条件: 多次调用newCondition()方法即可
- 非阻塞同步
CAS :CAS指令需要有三个操作数, 分别是内存位置(在Java中可以简单地理解为变量的内存地址, 用V表示) 、 旧的预期值(用A表示) 和准备设置的新值(用B表示)。CAS指令执行时, 当且仅当V符合A时, 处理器才会用B更新V的值, 否则它就不执行更新。 但是, 不管是否更新了V的值, 都会返回V的旧值, 上述的处理过程是一个原子操作, 执行期间不会被其他线程中断。
CAS漏洞: ABA问题:如果一个变量V初次读取的时候是A值, 并且在准备赋值的时候检查到它仍然为A值, 那就能说明它的值没有被其他线程改变过了吗? 这是不能的, 因为如果在这段期间它的值曾经被改成B, 后来又被改回为A, 那CAS操作就会误认为它从来没有被改变过。不过大部分情况下ABA问题不会影响程序并发的正确性
- 无同步方案
- 可重入代码
- 线程本地存储 ThreadLocal
锁优化
高效并发是从JDK 5升级到JDK 6后一项重要的改进项, HotSpot虚拟机开发团队在这个版本上花费了大量的资源去实现各种锁优化技术, 如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening) 、 轻量级锁(Lightweight Locking) 、 偏向锁(Biased Locking) 等, 这些技术都是为了在线程之间更高效地共享数据及解决竞争问题, 从而提高程序的执行效率。自旋锁与自适应锁
锁消除
锁消除是指虚拟机即时编译器在运行时, 对一些代码要求同步, 但是对被检测到不可能存在共享数据竞争的锁进行消除。 锁消除的主要判定依据来源于逃逸分析的数据支持锁粗化
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁, 将会把加锁同步的范围扩展(粗化) 到整个操作序列的外部, 以StringBuffer为例,就是扩展到第一个append()操作之前直至最后一个append()操作之后, 这样只需要加锁一次就可以了轻量级锁
HotSpot虚拟机头部的第一部分(Mark Word)是实现轻量级锁和偏向锁的关键
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁, 在整个同步周期内都是不存在竞争的”这一经验法则。 如果没有竞争, 轻量级锁便通过CAS操作成功避免了使用互斥量的开销; 但如果确实存在锁竞争, 除了互斥量的本身开销外, 还额外发生了CAS操作的开销。 因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
偏向锁
如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量, 那偏向锁就是在无竞争的情况下把整个同步都消除掉, 连CAS操作都不去做了。
Ohter
JAVA 对象在不同位置的生命周期
全局,局部
List