JVM
Java内存区域
JDK 1.8之前
Java内存区域包括两部分:
- 运行时数据区域
- 本地内存区域
本地内存区域包括:
- 直接内存Directory Memory
运行时数据区域包括:
- 线程共享部分
- 方法区 Method Area(JDK1.8之前)
- 堆 Heap ,也称GC 堆(Garbage Collected Heap)
- 线程私有部分
- 虚拟机栈 VM Stack
- 本地方法栈 Native Method Stack
- 程序计数器 Program Counter Register
JDK 1.8及之后:
Java内存区域包括两部分:
- 运行时数据区域
- 本地内存区域
本地内存区域包括:
- 直接内存Directory Memory
- 元空间Metaspace(JDK1.8新增)
运行时数据区域包括:
- 线程共享部分
- 堆 Heap ,也称GC 堆(Garbage Collected Heap)
- 线程私有部分
- 虚拟机栈 VM Stack
- 本地方法栈 Native Method Stack
- 程序计数器 Program Counter Register
线程共享部分
方法区 Method Area
方法区的功能
- 存储类元信息,包括:类型信息、字段信息、方法表等
- JIT代码缓存
- 运行时常量池 Runtime Constant Pool
- Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种**字面量(Literal)和符号引用(Symbolic Reference)**的 常量池表(Constant Pool Table)
- 字面量:字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量
- 符号引用:符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。
- Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种**字面量(Literal)和符号引用(Symbolic Reference)**的 常量池表(Constant Pool Table)
- 字符串常量池(1.7后移到Java堆中)
- 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
- 静态变量(1.7后移到Java堆中)
方法区的实现
- 永久代(Permanent Generation)
- 永久代有一个JVM本身设置的固定大小上限,无法进行调整
- -XX:PermSize=512m 修改永久代初始大小
- -XX:MaxPermSize=512m 修改永久代最大大小
- Metaspace(元空间)
- 元空间使用的直接内存,受本机可用内存限制。
- 当未显实指定元空间内存大小时,元空间可根据应用程序需求动态地调整大小
- -XX:MetaspaceSize=512m 修改元空间初始大小
- -XX:MaxMetaspaceSize=512m 修改元空间最大大小
堆 Heap ,也称GC 堆(Garbage Collected Heap)
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
堆结构
- 新生代内存(Young Generation),新生代又分为:
- Eden 区
- Survivor 区,该区又分为:
- S0
- S1
- 老生代(Old Generation)
- 永久代(Permanent Generation)(JDK7及之前)
在JDK 8之后PermGen(永久代)被MetaSpace(元空间)取代, 元空间使用的是直接内存。
垃圾回收机制
每次执行垃圾回收后,对象的年龄会加1。当对eden区对象执行垃圾回合后,对象年龄加1进入到Survivor区。随着对象年龄的增加,当对象年龄到达阈值后(默认为15)会进入到老年代。阈值可以通过-XX:MaxTenuringThreshold=15 来设置。
修改堆大小
- -Xms=512m:修改最小队内存
- -Xmx=512m:修改最大堆内存
线程私有部分
虚拟机栈 VM Stack
虚拟机栈由多个栈帧组成,每个栈帧包含多个部分的数据:
- 局部变量表:主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)

