JVM虚拟机的深入浅出

目录

Java语言规范与Java虚拟机规范

Java虚拟机基本结构

关于Java堆: 对象实例、自动管理、自动清理、最为密切的内存空间

关于Java栈: (一样是先进后出)

局部变量表

操作数栈 (先进后出)

帧数据区

方法区 (可以保存多少个类)

常用Java虚拟机参数

类加载/卸载的跟踪

查看系统参数

堆的设置

新生代与老年代的配置

其他内存配置

Client和Server

垃圾回收

引用计数法

标记清除法

复制算法

标记压缩法

分代算法

分区算法

可触及性状态

强引用:

软引用:  ​编辑

弱引用:  ​编辑

虚引用:

关于Stop-The-World

垃圾收集器和内存分配

1、串行回收器 

2、并行回收器

新生代ParNew回收器

新生代ParallelGC回收器

老年代ParallelOldGC回收器

CMS回收器 (JDK8及之前版本)

G1回收器 (分代 + 分区) -- 垃圾优先的垃圾回收器 -- 优先选取垃圾比例最高的区域

关于TLAB (线程本地分配缓存)

对象分配流程

关于finalize()函数

回收器参数汇总

简要性能监控工具 -- 稍后更新

分析Java堆

关于String

MAT分析Java堆 -- OQL语句

Visual VM对OQL的支持 -- 使用细节后期可能更新

锁与并发 (这章更详细的内容可参考Java高并发深入浅出的文章)

Class文件结构

1、魔数--Class文件的标志 (它固定为0xCAFEBABE)

2、Class文件的版本 (表示当前Class文件由哪个版本的编译器编译产生的)

3、存放所有常数--常量池

Class装载系统

类装载的条件

1、加载

2、验证 (保证加载的字节码是合法、合理并符合规范的)

3、准备 (为这个类分配相应的内存空间,并设置初始值)

4、解析 (将类、接口、字段和方法的符号引用转为直接引用)

5、初始化

ClassLoader (从系统外部获得class二进制数据流)

ClassLoader的分类 (启动类加载器、扩展类加载器、应用类加载器)

双亲委托模式 (双亲为null有两种情况:第一,双亲就是启动类加载器;第二,当前加载器就是启动类加载器)

字节码执行


Java语言规范与Java虚拟机规范

词法定义规定了什么样的单词是合理的,语法定义规定了什么样的语句是合乎规范的。Java的数据类型分为原始数据类型和引用数据类型。原始数据类型又分为数字型和布尔型。

引用数据类型分为3种:类或接口、泛型类型和数组类型。

虚拟机,就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。系统虚拟机(Visual Box、VMware)和程序虚拟机。在Java虚拟机中执行的指令我们称为Java字节码指令。

以下主要说明的是Hotspot虚拟机

Java虚拟机规范的主要内容大概有以下几个部分:
    · 定义了虚拟机的内部结构
    · 定义了虚拟机执行的字节码类型和功能
    · 定义了Class文件的结构
    · 定义了类的装载、连接和初始化

相对于原码,使用补码作为计算机内的实际存储方式至少有以下两个好处。

(1)可以统一数字0的表示。由于0既非正数,又非负数,使用原码表示时符号位难以确定,把0归入正数或者负数得到的原码是不同的。但是使用补码表示时,无论把0归入正数还是负数都会得到相同的结果。

(2)使用补码可以简化整数的加减法计算,将减法计算视为加法计算,实现减法和加法的完全统一,实现正数和负数加法的统一。

Java虚拟机基本结构

类加载子系统负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间中。除了类的信息,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量

几乎所有的Java对象实例都存放于Java堆中。堆空间是所有线程共享

Java的NIO库允许Java程序使用直接内存。直接内存是在Java堆外的、直接向系统申请的内存区域。通常,访问直接内存的速度会优于Java堆。因此,出于性能考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在Java堆外,因此,它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统的最大内存

垃圾回收器可以对方法区、Java堆和直接内存进行回收 (默默回收)

每一个Java虚拟机线程都有一个私有的Java栈一个线程的Java栈在线程创建的时候被创建。Java栈中保存着帧信息(栈帧),Java栈中保存着局部变量、方法参数,同时和Java方法的调用、返回密切相关

本地方法栈: Java虚拟机允许Java直接调用本地方法(通常使用C语言编写)

PC(Program Counter)寄存器也是每个线程私有的空间,Java虚拟机会为每一个Java线程创建PC寄存器。在任意时刻,一个Java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。【如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined

执行引擎(最核心组件之一): 执行虚拟机的字节码 (即时编译技术将方法编译成机器码后再执行)

关于java进程的命令行使用方法:  

在红框中手动输入的是虚拟机参数

关于Java堆: 对象实例、自动管理、自动清理、最为密切的内存空间

根据垃圾回收机制不同,常见的结构也不同,

常见新生代老年代结构1:

存放示例: 

关于Java栈: (一样是先进后出)

线程执行的基本行为是函数调用每次函数调用的数据都是通过Java栈传递的。

每一次函数调用,都会有一个对应的栈帧被压入Java栈,每一次函数调用结束,都会有一个栈帧被弹出Java栈。

当前正在执行的函数所对应的帧就是当前的帧(位于栈顶),它保存着当前函数的局部变量、中间运算结果等数据

当函数返回时,栈帧从Java栈中被弹出。 正常return和异常情况,都会从栈中弹出。

栈帧 【局部变量表、操作数栈、帧数据区

