JVM知识点总结

1、内存区域

1、堆内存

  • 所有的对象实例都存放在堆内存,所有线程共享的堆内存
  • Java堆自动化管理,通过垃圾回收机制自动清理垃圾对象
  • 分为新生代(Eden、s0、s1)和老年代

2、栈

  • 线程私有的内存空间(三部分:局部变量表、操作数栈、帧数据区)
  • 随着线程的结束而结束

3、方法区

  • 线程共享,保存已经被加载的类信息、方法、常量池、静态变量、即时编译器编译后的代码数据。
  • 可以理解为永久区
  • 这部分的内存回收主要是针对常量池的回收和对类型的卸载

4、CAS(Compare And Swap)

1
2
3
4
执行函数:CAS(V,E,N)
V表示需要读写的内存位置
E表示进行比较的预期原值
N表示打算写入的新值

如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置的值更新为新值,否则不做任何操作。
CAS操作是原子的。

5、如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。

2、垃圾回收

1、对象是否存活(判断一个对象是否可回收的方法)

  • 引用计数法

    给对象添加一个计数器,有引用值加1,引用失效值减1,当计数器值为0时,该对象不能再被使用
    缺点:存在对象之间相互循环引用的问题导致对象无法被回收,占用资源

  • 可达性分析算法

    通过“GC Roots”对象为起始点,当一个对象到GC Roots没有任何引用链相连,则该对象不可用,将会被回收。

2、引用

  • 强引用

    类似” Object obj = new Object()“这类声明对象引用,只要对象存在强引用就不会被回收

  • 软引用

    做缓存用,有用并非必需的对象。在内存溢出之前会被回收以空出内存

  • 弱引用

    做缓存用,非必需的对象。只能生存到下一次垃圾回收发生之前

  • 虚引用

    唯一目的就是在这个对象在被回收时收到一个系统通知

3、垃圾回收算法

  • 标记-清除算法

    标记需要被回收的对象--->标记完成后统一回收
    标记和清除效率都不高;会产生大量不连续的内存碎片

  • 复制算法

    内存分两块,一次使用一块,清理的时候将存活的对象复制到另一块内存上,然后全部清理
    一般分成较大的Eden空间和两块较小的Survivor空间,一次只使用Eden和一块Survivor,当另一块Survivor无法存放上一次新生代收集的存活对象则这些对象会通过分配担保机制进入老年代。对象到达一定的年龄也会自动进入老年代,对象年龄由GC次数决定

  • 标记-整理算法

    标记需要回收的对象,然后让所有存活的对象向一端移动,清理端边界外的内存
    新生代使用复制算法,老年代使用标记-整理算法

4、垃圾收集器

  • Serial收集器

    单线程,垃圾收集的时候必须暂停其他所有线程的运行
    优点:简单高效。 新生代收集器

  • ParNew收集器

    Serial的多线程版本
    新生代收集器

  • Parallel Scavenge收集器

    新生代收集器,使用复制算法,并行多线程
    目标是达到设定的吞吐量目标,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

  • Serial Old收集器

    Serial的老年代版本,使用标记-整理算法

  • Parallel Old收集器

    Parallel Scavenge的老年代版本,使用多线程标记-整理算法
    在注重吞吐量和CPU资源敏感的场合,可以优先考虑Parallel Scavenge和Parallel Old的组合

  • CMS(Concurrent Mark Sweep)收集器

    获取最短回收停顿时间,基于标记-清除算法
    优点:并发收集、低停顿
    缺点:

    • 对CPU资源非常敏感
    • 无法处理浮动垃圾,可能出现Concurrent Mode Failure?而导致另一次Full GC的产生
    • 标记-清除算法产生大量空间碎片,容易导致没有足够空间来分配给大对象,这样就会提前触发一次Full GC
  • G1收集器

    面向服务端应用的垃圾收集器
    优点:

    • 并行与并发:利用多核多CPU缩短停顿时间
    • 空间整合:整体使用标记-整理算法,局部使用复制算法,收集垃圾后能提供规整的可用内存
    • 分代收集:将堆分成一块块的区域,新生代和老年代不再是物理上隔离的
    • 可预测的停顿

5、内存分配与回收

  • 对象优先在新生代Eden区分配

    当Eden区空间不够时,虚拟机会发起一次Minor GC(发生在新生代的GC,比较频繁,回收速度也快)

  • 大对象直接进入老年代

    大对象就是需要大量连续内存的对象

  • 长期存活的对象进入老年代

  • 动态对象年龄判定

    如果Survivor空间中相同年龄的所有对象占有空间超过Survivor的一半,则大于等于该年龄的对象直接进入老年代

  • 空间分配担保

    JDK6之后的规则:只要老年代的连续内存空间大于新生代对象的总大小或历次晋升的平均大小就会进行Minor GC,否则进行Full GC

