JVM

JVM(Java Virtual Machine) java虚拟机
JVM的作用:加载并执行Java字节码文件(.class)-加载字节码文件、分配内存(运行时数据区)、运行程序
JVM的特点:一次编译到处运行、自动内存管理、自动垃圾回收

java执行器

JVM执行引擎由解释器和即时编译器组成
解释器:逐条解析和执行字节码指令
即时编译器(JIT Compiler):将字节码动态地编译为本地机器码,会缓存热点代码复用执行,提高效率。

native方法区

Native Method Stack:本地方法栈存储了从java代码中调用本地方法时所需的信息。是线程私有的。
native方法是用来与C/C++对接的,通过JNI(java本地接口规范)调用C/C++函数使用,或C/C++调用java方法。

PC寄存器(Program Counter Register)

每个线程都有一个程序计数器,是线程私有的,保证程序执行顺序。是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,即将要指向的指令代码由执行引擎读取下一条指令。

类加载器ClassLoader

  • 负责加载class文件,class文件在文件开头有特定的文件标识(cafe babe)
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
  • 加载的类信息存放到方法区的内存空间

分类

启动类加载器(BootstrapClassLoader) 由C++实现。
扩展类加载器(ExtClassLoader/PlatformClassLoader) 由java实现,派生自ClassLoader类。
应用程序类加载器(AppClassLoader) 系统类加载器。由java实现,派生自ClassLoader类。
自定义加载器 程序员可以定制类的加载方式,派生自ClassLoader类。

双亲委派模型

自底向上检查是否加载成功:避免重复加载,下层类加载器如果已加载过此类,则不在继续向上委托,系统中绝大部分都是自定义的类,因此自底向上检查效率较高。
自顶向下尝试加载:避免核心类被修改,顶层类加载器能加载则提前加载,避免下层类加载器加载此类->沙箱安全机制。

方法区/永久代/元空间

方法区JVM规范中定义的一块内存区域,用来存储类元信息,方法字节码,即时编译器信息等
永久代是Hotspot虚拟机对JVM规范的实现(1.8之前)
元空间是Hopspot虚拟机对JVM规范的另一种实现(1.8之后),使用本地内存作为这些信息的存储空间

栈(Stack)

  • 先进后出的运行逻辑
  • 栈帧是栈中的每个单元
1
2
3
4
5
6
主要存储:
局部变量表:栈帧中的局部变量
方法返回地址:记录方法返回地址
操作数栈:运算相关的操作
动态链接:可以知道当前帧执行的是哪个方法。指向运行时常量池中方法的符号引用。程序执行时,类加载到内存中后,会将符号引用变为直接引用。
其他附加信息...

栈溢出

说明

StackOverflowError 栈空间溢出:超出栈空间大小导致溢出

堆(Heap)

区域划分

新生代(Young):伊甸园区(Eden)、幸存零区、幸存一区
老年代(Old):

分代空间工作流程:

  • 生命周期较短的对象,创建在新生代,在新生代中被垃圾回收。
  • 生命周期非常长的对象,创建在新生代,在老年代中被垃圾回收,甚至与JVM生命周期保持一致。
  • 几乎所有的对象创建在伊甸园区,绝大部分对象销毁在新生代,大对象直接进入老年代

新生代工作流程

  1. 新创建的对象先放在伊甸园区。
  2. 当伊甸园区的空间用完时,程序又需要创建新对象,触发JVM的垃圾回收器堆伊甸园区进行垃圾回收(Minor GC/Young GC),将伊甸园区不再被引用的对象销毁
  3. 然后将伊甸园区的剩余对象移动到空的幸存零区
  4. 此时伊甸园区清空
  5. 被移到幸存者零区的对象上有一个年龄计数器,值为1
  6. 然后再次将新对象放入伊甸园区
  7. 如果伊甸园区的空间再次用完,则再次触发垃圾回收,对伊甸园区和幸存零区进行垃圾回收。
  8. 此时幸存一区为空,将伊甸园区和幸存零区的剩余对象移动到幸存一区
  9. 此时伊甸园区和幸存零区为空
  10. 从伊甸园区被移动到幸存一区的对象上有一个年龄计数器,值为1,从幸存零区被移动到幸存一区的对象上的年龄计数器加一,值为2
  11. 然后再次将新对象放到伊甸园区,如果空间再次用完,再次触发垃圾回收,那么伊甸园区和幸存一区的剩余对象移动到幸存零区,对象上的年龄计数器加一
  12. 当对象上的年龄计数器达到15时(-XX:MaxTenuringThresHold),则晋升为老年代。
    特殊:如果幸存区满有溢出时直接晋升为老年代