当请求的栈深度大于最大可用栈深度时,系统就会抛出StackOverflowError栈溢出错误。 虚拟机参数-Xss指定线程最大栈空间

** 函数嵌套调用的层次在很大程度上由栈的大小决定,栈越大,函数可以支持的嵌套调用次数就越多

局部变量表

保存函数的参数及局部变量。局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,函数栈帧销毁,局部变量表也会随之销毁 (然后垃圾回收就可以回收了)

如果函数的参数和局部变量较多,会使局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少

long类型和double类型在局部变量表中需要占用2字,其他如int、short、byte、对象引用等占用1字。

** 字(Word)指的是计算机内存中占据一个单独的内存单元编号的一组二进制串。一般32位计算机上一个字为4个字节长度。

栈帧中的局部变量表中的槽位是可以重用的,如果局部变量a超过了其作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用局部变量a的槽位,从而达到节省资源的目的。

局部变量表中的变量也是重要的垃圾回收根节点,被局部变量表中直接或间接引用的对象都是不会被回收的

操作数栈 (先进后出)

保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

帧数据区

大部分Java字节码指令需要进行常量池访问,在帧数据区中保存着访问常量池的指针,方便程序访问常量池

当函数返回或者出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者函数继续执行。

异常处理表也是帧数据区中重要的一部分 (方便发生异常的时候找到处理异常的代码)