- 操作数栈:主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
- 动态链接:主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
- 方法返回地址
程序运行中栈可能会出现两种错误
- StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
- OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
本地方法栈 Native Method Stack
本地方法栈 Native Method Stack的作用
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
程序运行中栈可能会出现两种错误
- StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
- OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
程序计数器 Program Counter Register
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
- 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
内存中的对象
对象的创建步骤
- 类加载检查:当虚拟机遇到一条new指定,首先会检查这条指令的参数能否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已经加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 分配内存:在类加载检查通过后,虚拟机将为新对象分配内存,内存大小在类加载完成后便可确定。对象分配空间的任务,等同于把一块确定大小的内存从Java堆中划出
- 内存分配方式:
- 指针碰撞
- 空闲列表
- 内存分配并发问题的解决方式
- CAS+失败重试
- TLAB
- 内存分配方式:
- 初始化零值
- 设置对象头
- 执行init方法
对象的内存布局和组成
- 对象头
- 储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等)
- 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 实例数据:实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
- 对齐填充:对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。
对象的访问定位方式
- 使用句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
- 直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
Java垃圾回收机制
内存分配原则
要理解Java的垃圾回收机制,需要先了解对象的内存分配原则。在java中对象的内存分配遵循以下3个原则:
- 对象优先在Eden区分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
内存回收原则
在运行过程中,JVM会定期进行内存回收。内存回收有多种回收方式,主要分为:部分收集 (Partial GC)和整堆收集 (Full GC)。
- 部分收集 (Partial GC)
- 部分收集针对不同的内存区域,有不同的收集方式:
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
- 部分收集针对不同的内存区域,有不同的收集方式:
- 整堆收集 (Full GC)
- 收集整个 Java 堆和方法区
空间分配担保
空间分配担保机制是指在进行新生代垃圾回收(Minor GC)时,jvm会检查老年代内存空间是否有足够空间用来存放新生代存活下来的对象,如果不够则会尝试进行一次FullGC,确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。
死亡对象的判断
Java虚拟机中判断对象死亡的主要方法:引用计数法、可达性分析算法。
- 引用计数法:每个对象有一个引用计数器,当有引用指向该对象时,计数器加一,引用失效时,计数器减一。当计数器为零时,表示对象不再被引用,可以被回收。然而,Java虚拟机一般不使用引用计数法,因为该方法无法处理循环引用的情况。
- 可达性分析算法: Java虚拟机通过可达性分析算法来判断对象是否存活。该算法的基本思想是通过一组称为“GC Roots”的根对象作为起始点,从这些根对象开始,通过对象之间的引用关系,追踪到所有能够被根对象直接或间接引用到的对象。如果某个对象无法通过任何引用链与GC Roots相连,那么该对象就被判定为不可达,即死亡对象。
GC Roots根对象有以下这些对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
- 反映 Java 虚拟机内部状态的对象
- 虚拟机内部的引导类加载器(Bootstrap Class Loader)加载的类的类对象和类的引用
判断一个常量是无用常量?
在字符串常量池有一个字符串"abc",如果当前没有任何String对象引用该字符串常量的话,说明它就是废弃常量。如果此时发生内存回收且有必要的话,"abc"就会被系统清理出常量池。
如何判断一个类是无用的类?
- 该类的所有实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法通过反射访问该类的方法
垃圾回收算法
垃圾回收算法有以下这些,每个垃圾回收器使用的算法不一样。
- 标记-清除算法:先标记不需要回收对象,标记完成后统一回收掉所有没有被标记的对象。这是最简单的算法,但是存在效率问题和空间问题(标记清除后产生大量不连续的碎片)。
- 标记-复制算法:将内存分为大小相同的两块,每次使用其他一块,当这一块内存使用完后,就将还存活的对象复制到另一块,然后再把使用的空间一次清理掉。
- 标记-整理算法:先标记不需要回收的对象,标记完成后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
- 分代收集算法:根据堆中不同的分区特点,使用合适的垃圾收集算法
- 新生代:新生代每次收集都有大量对象回收,使用“标记-复制”算法,只需要付出少量对象的复制成本就可以完成垃圾收集。
- 老年代:老年代的对象存活几率比较高,而且没有额外的空间对它进行分配担保,所以选择"标记-清除"或“标记-整理”算法进行垃圾收集
垃圾收集器
- Serial收集器
- 单线程垃圾收集器
- 新生代使用标记-复制算法,老年代使用标记-整理算法。
- ParNew收集器
- Serial收集器的多线程版本,多条垃圾收集线程并行工作,此时用户线程仍然处于等待状态
- 新生代使用标记-复制算法,老年代使用标记-整理算法。
- Paralled Scavenge收集器
- 与ParNew类似,但更加关注吞吐量(高效率的利用CPU),提供很多参数供用户找到合适的停顿时间或最大吞吐量。
- 新生代使用标记-复制算法,老年代使用标记-整理算法。
- Serial Old收集器:SerialOld收集器的老年代版本
- Paralled Old收集器:Paralled收集器的老年代版本
- CMS收集器
- 特点:
- 以获取最短回收停顿时间为目标的收集器
- 第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
- 使用标记-清除算法
- 收集步骤
- 初始标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快。
- 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。
- 重新标记:修正并发标记期间因用户程序继续运行而导致标记产生变动的部分。
- 并发清除:开启用户线程,同时GC线程开始对未标记区域做清扫。
- 优点:并发收集、低停顿
- 缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 使用”标记-清除"算法会导致收集结束时会有大量空间碎片产生
- 特点:
- G1收集器:面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征
- 特点:
- 并行与并发:充分利用多cpu的硬件优势,缩短STW(Stop-The-World停顿时间),并且可与用户线程并发运行
- 分代收集:可以独立管理整个GC堆,但还是保留了分代的概念
- 空间整合,整体基于“标记-整理”算法,局部基于"标记-复制"实现
- 收集步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
- 特点:
- ZGC收集器
- 新一代垃圾回收期 jdk11开始支持
- 基于“标记-复制”算法,同时做了重大改进
引用类型
从java1.2开始,java对引用的概概念进行了扩充,将引用类型分为了强引用、软引用、弱引用、虚引用(强度逐渐下降)。
强引用
如果一个对象具有强引用,就类似必不可少的生活用品,就算虚拟机内存不够用而抛出异常了,也不会回收该对象。
软引用
如果一个对象具有软引用,就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器不会回收它。如果内存不足时,就会回收该对象。只要垃圾器没有回收它,它就依然能被使用。软引用可用来实现内存敏感的高速缓存。
弱引用
如果一个对象具有弱引用,就类似于可有可无的生活用品。相比软引用,弱引用具有更短的生命周期。垃圾回收器扫描到弱引用的对象时,即使内存空间充足,也会回收弱引用对象。
虚引用
如果一个对象具有虚引用,那这个对象就跟没有引用一样,随时会被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。
JVM命令
- jps:查看本机java进程信息。
- jstack:打印线程的栈信息,制作线程dump文件。
- jmap:打印内存映射,制作堆dump文件
- jstat:性能监控工具
- jhat:内存分析工具
- jconsole:简易的可视化控制台
- jvisualvm:功能强大的控制台
垃圾收集算法
新生代基本采用标记-复制算法,老年代采用标记-整理算法,cms采用标记算法。
标记-清除算法
先标记后清除,这种算法会产生内存碎片。
标记-复制算法
堆一分为二,只使用一半堆内存。算法运行时,先标记可回收对象,然后清除可回收对象,再将存活对象复制到另一半堆内存,最后清空原内存空间。
此为新生代最常用算法。
标记-整理算法
标记可回收对象,然后清理可回收对象,清理后对内存空间进行整理。
垃圾收集器
- Serial New收集器:针对新生代的收集器,采用标记-复制算法。
- Serial Old(串行)收集器:新生代采用标记-复制,老年代采用标记-整理算法。
- Parallel New(并行)收集器:新生代采用标记-复制算法,老年代采用标记-整理算法。
- Parallel Old(并行)收集器:针对老年代的收集器,采用标记-整理算法。
- Parallel Scavenge(并行)收集器:针对新生代的收集器,采用标记-复制算法。
- CMS收集器:基于标记-清理算法。
- G1收集器:整体基于标记-整理算法,局部采用标记-复制算法。
JVM工具
jstat
jstat命令可以实时查看Java应用的运行数据,包括内存使用情况。
常用命名如下:
jstat -gc <pid> 1000 5显示Java堆内存的垃圾回收统计信息,包括新生代、老年代的内存使用情况,GC回收次数,回收时间等。
- 1000表示1000ms采样1次
- 5表示采样5次
jstat -gccapacity <pid>输出指定程序的堆内存各区域容量信息。
jstat -gcutil <pid> 2000输出一次指定进行的堆内存各区域使用情况的百分比。
jstat -class <pid>显示类加载的统计信息,包括已加载类数量、总加载时间等信息。
jstat -compiler <pid>显示JIT编译器的统计信息,如编译任务的数量、编译时间等。
jps
jps -l显示服务器当前运行的java应用