堆溢出

OutOfMemoryError:Java heap space 使用超过堆最大空间

设置VM参数

-Xss1024k 设置栈空间为1024k,单位字节Byte
-Xms 初始堆内存总量,等价于-XX:InitialHeapSize,默认是物理内存的1/64。
-Xmx 最大堆内存总量,等价于-XX:MaxHeapSize,默认是物理内存的1/4。
-Xmn 新生代堆内存总量,等价于-XX:NewSize,默认新生代占堆的1/3空间,老年代占堆的2/3空间。
-XX:+PringGCDetails/-Xlog:gc* 输出详细的GC处理日志
-XX:MaxTenuringThreshold 设置对象在新生代中的存活次数,默认为15

VisualVM工具

说明

可视化VM工具

使用

下载

https://visualvm.github.io/download.html

配置

etc/visualvm.conf中visualvm_jdkhome=”” 配置jdk路径

参数配置

-XX:HeapDumpPath=D:\myDump 指定dump文件的位置和文件名称
-XX:+HeapDumpOnOutOfMemoryError 开启内存溢出时自动生成内存快照

GC回收

说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. 回收堆和方法区的内存
2. 分代收集算法:
频繁收集新生代
较少收集老年代
基本不动元空间
3. 对象存活判断:
引用计数:为每个对象建立引用计数器,计算使用率
缺点:
每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗
较难处理循环引用
根可达:从根节点触发可以达到的整个链路都视为存活,不可达的视为垃圾
可以作为根的介质:
虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中引用的对象
方法区中的类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(Native)引用的对象

分类

minor GC:只针对新生代区域的GC
major GC/Full GC:只针对老年代区域的GC

GC算法

  1. 复制算法(Copying)
    年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)
    逻辑:复制存活对象,之后清空该幸存区
    优点:没有碎片,复制操作采用复制后清空进行。
    缺点:总有一个幸存区是空闲的;存活率高时复制效率低。
  2. 标记清除(Mark-Sweep)
    老年代一般由标记清除或标记清除与标记整理的混合实现
    逻辑:先标记需要回收的垃圾,之后清除被标记的垃圾;或者标记存活对象,之后清除没有标记的垃圾
    优点:不需要额外空间
    缺点:两次扫描耗时严重;会产生内存碎片
  3. 标记压缩(Mark-Compact)
    老年代一般由标记清除或标记清除与标记整理的混合实现
    逻辑:先标记,再清除,再重新扫描移动对象使其排列
    优点:没有内存碎片,可以利用bump-the-pointer
    缺点:需要移动对象的成本,效率不及复制算法
  4. 标记-清除-压缩
    标记清除和标记压缩的混合实现,在多次清除后执行一次压缩。

GC算法与垃圾回收器

GC算法是内存回收的思想论,垃圾收集器就是算法实现
因为目前还没有完美的收集器出现,更没有万能的收集器,只能针对具体应用选择合适的收集器,进行分代收集

垃圾回收器分类

串行垃圾回收器(Serial) 它为单线程环境设计且只使用一个线程进行垃圾回收,会赞同所有的用户线程。不适合服务器环境
并行垃圾回收器(Parallel) 多个垃圾收集线程并行工作,此时线程是赞同的,适合科学计算/大数据后台处理等弱交互场景
并发垃圾回收器(CMS) 用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,使用堆想要有要求的场景。
G1垃圾回收器 G1垃圾回收器将堆内存分割成不同的区域,然后并发的对其进行垃圾回收
ZGC jdk17之后的垃圾回收器

