JVM篇

JVM组成
什么是程序计数器
线程私有的,内部保存的字节码行号。用于记录正在执行的字节码指令的地址。
堆
线程共享的区域:主要用来存储对象、数组等,当堆中没有内存空间可分配给实例,也无法扩展时,则会抛出OOM异常。
堆由年轻代和老年代组成。年度又分为Eden区和两个大小一致的Survivor区,老年代主要保存生命周期长的对象。
1.7对中有一个方法区/永久代,存储的是类信息、静态变量、常量、编译后的代码。1.8移除了1.7中的方法区/永久代,把数据存储到了本地内存的元空间中,防止内存溢出。
虚拟机栈
每个线程运行时需要的内存被称为虚拟机栈。
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存。
每个线程只能由一个活动的栈帧,对应着当前正在执行的那个方法。
垃圾回收是否涉及栈内存?
垃圾回收主要是针对堆,当栈栈弹栈后,内存就会被释放。
栈内存分配越大越好嘛?
不一定,默认的栈内存通常为1024k。
栈帧过大会导致线程数变少。机器总内存512M,目前能活动的线程数则为512个,若给栈内存改为2048K,那么活动栈帧就会减半。
方法内的局部变量释放线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
栈内存溢出
栈帧过多:递归。
栈帧过大。
堆和栈的区别
栈内存一般存储局部变量和方法调用,堆内存是用来存储Java对象和数组的。
栈不会被GC,堆会。
栈内存是私有的,堆是工有的。
栈或堆空间不在都会抛异常,栈抛的StackOverFlowError,堆是OutOfMemoryError。
方法区
方法区是各个线程共享的内存区域
主要存储类的信息、运行时常量池。
虚拟机启动的时候创建,关闭的时释放
如果方法区中内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace。
常量池
可以看作一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
直接内存
直接内存并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存,常见与NIO操作,用于数据缓冲,分配回收成本较高,但读写性能也高,不受JVM内存回收管理。
类加载器
什么是类加载器
类加载器将字节码文件加载到JVM中,并生成java.lang.Class类的一个实例让。
四种类加载器
- 启动类加载器(Bootstrap ClassLoader)加载
JAVA_HOME/jre/lib目录下的库。 - 扩展类加载器(ExtClassLoader):主要加载
JAVA_HOME/jre/lib/ext目录中的类。 - 应用类加载器(AppClassLoader):用于加载classpath下的类。
- 自定义类加载器(CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则。
什么是双亲委派模型
加载某个类,先委托上一级的类加载器进行加载,若上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载类。
为什么需要双亲委派机制
- 通过双亲委派可以避免某个类被重复加载,当父类加载后则无需重复加载,保证唯一性。
- 为了安全,保证类库API不会被修改。
类转载的执行过程
加载、验证、准备、解析、初始化、使用、卸载。共七个阶段。其中验证、准备、解析被称为连接阶段。
-
加载:查找和导入class文件。
- 通过全类名,获取类的二进制数据流。
- 解析类的二进制数据流到方法区,存储类的方法代码、变量名、方法名、访问权限等等。
- 创建
java.lang.Class类的实例。类的Class实例只会有一个,因为类只加载一次。每个对象都会记得自己是由哪个Class实例生成。通过Class可以完整地得到一个类的完整结构。通过这个Class实例,获取方法区的数据。
-
验证:保证加载类的准确性。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
前面三个验证都是格式验证,如检查文件格式释放错误、语法是否错误、字节码是否合规。
符号验证:Class文件在其常量池通过字符串记录自己将要使用的其他类或方法,检查它们是否都存在。
-
准备:为类变量分配内存并设置类变量的初始值
static变量,分配空间在该阶段完成并设置默认值,赋值在初始化阶段完成。
static final 基本类型,字符串类型,值已经确定,分配空间并赋值。
static final Object,分配空间并初始化。
-
解析:把类中的符号引用转化为直接引用。符号引用:方法中调用了其他方法,被调用的方法名可以理解为符号引用。直接引用就是使用指针直接指向方法。
-
初始化:对类的静态变量,静态代码块执行初始化操作。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量或静态代码块,则按照自上而下的顺序执行。
-
使用:JVM从入口方法开始执行用户的程序代码。
调用类的静态属性或方法。
使用new关键字为其创建对象实例。
-
卸载:用户代码执行完毕后,JVM便开始销毁创建的Class对象。
垃圾回收
如果一个或多个对象没有任何引用指向它了,那么这个对象就可以被回收了。
如何定位这种对象?可以使用引用计数法和根可达算法。
引用计数法:一个对象被引用了一次,在当前对象头递增一次引用技术,如果这个对象引用次数为0,则代表这个对象可被回收。不过它解决不了循环引用。
以GC Root为起点开始扫描,找不到的对象代表可以被回收。
GC Root:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的变量。
- 方法区中常量引用的对象
- 本地方法栈中JNI(一般来说的Native方法)引用的对象。
JVM回收算法
标记清除
垃圾回收分为两个阶段:标记和清除。效率高,有内存碎片,导致内存不连续。
标记整理
将存活对象都向内存另一端移动,然后清理边界以外的垃圾。没有内存碎片,对象需要移动,效率相对较低。
复制算法
将原有的空间一分为二,每次只使用其中的一块,清理内存的时候将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色。虽说没有碎片,但是内存使用率低。
分代回收

- 新创建的对象,都会被分配到eden区。
- 当eden区内存不足,标记eden区和from区的存活对象。
- 将存活对象使用复制算法复制到to中,复制完成后,eden区和from区内存都得到释放。from和to交互角色。
- 当幸存区对象熬过几次回收,晋升到老年代。若幸存区内存不足或大对象会导致提前晋升。
Minor GC(young GC):发生在新生代的垃圾回收,暂停时间短。
Mixed GC:新生代+老年代部分区域的垃圾回收,G1收集器特有。
Full GC:新生代+老年代完整垃圾回收,暂停时间长,应尽力避免。
垃圾回收器
串行垃圾回收
Serial和Serial Old串行垃圾回收,是指使用单线程进行垃圾回收,堆内存比较小,适合个人电脑。
Serial作用新生代,采用复制算法。
Serial Old作用老年代,采用标记整理。
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW,Stop the world),等待垃圾回收的完成。