3、JDK工具

  • jps(JVM Process Status Tool)虚拟机进程状况工具

    列出正在运行的虚拟机进程,显示虚拟机执行主类名称,以及这些进程的本地虚拟机唯一ID(LVMID)

  • jstat(JVM Statistics Monitoring Tool)虚拟机统计信息监视工具

    可以显示本地或远程虚拟机进程的类加载、内存、垃圾收集、JIT编译等运行数据

  • jinfo(Configuration Info for Java )Java配置信息工具

    实时查看和调整虚拟机各项参数

  • jmap(Memory Map for Java)内存映像工具

    主要用于生成堆转储快照

  • jstack(Stack Trace for Java)堆栈跟踪工具

    用于生成虚拟机当前时刻的线程快照
    线程快照:当前虚拟机每一条线程正在执行的方法堆栈的集合

4、类加载

1、Class类文件

Class文件是一组以八位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列

2、类加载的生命周期

从类被加载到虚拟内存到卸载出内存为止,整个生命周期:加载-->验证-->准备-->解析-->初始化-->使用-->卸载

1、对一个类的主动引用会触发初始化(在之前加载、验证、准备需要开始),有且只有以下五种情况:

  • 遇到 new、getstatic、putstatic、invokestatic这四条字节码指令时
  • 使用java.lang.reflect包的方法对类进行反射调用时
  • 初始化一个类时,如果其父类还没初始化则要先初始化父类
  • 虚拟机启动时要指定一个要执行的主类(包含main方法的类),虚拟机先初始化这个类
  • 使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化,则需要先对这个类初始化
    其他都称为被动引用,不会触发初始化。被动引用的例子:
  • 通过子类引用父类的静态字段,不会导致子类的初始化
  • 通过数组定义来引用类,不会触发此类的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SuperClass(){
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static{
System.out.println("SubClass init");
}
}
//引用静态字段
SubClass.value;
//数组定义引用
SuperClass[] sca = new SuperClass[10];
  • 常量在编译阶段会存入调入类的常量池中,本质上没有直接引用定义常量的类,因此不会触发定义常量的类初始化
    接口的初始化与类的初始化的区别是五种有且仅有?前三种:父接口只有在使用到的时候才会初始化

2、类加载

1、验证

  • 文件格式验证

    基于二进制字节流进行

  • 元数据验证

  • 字节码验证

  • 符号引用验证

2、准备
正式为类变量分配内存并设置初始值,这里的类变量是指被static修饰的变量,内存分配不包括实例变量;初始值一般是数据类型的零值,特殊情况比如被final修饰,则会直接赋值。

3、类和加载它的类加载器确定了其在Java虚拟机中的唯一性。
比较两个类是否“相等”,只有在这两个类都由同一个类加载器加载的前提下才有意义。

4、双亲委派模型

  • 启动类加载器
  • 扩展类加载器
  • 应用程序类加载器
  • 自定义类加载器
    如果一个类加载器收到类加载的请求,首先不会自己去加载,而是委派给父类加载器去加载,每一层加载器都是如此,因此最后所有的类加载请求都会传送到顶层的启动类加载器,只有父类加载器反馈说自己无法加载,子类加载器才会自己尝试加载。

好处:
Java类随着类加载器一起具有了层级优先关系,能够有效确保一个类的全局唯一性。如java.lang.object类,由启动类加载器加载,这样object不管在哪个类加载器环境下都是同一个类。

5、虚拟机字节码执行引擎

1、栈帧结构

栈帧是用于支持虚拟机方法调用和方法执行的数据结构,是虚拟机运行时数据区的虚拟机栈的栈元素,存储着方法的局部变量表、操作数栈、动态连接和方法返回地址等。

  • 局部变量表

    容量的最小单位是槽(slot),一个slot能够存放一个32位以内的数据类型
    64位的数据类型虚拟机会以高位对齐的方式分配连续的两个slot空间
    slot可以重用

  • 操作数栈

    栈元素可以是任何的Java数据类型,32位占容量1,64位占容量2

  • 方法返回地址
    方法退出等同把当前栈帧出栈,可能的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有)压入调用者栈帧的操作数栈,调整PC计数器的值以指向方法调用指令后的一条指令

2、方法调用

方法调用不是方法执行,该阶段唯一任务就是确定被调用方法的版本(调用哪个方法)

  • 解析调用

    主要是四类方法的调用:静态方法、私有方法、实例构造器、父类方法
    解析调用是一个静态过程,在编译期就完全确定,在类加载的解析阶段就会把涉及到的符号引用转化为可确定的直接引用。

  • 静态分派(典型应用:重载)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    public class StaticDispatch {
    static abstract class Human{}
    static class Man extends Human{}
    static class Woman extends Human{}
    public void SayHello(Human guy){
    System.out.println("Hello guy");
    }
    public void SayHello(Man man){
    System.out.println("Hello gentleman");
    }
    public void SayHello(Woman woman){
    System.out.println("Hello lady");
    }
    public static void main(String[] args) {
    Human man = new Man();
    Human woman = new Woman();
    StaticDispatch st = new StaticDispatch();
    st.SayHello(man);
    st.SayHello(woman);
    }
    }

    输出结果:
    Hello guy
    Hello guy