参数说明

DefNew Default New Generation
Tenured Old
ParNew Parallel New Generation
PSYoungGen Parallel Scavenge
ParOldGen Parallel Old Generation

Serial

说明:一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。
串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World”状态)。虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。
对应JVM参数是:-XX:+UseSerialGC
开启后会使用:Serial(Young区用)+Serial Old(Old区用)的收集器组合
配置:-Xms1Om -Xmx1Om -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC

Parallel

ParallelScavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。串行收集器在新生代和老年代的并行化,Parallel就是Serial收集器的升级并行版,使用多个收集线程
常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器
开启该参数后:新生代使用复制算法,老年代使用标记-整理算法
-XX:ParallelGCThreads=数字N表示启动多少个GC线程
配置:
-Xms10m-Xmx10m-XX:+PrintGCDetails -XX:+PrintCommandLineFlags-XX:+UseParalleIGC
-Xms1Om-Xmx1Om-XX:+PrintGCDetails -XX:+PrintCommandLineFlags-XX:+UseParallelOIdGC

CMS

初始标记(CMS initial marl)会发生STW:只是标记一下GCRoots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
并发标记(CMS concurrent mark)GC线程和用户线程一起:进行GCRoots跟踪的过程,跟用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象
重新标记(CMS remark)会发生STW:(CMS remark)会发生STW,为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
并发清除(CMS concurrent sweep)GC线程和用户线程一起:(CMS concurrent sweep)GC线程和用户线程一起,清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象 ,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
说明:
CMS收集器(ConcurrentMarkSweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。
适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
ConcurrentMarkSweep并发标记清除,并发收集停顿低,并发指的是与用户线程一起执行
开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC开启该参数后会自动将-XX:+UseParNewGC打开
开启该参数后,使用ParNew(Young区用)+CMS(Old区用)+Serial Old的收集器组合,SerialOld将作为CMS出错的后备收集器
-Xms10m-Xmx10m-XX:+PrintGCDetails-XX:+PrintCommandLineFlags-XX:+UseConcMarkSweepGC
优点:并发收集停顿低
缺点:

  1. 并发执行,对CPU资源压力大:由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间
  2. 采用的标记清除算法会导致大量碎片,能导致FullGC:标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。

G1

从官网的描述中,我们知道G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间短小的要求。另外,它还具有以下特性:
像CMS收集器一样,能与应用程序线程并发执行。
整理空闲空间更快。
需要更多的时间来预测GC停顿时间。
不希望牺牲大量的吞吐性能。
不需要更大的Java Heap。
G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
G1的StopTheWorld(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7预发布了一个新的垃圾收集器-G1垃圾收集器。

G1是在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换jaVa8以前的CMS收集器。

主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region,
每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。

特点
1:G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW。
2:G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。
3:宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘。
4:G1收集器里面将整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域
5:G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(tospace)堆做复制准备
G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;

说明
区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。
核心思想是将整佃堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,
在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和
老年代之间切换。

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器
这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有
应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。
这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复
制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程
中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有
CMS内存碎片问题的存在了。

在G1中,还有一种特殊的区域,叫Humongous(巨大的)区域
如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为
这是一个巨型对象。这些巨型对象默认直接会被分配在年老征,但是如
果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为
了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型
对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区
来存储。为了能找到连续的H区,有时候不得不启动FullGC。

回收步骤
G1收集器下的YoungGC
针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片

  • Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部会晋升到Old区
  • Survivor区的数据移动到新的Survivor区,部会数据晋升到Old区
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。

执行过程
初始标记:只标记GCRoots能直接关联到的对象
并发标记:进行GCRootsTracing的过程
最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
筛选回收:根据时间来进行价值最大化的回收

阿尔萨斯Arthas

arthas:阿里开源的一款ava问题诊断利器,
详情见:https://arthas.aliyun.com/doc/quick-start.html