并行垃圾收集器
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器。
Parallel New作用新生代,采用复制算法。
Parallel Old作用老年代,采用标记整理算法。
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停,等待垃圾回收的完成。

CMS(并发)垃圾收集器
CMS全程Concurrent Mark Sweep,是一款并发的、使用是标记清除算法的垃圾回收器,该回收器针对老年代垃圾回收的。目标是获取最短停顿时间。在垃圾回收时,应用仍然能正常运行。

G1
应用于新生代和老年代,在JDK9之后默认使用G1。
划分了多个区域,每个区域都可以充当eden、survivor、old、humongous,其中humongous专为大对象准备。
使用的标记复制算法。
响应时间与吞吐量兼顾。
分成新生代回收(STW)、并发标记(重写标记STW)、混合收集三个阶段。
如果并发失败(即回收速度赶不上创建新对象速度),会触发Full GC。
新生代回收
初创时,所有区域都处于空闲状态。
创建了一些对象,挑出一些空闲区域左右eden区域来存储这种对象。
当eden需要垃圾回收时,挑出一个空闲区域做为幸存者区,用复制算法复制存活对象到幸存者区。需要暂停用户线程。
随着时间的流逝,eden内存又不足,会将eden区以及之前幸存者区中存活的对象,采用复制算法,复制到新的幸存者区,其中较老的对象晋升至老年代。
新生代垃圾回收+并发标记
当老年代占用内存超过阈值(默认是45%)后,触发并发标记,这时不用暂停用户线程。
并发标记完成后,会重写标记漏标的问题,这时需要暂停用户线程。
前面两步都完成后,就知道老年代又哪些存活的对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高的区域(存活对象少)。
混合垃圾回收
该阶段中,参与回收的又eden,幸存区和老年代。
复制完成后,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集。
强引用、软引用、弱引用、虚引用
强引用:只有所有GC Roots对象都不通过强引用引用该对象,该对象才能被垃圾回收。
软引用:当内存不足时,就会被回收。
弱引用:垃圾回收时,不管内存是否足够,都会回收该对象。
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。
JVM实践
JVM调优参数有哪些
对于JVM调优,主要是调整新生代、老年代、元空间的内存空间大小以及使用垃圾回收器类型。
- 设置堆空间的大小。
- 虚拟机栈的设置
- 新生代eden区和两个幸存区的大小比例
- 新生代晋升老年代的阈值
- 设置垃圾回收器
JVM调优工具
命令工具
- jps:进程状态信息。
- jstack:查看java进程内线程的堆栈信息。
- jmap:查看堆栈信息
- jhat:堆转储快照分析工具
- jstat:JVM统计监测工具
可视化工具
- jconsole:用户对jvm的内存,线程,类的监控。
- VisualVM:能够监控线程,内存情况。
内存泄漏排查思路
通常情况下有三个地方会出现。
- JVM虚拟机栈:StackOverflow。
- 堆:OutOfMemoryError:java heap space。
- 元空间:OutOfMemory:metaspace。
如何排查
- 获取堆内存快照dump
- VisualVM去分析dump文件。
- 通过查看堆信息的情况,定位内存溢出问题。
CPU飙高
- 使用top命令定位那一个进程占用CPU较高。
- 使用
ps H -ep pid,tid,%cpu | grep 刚刚定位到的进程id定位是哪个线程占用CPU较高。 - 使用
jstack 进程id获取堆栈信息。其中nid表示的是线程id,只不过显示的是16进制数,只需要做一个进制转换即可确定具体是哪一行代码。