Human是变量的静态类型,Man是变量的实际类型,编译器在重载时依据的是变量的静态类型而不是实际类型,静态类型是编译器可知的,所以在编译阶段,javac编译器会根据静态类型选择重载版本。变量的实际类型变化结果只有在程序运行时才确定,在编译的时候无法确定一个对象的实际类型是什么。

  • 动态分派(典型应用:重写)

    在运行期根据实际类型确定方法执行版本
    虚拟机的虚方法表中存放各个方法实际入口地址,如果子类没有重写方法,那么子类和父类的虚方法表中同一方法的实际地址一样,如果子类重写了,则子类虚方法表中的方法实际入口地址指向子类的版本方法实际地址。

6、内存模型

1、主内存和工作内存

Java内存模型规定了所有变量存储在主内存中,每条线程有自己的工作内存用于保存被该线程用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,线程的工作内存之间相互隔离,线程之间变量的值传递通过主内存完成。

1、内存模型要求read和load操作、store和write操作必须顺序执行,但不要求连续执行。

2、内存模型规定基本操作的几条规则:

  • 不允许一个线程丢弃最近的assign操作,即一个变量在工作内存中改变了之后必须同步回主内存
  • 如果对一个变量执行lock操作,会清空工作内存中该变量的值,执行引擎使用该变量之前需要执行load或assign操作初始化该变量的值
  • 对一个变量执行unlock操作之前,必须先把变量值同步回主内存

2、volatile关键字

1、volatile型变量的两个特性

  • 保证此变量对所有线程的可见性

    volatile变量在各个线程下是一致的,但由于Java运算操作的非原子性导致volatile变量在并发下的运算也是不安全的

  • 禁止指令重排序优化

2、对64位的数据类型double和long,虚拟机允许将没有被volatile修饰的64位数据读写操作分两次32位操作进行

3、Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度

4、实现原理:
有volatile变量修饰的共享变量进行写操作的时候会多一行以lock为前缀的汇编代码,lock前缀的指令在多核处理器下会引发了两件事情:

  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

3、三个特性

  • 原子性

  • 可见性

    一个线程对一个共享变量的修改,另一个线程能够立即得知这个修改
    final和synchronized也可以保证可见性

  • 有序性

    如果在本线程中,所有操作都是有序的,如果在一个线程观察另一个线程,所有操作都是无序的。

4、Java线程

1、线程的调度

  • 协同式线程调度

    线程的执行时间由线程自身控制,执行完任务后主动通知系统切换到另一个线程
    好处:实现简单,线程切换对线程本身是可知的,没有同步问题
    坏处:线程执行时间不可控,线程如果不主动通知系统切换就会一直阻塞

  • 抢占式线程调度

    每个线程由系统分配执行时间,通过设定线程优先级决定线程获得分配时间的多少
    Java就是这种实现
    2、线程的五种状态

  • 新建(New)

  • 运行(Runnable)

    可能正在执行或者等待CPU分配时间

  • 无限期等待(Waiting)

    等待被其他线程显式唤醒

  • 限期等待(Timed Waiting)

    等待被系统自动唤醒

  • 阻塞(Blocked)

    等待获取一个排他锁

  • 结束(Terminated)

3、线程安全
Java中各种操作共享的数据分为五类:

  • 不可变

    不可变的对象一定是线程安全的
    比如,如果共享数据是基本类型,定义时用final修饰则数据就是不可变的。
    String类对象调用replace()、subString()等方法时只会返回一个新构造的字符串,本来的字符串值不受影响

  • 绝对线程安全

  • 相对线程安全

  • 线程兼容

  • 线程对立

4、线程安全的实现

  • 互斥同步(阻塞同步)

    同步是指多线程并发访问同一共享数据时保证同一时刻只能被一个线程使用;互斥是实现同步的一种手段,临界区、互斥量、信号量都是主要的互斥实现方式。
    Java中基本的互斥同步就是synchronized 关键字、java.util.concurrent包中的ReentrantLock(可重入锁)
    问题:进行线程阻塞和唤醒带来的性能问题

  • 非阻塞同步

5、内存泄漏

内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。

  • 年老代堆空间被占 java.lang.OutOfMemoryError: Java heap space

    根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。

  • 持久代被占满 java.lang.OutOfMemoryError: PermGen space

    主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满,,主要解决方法:设置-XX:MaxPermSize=16m;换用JDK。比如JRocket

  • 堆栈溢出 java.lang.StackOverflowError

    一般是递归没返回或者循环调用

------ 本文结束 ------