栈上分配(逃逸分析 -- 判断对象的作用域是否有可能逃逸出函数体): 对于那些线程私有的对象(这里指不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。

【也就是没出局部变量的这个范围存在】

对象User以局部变量的形式存在,并且该对象并没有被alloc()函数返回,或者出现了任何形式的公开。因此,它并未发生逃逸,所以对于这种情况,虚拟机就有可能将User分配在栈上,而不在堆上。

在Server模式下,才可以启用逃逸分析。栈上分配依赖逃逸分析和标量替换的实现

栈的空间较小,大对象无法也不适合在栈上分配。

方法区 (可以保存多少个类)

所有线程共享的内存区域。保存系统的类信息,比如类的字段、方法、常量池等。

在JDK 1.6、JDK 1.7中,方法区可以理解为永久区(Perm)。永久区可以使用参数-XX:PermSize和-XX:MaxPermSize指定。一个大的永久区可以保存更多的类信息。

在JDK 1.8、JDK1.9、JDK1.10中,永久区已经被彻底移除。取而代之的是元数据区,元数据区大小可以使用参数-XX:MaxMetaspaceSize指定(一个大的元数据区可以使系统支持更多的类),这是一块堆外的直接内存。与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存

常用Java虚拟机参数

使用给定的参数执行Java虚拟机,就可以在系统运行时打印相关日志,用于问题分析。

最简单的一个GC参数是-XX:+PrintGC(在JDK9、JDK10中建议使用-Xlog:gc),使用这个参数启动Java虚拟机后,只要遇到GC,就会打印日志。

JDK9、JDK10默认使用G1作为垃圾回收器,使用参数-Xlog:gc来打印GC日志

如果需要更加详细的信息,可以使用-XX:+PrintGCDetails参数,(JDK9、JDK10建议使用-Xlog:gc*)

如果需要更全面的堆信息,还可以使用参数-XX:+PrintHeapAtGC【在GC日志输出前、后都有详细的堆信息输出,分别表示GC回收前和GC回收后的堆信息】。考虑到兼容性,从JDK9开始已经删除此参数,查看堆信息可以使用VisualVM。

如果需要分析GC发生的时间,还可以使用-XX:+PrintGCTimeStamps(JDK9、JDK10中使用-Xlog:gc*已经默认打印出时间,前文关于-Xlog:gc*已经有讲述,这里不再赘述)参数。

使用参数-XX:+PrintGCApplicationConcurrentTime可以打印应用程序的执行时间,使用参数-XX:+PrintGCApplicationStoppedTime可以打印应用程序由于GC而产生的停顿时间

如果想跟踪系统内的软引用、弱引用、虚引用和Finallize队列,可以打开-XX:+PrintReferenceGC(考虑到兼容性,从JDK9开始已经删除此参数,查看堆信息可以使用VisualVM)

使用参数-Xloggc指定。比如使用参数-Xloggc:log/gc.log(在JDK9、JDK10中建议使用-Xlog:gc:log/gc.log)启动虚拟机,可以在当前目录的log文件夹下的gc.log文件中记录所有的GC日志

类加载/卸载的跟踪

使用参数-verbose:class跟踪类的加载/卸载,也可以单独使用参数-XX:+TraceClassLoading(在JDK9、JDK10中建议使用-Xlog:class+load=info,跟JDK8中的参数-XX:+TraceClassLoading效果相同)跟踪类的加载,使用参数-XX:+TraceClassUnloading(在JDK9、JDK10中建议使用-Xlog:class+unload=info,跟JDK8中的参数-XX:+TraceClassLoading效果相同)跟踪类的卸载。这两类参数是等价的

** 动态类的加载是非常隐蔽的,它们由代码逻辑控制,不出现在文件系统中,跟踪这些类需要使用-XX:+TraceClassLoading等参数

在运行时打印、查看系统中类的分布情况,只要在系统启动时加上-XX:+PrintClassHistogram参数,然后在Java的控制台中按下Ctrl+Break组合键,控制台上就会显示当前的类信息柱状图

查看系统参数

参数-XX:+PrintVMOptions可以在程序运行时打印虚拟机接收到的命令行显式参数。参数-XX:+PrintCommandLineFlags可以打印传递给虚拟机的显式和隐式参数,隐式参数未必是通过命令行直接给出的,它可能是在虚拟机启动时自行设置的

另一个有用的参数是-XX:+PrintFlagsFinal,它会打印所有的系统参数的值

堆的设置

初始堆: -Xms。如果初始堆空间耗尽,虚拟机将会对堆空间进行扩展,其扩展上限为最大堆空间,最大堆空间可以使用参数-Xmx指定。

因为当前总内存总是在-Xms和-Xmx之间,从-Xms开始根据需要向上增长。当前空闲内存应该是当前总内存减去当前已经使用的内存。

因为分配给堆的内存空间和实际可用内存空间并非一个概念。由于垃圾回收的需要,虚拟机会对堆空间进行分区管理,不同的区采用不同的回收算法,一些算法会使用空间换时间的策略,因此会存在可用内存的损失。实际最大可用内存为-Xmx的值减去对齐后的from的值。

** 在实际工作中,也可以直接将初始堆-Xms与最大堆-Xmx设置为相等。这样的好处是,可以减少程序运行时进行垃圾回收的次数,从而提高程序的性能

新生代与老年代的配置

参数-Xmn可以用于设置新生代的大小。设置一个较大的新生代会减小老年代的大小,这个参数对系统性能及GC行为有很大的影响。新生代的大小一般设置为整个堆空间的1/3到1/4

参数-XX:SurvivorRatio用来设置新生代中eden区和from/to区的比例。

分配示例:  

不同的堆分布情况对系统会产生一定影响。在实际工作中,应该根据系统的特点做合理的设置,基本策略是:尽可能将对象预留在新生代,减少老年代GC的次数

设置新生代与老年代的比例: 

** -XX:SurvivorRatio可以设置eden区与survivor的比例。-XX:NewRatio可以设置老年代与新生代的比例

堆分配参数

堆空间不足就会报 OOM 异常。

Java虚拟机提供了参数-XX:+HeapDumpOnOutOfMemoryError,可以在内存溢出时导出整个堆的信息。和它配合使用的还有-XX:HeapDumpPath,可以指定导出堆的存放路径

使用示例: 

使用MAT等工具打开dump文件进行分析。

其他内存配置

方法区 (同上述方法区)

栈配置 (参考上述栈帧节)

直接内存配置

直接内存跳过了Java堆,使Java程序可以直接访问原生堆空间

最大可用直接内存可以使用参数-XX:MaxDirectMemorySize设置,如果不设置,默认值为最大堆空间,即-Xmx的值。当直接内存使用量达到-XX:MaxDirectMemorySize时,就会触发垃圾回收,如果垃圾回收不能有效释放足够的空间,直接内存溢出依然会引起系统的OOM

** 直接内存适合申请次数较少、访问较频繁的场合【因为申请内存空间时,堆空间的速度远远快于直接内存】。如果需要频繁申请内存空间,则并不适合使用直接内存

Client和Server

使用参数-client可以指定使用Client模式,使用参数-server可以指定使用Server模式。在默认情况下,虚拟机会根据当前计算机系统环境自动选择运行模式。使用-version参数可以查看当前模式

Server启动较慢,但完全启动稳定期后,Server模式执行速度远远快于Client模式。对于后台长期运行的系统来说,使用-server参数启动对系统的整体性能可以有不小的帮助。

如果需要查看给定参数的默认值,可以使用-XX:+PrintFlagsFinal参数

Server模式会尝试收集更多的系统性能信息,使用更复杂的优化算法对程序进行优化


垃圾回收

引用计数法、标记清除法、复制算法、标记压缩法、分代算法和分区算法

引用计数法

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用

缺点

(1)无法处理循环引用。因此,在Java的垃圾回收器中没有使用这种算法。

(2)引用计算器要求在每次引用产生和消除的时候,伴随一个加法操作和一个减法操作,对系统性能会有一定的影响。

不可达对象,循环引用

标记清除法

标记阶段与清除阶段 : 首先通过根节点标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。标记清除法的最大问题是可能产生空间碎片 (不连续内存空间的工作效率要低于连续空间)

复制算法

将原有的内存空间分为两块,每次只使用其中一块,在进行垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

优点:

1、 如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量就会相对较少,需要垃圾回收的时刻,复制算法的效率是很高的;

2、 由于对象是在垃圾回收过程中统一被复制到新的内存空间中的,可确保回收后的内存空间是没有碎片

缺点: 系统内存折半

新生代分为eden区、from区和to区3个部分。其中from区和to区可以视为用于复制的两块大小相同、地位相等且可进行角色互换的空间。from区和to区也称为survivor区,即幸存者空间,用于存放未被回收的对象

标记压缩法

标记压缩法是一种老年代的回收算法。 标记与上述的标记算法相同。后续会做一次将所有的存活对象压缩到内存的一端,然后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,性价比较高。

分代算法

新生代的特点是对象朝生夕灭,大约90%的新建对象会被很快回收,因此新生代比较适合使用复制算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老年代的内存空间。在老年代中,几乎所有的对象都是经过几次垃圾回收依然得以存活的。因此,可以认为这些对象在一段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的

为了支持高频率的新生代回收,虚拟机可能使用一种叫作卡表(Card Table)的数据结构。卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用。

根据卡表新生代GC只需扫描部分老年代

分区算法

为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标停顿时间,每次合理地回收若干个小区间,而不是回收整个堆空间,从而减少一次GC所产生的停顿

可触及性状态

· 可触及的:从根节点开始,可以到达这个对象。
· 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()函数中复活。
· 不可触及的:对象的finalize()函数被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为finalize()函数只会被调用一次

** finalize()函数是一个非常糟糕的模式,不推荐读者使用finalize()函数释放资源
    第一,因为finalize()函数有可能发生引用外泄,在无意中复活对象;
    第二,由于finalize()函数是被系统调用的,调用时间是不明确的,因此不是一个好的资源释放方案,推荐在try-catch-finally语句中进行资源的释放

4个级别的引用: 强引用就是程序中一般使用的引用类型,强引用的对象是可触及的,不会被回收。软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下都是可以被回收的。

强引用:

· 强引用可以直接访问目标对象。
· 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向的对象。
· 强引用可能导致内存泄漏。

软引用:  

如果一个对象只持有软引用,那么当堆空间不足时,就会被回收。GC未必会回收软引用的对象,但是当内存资源紧张时,软引用对象会被回收,所以软引用对象不会引起内存溢出。

弱引用:  

在系统GC时,只要发现弱引用,不管系统堆空间使用情况如何,都会将对象进行回收。

由于垃圾回收器的线程通常优先级很低,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。一旦一个弱引用对象被垃圾回收器回收,便会加入一个注册的引用队列。

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况

虚引用:

随时都可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程

由于虚引用可以跟踪对象的回收时间,所以也可以将一些资源释放操作放在虚引用中执行和记录

关于Stop-The-World

停顿的目的是终止所有应用线程的执行,只有这样系统中才不会有新的垃圾产生,同时停顿保证了系统状态在某一个瞬间的一致性,也有益于垃圾回收器更好地标记垃圾对象 。 也就是说垃圾回收越频繁,累计起来的STW的时间就会多;总的来说,老年代的时间较新生代的时间会长,新生代的垃圾回收会更频繁,如果新生代的内存分配的更大,会减少频繁的回收,但是每次的回收时间又相对拉长。

垃圾收集器和内存分配

1、串行回收器 

每次回收时,串行回收器只有一个工作线程,对于并行能力较弱的计算机来说,串行回收器的专注性和独占性往往能让其有更好的性能表现。串行回收器可以在新生代和老年代使用

最古老的,特点是

第一,它仅仅使用单线程进行垃圾回收。
第二,它是独占式的垃圾回收方式。

高效稳定,新生代串行回收器使用复制算法, 没有线程切换的开销。场景: 单CPU处理器等硬件平台不是特别优越的情况下,它的性能表现可以超过并行回收器和并发回收器。

** 使用-XX:+UseSerialGC参数可以指定使用新生代串行回收器或老年代串行回收器。当虚拟机在Client模式下运行时,它是默认的垃圾回收器

老年代串行回收器使用的是标记压缩法。因为是串行回收器,老年代影响的STW的时间可能会更长。

参数

·-XX:+UseSerialGC:新生代、老年代都使用串行回收器。
·-XX:+UseParNewGC(JDK 9、JDK 10已经删除,因为ParNew需要和CMS搭配工作,而CMS已经被G1替代,不再支持此参数):新生代使用ParNew回收器,老年代使用串行回收器。
·-XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行回收器。

2、并行回收器

使用多个线程同时进行垃圾回收。对于并行能力强的计算机,可以有效减少垃圾回收所需的实际时间

新生代ParNew回收器

ParNew回收器也是独占式的回收器,在回收过程中应用程序会全部暂停。简单的将串行回收器多线程化

参数

·-XX:+UseParNewGC(JDK 9、JDK 10已经删除,因为ParNew需要和CMS搭配工作,而CMS已经被G1替代,不再支持此参数):新生代使用ParNew回收器,老年代使用串行回收器
·-XX:+UseConcMarkSweepGC(JDK 9、JDK 10不建议使用,建议使用默认的G1垃圾回收器):新生代使用ParNew回收器,老年代使用CMS

ParNew回收器工作时的线程数量可以使用-XX:ParallelGCThreads参数指定。一般,最好与CPU数量相当,避免过多的线程数影响垃圾回收性能。在默认情况下,当CPU数量小于8时,ParallelGCThreads的值等于CPU数量,当CPU数量大于8时,ParallelGCThreads的值等于3+((5×CPU_Count)/8)

新生代ParallelGC回收器

ParallelGC也是复制算法,多线程、独占式。 ParallelGC回收器有一个重要的特点:它非常关注系统的吞吐量。

参数

·-XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行回收器。
·-XX:+UseParallelOldGC:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC回收器。
ParallelGC回收器提供了两个重要的参数用于控制系统的吞吐量。
·-XX:MaxGCPauseMillis:设置最大垃圾回收停顿时间。它的值是一个大于0的整数。ParallelGC 在工作时,会调整 Java 堆大小或者其他参数,尽可能地把停顿时间控制在MaxGCPauseMillis 以内。如果读者希望减少停顿时间而把这个值设得很小,为了达到预期的停顿时间,虚拟机可能会使用一个较小的堆(一个小堆比一个大堆回收快),而这将导致垃圾回收变得很频繁,从而增加垃圾回收总时间,降低吞吐量。
·-XX:GCTimeRatio:设置吞吐量大小。它的值是一个 0 到 100 之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间进行垃圾回收。比如GCTimeRatio等于19(默认值),则系统用于垃圾回收的时间不超过1/(1+19)=5%。默认情况下,它的取值是99,即有不超过1/(1+99)=1%的时间用于垃圾回收。

** 使用-XX:+UseAdaptiveSizePolicy可以打开自适应GC策略。在这种模式下,新生代的大小、eden区和survivor区的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。

老年代ParallelOldGC回收器

同ParallelGC回收器类似,和ParallelGC新生代回收器搭配使用。ParallelOldGC回收器使用标记压缩法,它在JDK1.6中才可以使用。

使用-XX:+UseParallelOldGC可以在新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器。这是一对非常关注吞吐量的垃圾回收器。在对吞吐量敏感的系统中,可以考虑使用。参数-XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量

CMS回收器 (JDK8及之前版本)

CMS回收器主要关注系统停顿时间。并发标记清除,多线程并行回收。

CMS工作时的主要步骤有:初始标记、并发标记、预清理、重新标记、并发清除和并发重置

并发重置是指在垃圾回收完成后,重新初始化CMS数据结构和数据,为下一次垃圾回收做好准备。并发标记、并发清理和并发重置都是可以和应用程序线程一起执行的。

预处理时会刻意等待一次新生代GC的发生,然后根据历史性能数据预测下一次新生代GC可能发生的时间,在当前时间和预测时间的中间时刻进行重新标记。这样可尽量避免新生代GC和重新标记重合,尽可能减少一次停顿的时间

参数

启用CMS回收器的参数是-XX:+UseConcMarkSweepGC

CMS默认启动的并发线程数是(ParallelGCThreads+3)/4。ParallelGCThreads表示GC并行时使用的线程数量,如果新生代使用ParNew,那么ParallelGCThreads也就是新生代GC的线程数量。这意味着有4个ParallelGCThreads时,只有1个并发线程,而有两个并发线程时,有5~8个ParallelGCThreads线程。

CMS会和应用程序同时运行,因此 CMS回收器不会等待堆内存饱和时才进行垃圾回收,而是当堆内存使用率达到某一阈值时便开始进行回收,以确保应用程序在CMS工作过程中,依然有足够的空间支持应用程序运行。

通过-XX:CMSInitiatingOccupancyFraction可以指定当老年代空间使用率达到多少时进行一次CMS垃圾回收

如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数,可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行回收器。

-XX:+UseCMSCompactAtFullCollection参数可以使CMS在垃圾收集完成后,进行一次内存碎片整理,内存碎片的整理不是并发进行的

-XX:CMSFullGCsBeforeCompaction参数可以用于设定进行多少次CMS回收后,进行一次内存压缩。

如果在CMS工作过程中,出现非常频繁的并发模式失败,就应该考虑进行调整,尽可能预留一个较大的老年代空间。或者可以设置一个较小的-XX:CMSInitiatingOccupancyFraction参数,降低CMS触发的阈值,使CMS在执行过程中仍然有较大的老年代空闲空间供应用程序使用。

如果希望使用CMS回收器回收Perm区,则必须打开-XX:+CMSClassUnloadingEnabled开关。

G1回收器 (分代 + 分区) -- 垃圾优先的垃圾回收器 -- 优先选取垃圾比例最高的区域

G1依然是分代垃圾回收器,但从堆的结构上看,它并不要求整个eden区、年轻代或者老年代都连续。它使用了分区算法。

· 并行性:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力。
· 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,一般来说,不会在整个回收期间完全阻塞应用程序。
· 分代 GC:G1 依然是一个分代回收器,但是和之前的回收器不同,它同时兼顾年轻代和老年代,其他回收器或者工作在年轻代,或者工作在老年代。
· 空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS,只是简单地标记清理对象,在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少碎片空间
· 可预见性:由于分区的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,全局停顿也能得到较好的控制

4个阶段

· 新生代GC。
· 并发标记周期。
· 混合回收。
· 如果需要,可能会进行Full GC。

新生代GC的主要工作是回收eden区和survivor区。一旦eden区被占满,新生代GC就会启动。

并发标记周期: 初始标记、根区域扫描、并发标记、重新标记(STAB -- G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度)、独占清理、并发清理

混合回收

这个阶段既会执行正常的年轻代GC,又会选取一些被标记的老年代区域进行回收,它同时处理了新生代和老年代。

混合GC会执行多次,直到回收了足够多的内存空间,然后它会触发一次新生代GC。新生代GC后,又可能会发生一次并发标记周期的处理,最后又会引起混合GC的执行。

FullGC

当在特别繁忙的场合出现在回收过程中内存不充足的情况, G1也会转入一个FullGC。

可以使用-XX:+UseG1GC标记打开G1的开关,对G1进行设置时,最重要的一个参数就是-XX:MaxGCPauseMillis,它用于指定目标最大停顿时间

另外一个重要的参数是-XX:ParallelGCThreads,它用于设置并行回收时GC的工作线程数量。

-XX:InitiatingHeapOccupancyPercent参数可以指定当整个堆使用率达到多少时,触发并发标记周期的执行。默认值是45,即当整个堆的占用率达到45%时,执行并发标记周期。InitiatingHeapOccupancyPercent一旦设置,始终都不会被G1修改,这意味着G1不会试图改变这个值来满足MaxGCPauseMillis的目标。如果InitiatingHeapOccupancyPercent值设置得偏大,会导致并发周期迟迟得不到启动,那么引起Full GC的可能性也大大增加,反之,一个过小的InitiatingHeapOccupancyPercent值会使得并发标记周期执行非常频繁,大量GC线程抢占CPU,导致应用程序的性能有所下降。

关于System.gc()

** 如果设置了-XX:-+DisableExplicitGC,条件判断就无法成立,那么就会禁用显式GC,System.gc()等价于一个空函数调用

** 只有在打开虚拟机参数-XX:+ExplicitGCInvokesConcurrent开关后,System.gc()这种显式GC才会使用并发的方式进行回收,否则,无论是否启用了CMS或者G1,都不会进行并发回收

并行GC前额外触发的新生代GC, 目的是避免将所有回收工作同时交给一次Full GC进行,从而尽可能地缩短一次停顿时间。参数-XX:-ScavengeBeforeFullGC去除发生在Full GC之前的那次新生代GC,默认为true。

虚拟机提供了一个参数来控制新生代对象的最大年龄:MaxTenuringThreshold。在默认情况下,这个参数的值为15。也就是说,新生代的对象最多经历15次GC,就可以晋升到老年代

MaxTenuringThreshold指的是最大晋升年龄。它是对象晋升到老年代的充分非必要条件。即达到该年龄,对象必然晋升,而未达到该年龄,对象也有可能晋升。事实上,对象的实际晋升年龄,是由虚拟机在运行时自行判断

大对象可能直接进入老年代

除了年龄,对象的体积也会影响对象的晋升。如果对象体积很大,新生代无论eden区还是survivor区都无法容纳这个对象,自然这个对象无法存放在新生代,也非常有可能被直接晋升到老年代。

参数PretenureSizeThreshold,它用来设置对象直接晋升到老年代的阈值,单位是字节。只要对象的大小大于指定值,就会绕过新生代,直接在老年代分配。这个参数只对串行回收器和ParNew有效,对于ParallelGC无效。默认情况下该值为0,也就是不指定最大的晋升大小,一切由运行情况决定。

关于TLAB (线程本地分配缓存)

使用了TLAB这种线程专属的区域来避免多线程冲突,提高对象分配的效率。TLAB本身占用了eden区的空间。在TLAB启用的情况下,虚拟机会为每一个Java线程分配一块TLAB区域

如果TLAB不满足后面比它剩余存储空间的对象的处理方法: 虚拟机内部会维护一个叫作refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,若小于该值,则会废弃当前TLAB区域,新建TLAB区域来分配新对象。这个阈值可以使用虚拟机参数TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。它的默认值为64,即表示使用约为1/64的TLAB区域作为refill_waste。

自动调整,如果想禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,并使用-XX:TLABSize手工指定TLAB的大小。

对象分配流程

关于finalize()函数

尽量不使用finalize()函数进行资源释放

· 在使用finalize()函数时可能会导致对象复活

· finalize()函数的执行时间是没有保障的,它完全由GC线程决定,在极端情况下,若不发生GC,finalize()函数将没有机会执行

· 一个糟糕的finalize()函数会严重影响GC的性能

finalize()函数可能会被作为一种补偿措施,在正常方法出现意外时(开发人员疏忽)进行补偿,尽可能确保系统稳定。当然,由于其调用时间的不确定性,这不能单独作为可靠的资源回收手段。

回收器参数汇总

1.与串行回收器相关的参数
    ·-XX:+UseSerialGC:在新生代和老年代使用串行回收器。
    ·-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例。
    ·-XX:PretenureSizeThreshold:设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接被分配在老年代。
    ·-XX:MaxTenuringThreshold:设置对象进入老年代的年龄的最大值。每一次 Minor GC后,对象年龄就加1。任何大于这个年龄的对象,一定会进入老年代。
2.与并行GC相关的参数
    ·-XX:+UseParNewGC(考虑到兼容性问题,JDK 9、JDK 10已经删除):在新生代使用并行回收器。
    ·-XX:+UseParallelOldGC:老年代使用并行回收器。
    ·-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等,但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。
    ·-XX:MaxGCPauseMillis:设置最大垃圾回收停顿时间。它的值是一个大于0的整数。回收器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMillis以内。

·-XX:GCTimeRatio:设置吞吐量大小。它的值是一个 0 到 100 之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾回收。
    ·-XX:+UseAdaptiveSizePolicy:打开自适应GC策略。在这种模式下,新生代的大小、eden区和survivior区的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡。
3.与CMS回收器相关的参数(JDK9、JDK10已经开始废弃CMS回收器,建议使用G1回收器)
    ·-XX:+UseConcMarkSweepGC:新生代使用并行回收器,老年代使用CMS+串行回收器。
    ·-XX:ParallelCMSThreads:设定CMS的线程数量。
    ·-XX:CMSInitiatingOccupancyFraction:设置 CMS 回收器在老年代空间被使用多少后触发,默认为68%。
    ·-XX:+UseCMSCompactAtFullCollection:设置 CMS 回收器在完成垃圾回收后是否要进行一次内存碎片的整理。
    ·-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩。
    ·-XX:+CMSClassUnloadingEnabled:允许对类元数据区进行回收。
    ·-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收(前提是激活了-XX:+CMSClassUnloadingEnabled)。
    ·-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阈值的时候才进行CMS回收。
    ·-XX:+CMSIncrementalMode:使用增量模式,比较适合单CPU。增量模式在JDK 8中标记为废弃,并且将在JDK 9中彻底移除。
4.与G1回收器相关的参数
    ·-XX:+UseG1GC:使用G1回收器。
    ·-XX:MaxGCPauseMillis:设置最大垃圾回收停顿时间。
    ·-XX:GCPauseIntervalMillis:设置停顿间隔时间。

5.TLAB相关
    ·-XX:+UseTLAB:开启TLAB分配。
    ·-XX:+PrintTLAB(考虑到兼容性问题,JDK 9、JDK 10不再支持此参数):打印TLAB相关分配信息。
    ·-XX:TLABSize:设置TLAB区域大小。
    ·-XX:+ResizeTLAB:自动调整TLAB区域大小。
6.其他参数
    ·-XX:+DisableExplicitGC:禁用显式GC。
    ·-XX:+ExplicitGCInvokesConcurrent:使用并发方式处理显式GC。


简要性能监控工具 -- 稍后更新


分析Java堆

OOM 内存空间耗尽

堆溢出绝大部分Java内存溢出都属于这种情况。其原因是大量对象占据了堆空间,而这些对象都持有强引用,无法回收,当对象大小之和大于由Xmx参数指定的堆空间大小时,溢出错误就自然而然地发生了

通过-Xmx扩大最大堆空间或者通过MAT或者Visual VM来排查问题

直接内存溢出:  在Java的NIO(New I/O)中,支持直接内存使用,也就是通过Java代码获得一块堆外的内存空间,这块空间是直接向操作系统申请的。直接内存的申请速度一般要比堆内存慢,但是其访问速度要快于堆内存。因此,对于那些可复用的,并且会被经常访问的空间,使用直接内存是可以提高系统性能的。但由于直接内存没有被Java虚拟机完全托管,若使用不当,也容易触发直接内存溢出,导致宕机

为避免直接内存溢出,在确保空间不浪费的基础上,合理执行显式GC可以降低直接内存溢出的概率,设置合理的-XX:MaxDirectMemorySize值也可以避免意外的内存溢出,而设置一个较小的堆在32位虚拟机上可以使得更多的内存用于直接内存。

过多线程导致OOM

处理这类OOM,除了合理地减少线程总数,减小最大堆空间、减小线程的栈空间也是可行的

永久区溢出

· 增大MaxPermSize的值。

· 减少系统需要的类的数量。
· 使用ClassLoader合理地装载各个类,并定期进行回收。

GC效率低下引起的OOM

· 花在GC上的时间是否超过了98%。
· 老年代释放的内存是否小于2%。
· eden区释放的内存是否小于2%。
· 是否连续5次GC都出现了上述几种情况(注意是同时出现,不是出现一个)。

虚拟机并不强制一定要开启这个错误提示,可以通过关闭-XX:-UseGCOverheadLimit来禁止这种OOM产生。

关于String

不变性、针对常量池的优化、类的final定义

不变模式的主要作用在于,当一个对象需要被多线程共享并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能

比如String.substring()、String.concat()方法,它们都没有修改原始字符串,而是产生了一个新的字符串,这一点是非常值得注意的。如果需要一个可以修改的字符串,那么需要使用StringBuffer或者StringBuilder对象。

针对常量池的优化指当两个String对象拥有相同的值时,它们只引用常量池中的同一个副本。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

作为final类的String对象在系统中不可能有任何子类,这是对系统安全性的保护。

MAT分析Java堆 -- OQL语句

** 使用MAT既可以打开一个已有的堆快照,也可以通过MAT直接从活动Java程序中导出堆快照

Visual VM对OQL的支持 -- 使用细节后期可能更新

** 对于MAT来说,OQL的关键字,如select、from等可以使用大写,也可以使用小写,但对于Visual VM而言,必须统一使用小写。

锁与并发 (这章更详细的内容可参考Java高并发深入浅出的文章)

通过锁可以让多个线程排队一个一个地进入临界区访问目标对象,使目标对象的状态总保持一致,这也就是锁存在的价值。

对象头和锁 【Mark World信息】

关于偏向锁、轻量级锁、自旋锁、锁消除、锁膨胀的详细内容在另一篇文章中已有论述。

锁在应用层的优化思路(减少锁持有时间【只在需要同步的地方加锁】、减少锁粒度【ConcurrentHashMap的段的概念】、锁分离【典型案例LinkedBlockingQueue,put和take分别使用了两把不同的锁,而不是一把独占锁】、锁粗化【虚拟机在遇到一连串连续地对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数】)

无锁: 最简单的非阻塞同步 ThreadLocal (每个线程拥有各自独立的变量副本)  、 CAS (它包含3个参数,形式为CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V的值等于E的值时,才会将V的值设为N,如果V的值和E的值不同,说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值)

原子操作 (atomic包)

理解Java内存模型 (原子性、有序性、可见性)

Happens-Before原则

· 程序顺序原则:一个线程内保证语义的串行性。
· volatile规则:volatile变量的写先发生于读,这保证了volatile变量的可见性。
· 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。
· 传递性:A先于B,B先于C,那么A必然先于C。
· 线程的start()方法先于它的每一个动作。
· 线程的所有操作先于线程的终结(Thread.join())。

· 线程的中断(interrupt())先于被中断线程的代码。
· 对象的构造函数执行结束先于finalize()方法。


Class文件结构

统一而强大的Class文件,它是异构语言和Java虚拟机之间的重要桥梁。

Class文件总体结构

Class文件使用一种类似于C语言结构体的方式进行描述,并且统一使用无符号整数作为基本数据类型,由u1、u2、u4、u8分别表示无符号单字节、2字节、4字节和8字节整数。对于字符串,则使用u1数组进行表示。  【粗略表示如图所示】

1、魔数--Class文件的标志 (它固定为0xCAFEBABE)

示例分析代码

2、Class文件的版本 (表示当前Class文件由哪个版本的编译器编译产生的)

Class文件的版本号和Java编译器的对应关系

高版本的Java虚拟机可以执行低版本编译器生成的Class文件,但是反之不行

3、存放所有常数--常量池

常量池对于Class文件中的字段和方法解析也有至关重要的作用,可以说,常量池是整个Class文件的基石。

常量池表项的类型及其TAG值

类型的字符串表示方法

-- 稍后更新--


Class装载系统

Class文件的装载流程

系统装载Class类型可以分为加载、连接(验证、准备和解析)和初始化3步。 

类装载的条件

Java虚拟机规定,一个类或接口在初次使用前,必须进行初始化 (只有下面主动使用的情况)

· 当创建一个类的实例时,比如使用new关键字或者反射、克隆、反序列化。
· 当调用类的静态方法时,即使用了字节码invokestatic指令。
· 当使用类或接口的静态字段时(final常量除外),比如使用getstatic或者putstatic指令。
· 当使用java.lang.reflect包中的方法反射类的方法时。
· 当初始化子类时,要求先初始化父类。
· 作为启动虚拟机,含有main()方法的那个类。

被动使用,未初始化示例

结果 

虽然Child类没有被初始化,但是此时Child类已经被系统加载,只是没有进入初始化阶段。

引用final常量: 因为在Class文件生成时,final常量由于不变性,做了适当的优化,所以不会进行初始化。

** 并不是在代码中出现的类就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会被初始化

1、加载

· 通过类的全名获取类的二进制数据流。
· 解析类的二进制数据流为方法区内的数据结构。
· 创建java.lang.Class类的实例,表示该类型。

在Java虚拟机中,完成加载类尤其是获取类的二进制信息的组件就是ClassLoader类加载器

2、验证 (保证加载的字节码是合法、合理并符合规范的)

粗略上要做的检查

3、准备 (为这个类分配相应的内存空间,并设置初始值)

各类型变量默认的初始值

** Java并不支持boolean类型,对于boolean类型,内部实现实际上是int类型,由于int类型的默认值是0,故对应的boolean类型的默认值就是false

在准备阶段不会有任何Java代码被执行

ldc加载一个常量压入到操作数栈上,putstatic字节码设置给定的静态字段的值

4、解析 (将类、接口、字段和方法的符号引用转为直接引用)

通过解析操作,符号引用可以转变为目标方法在类的方法表中的位置,从而使得方法被成功调用

5、初始化

初始化阶段的重要工作是执行类的初始化方法<clinit>。方法<clinit>是由编译器自动生成的,它是由类静态成员的赋值语句及static语句块共同产生的。

示例

Java编译器并不会为所有的类都产生<clinit>初始化方法。如果一个类既没有赋值语句,也没有static语句块,那么生成的<clinit>方法就应该为空,因此编译器就不会为该类插入<clinit>方法。

对于<clinit>方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。

ClassLoader (从系统外部获得class二进制数据流)

ClassLoader在整个装载阶段,只能影响类的加载,而无法通过ClassLoader改变类的连接和初始化行为。

主要方法

· public Class<?>loadClass(String name) throws ClassNotFoundException
给定一个类名,加载一个类,返回代表这个类的Class实例,如果找不到类,则返回ClassNotFoundException异常。

· protected final Class<?>defineClass(byte[] b,int off,int len)

· protected Class<?>findClass(String name) throws ClassNotFoundException

· protected final Class<?>findLoadedClass(String name)

在ClassLoader的结构中,还有一个重要的字段:parent。它也是一个ClassLoader的实例,这个字段所表示的ClassLoader称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader可能会将某些请求交给自己的双亲处理

ClassLoader的分类 (启动类加载器、扩展类加载器、应用类加载器)

系统的核心类就是由启动类加载器进行加载的,它也是虚拟机的核心组件,没有对象与之对应。扩展类加载器和应用类加载器都有对应的Java对象可用。

任何在启动类加载器中加载的类是无法获得其ClassLoader实例的,会获得Null。

双亲委托模式 (双亲为null有两种情况:第一,双亲就是启动类加载器;第二,当前加载器就是启动类加载器)

在尝试加载时,会先请求双亲处理,如果请求失败,则会自己加载

判断类是否加载时,应用类加载器会顺着双亲路径往上判断,直到启动类加载器 (方向上请参见上图)。但是启动类加载器不会往下询问,这个委托路线是单向的,理解这点很重要

如何解决不会往下询问的弊端

这两个方法分别是取得设置在线程中的上下文加载器和设置一个线程的上下文加载器。通过这两个方法,可以把一个ClassLoader置于一个线程实例中,使该ClassLoader成为一个相对共享的实例。默认情况下,上下文加载器就是应用类加载器,这样即使启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类。

双亲模式的类加载方式是虚拟机默认的行为,但并非必须这么做,通过重载ClassLoader可以修改该行为。

热替换

对Java来说,并非天生就支持热替换,如果一个类已经加载到系统中,通过修改类文件无法让系统再来加载并重定义这个类。因此,在Java中实现这一功能的一个可行的方法就是灵活运用ClassLoader

** 由不同ClassLoader加载的同名类属于不同的类型,不能相互转化和兼容

** 两个不同ClassLoader加载同一个类,虚拟机内部会认为这两个类是完全不同的


字节码执行

--稍后更新--

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值