目录
1.Java内存区域与内存溢出异常
1.1运行时数据区域
1.1.1 程序计数器
内存空间小,线程私有。字节码解释器工作是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
1.1.2 Java 虚拟机栈
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
1.1.3 本地方法栈
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
1.1.4 Java 堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
1.1.5 方法区
线程共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。为堆得一个逻辑部分,名叫Non-Heap(非堆)
java虚拟机规范:当方法区无法满足内存分配需求时候,抛出OutOfMemoryError异常。
现在用一张图来介绍每个区域存储的内容。
1.1.6 运行时常量池
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
1.1.7 直接内存
非虚拟机运行时数据区的部分
- 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
- 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。
1.2 HotSpot 虚拟机对象探秘
深入探讨HotSpot虚拟机在java堆中对象创建分配、内存布局、访问定位
1.2.1 对象的创建
遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。
类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。
前面讲的每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。
内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。
1.2.2 对象的内存布局
HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。
实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。
1.2.3 对象的访问定位
使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。
通过句柄访问
Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。
详情如图:
使用直接指针访问
reference 中直接存储对象地址
比较:
使用句柄的最大好处: reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。
直接指针访问的最大好处:速度快,节省了一次指针定位的时间开销。
如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。
1.3 实战:OutOfMemoryError异常
1.3.1 堆溢出:
Java堆存储对象实例,不断创建对象,且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,当对象到达堆内存的最大容量时就会报异常。
参数设置:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/jvmdump/HeapOOM.dump
-Xms20M
-Xmx20M
-XX:+PrintGCDetails
-Xms : 初始堆大小; -Xms20m:表示初始堆20M
-Xmx : 最大堆大小; -Xmx20m : 表示最大可用20M
+HeapDumpOnOutOfMemoryError: 将溢出转存dump快照
-XX:HeapDumpPath :转存的dump快照通常都需要指定一个路径。然后分析结果
+ 表示使用
- 表示不使用
源码:
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
具体调试信息请看:https://blog.youkuaiyun.com/qq_31156277/article/details/79905357
1.3.2 虚拟机栈和本地方法栈溢出
对于HotSpot,不区分虚拟机栈和方法栈,栈容量只由-Xss设置
第一个测试:
单个线程下,由于栈帧太大或是虚拟机栈容量太小,内存无法分配的时候,虚拟机抛出StackOverflowError。
-Xss128k idea64位
package test1;
public class StackOverflowError {
private int stacklength=1;
public void stackLeak(){
stacklength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
StackOverflowError sof = new StackOverflowError();
try{
sof.stackLeak();
}catch(Throwable e){
System.out.println("stack length :"+ sof.stacklength);
throw e;
}
}
}
第二个测试:
1.3.3 方法区和运行时常量池溢出
1.3.4 本机直接内存溢出
小结:内存如何划分、哪部分区域、什么样的代码和操作导致内存溢出异常
2.垃圾收集器与内存分配策略
2.1 概述
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。
2.2 对象已死吗?
内存回收之前要做的就是判断哪些对象是‘死’的(不能被任何途径使用的对象),哪些是‘活’的。
2.2.1 引用计数法
给对象添加一个引用计数器。但是难以解决对象之间循环引用问题。主流java虚拟机未使用此方法。
从图中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。
2.2.2 可达性分析法
通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。
可以作为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法中的JNI(一般说的Native方法)引用的对象
2.2.3 再谈引用
前面的两种方式判断存活时都与‘引用’有关。但是 JDK 1.2 之后,引用概念进行了扩充。
四种引用强度依次逐渐减弱
强引用
类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。
软引用
SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
弱引用
WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
虚引用
PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
2.2.4 生存还是死亡
即使在可达性分析算法中不可达的对象,也并非是“facebook”的,这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。
finalize() 方法只会被系统自动调用一次。
2.2.5 回收方法区
在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。
永久代垃圾回收主要两部分内容:废弃的常量和无用的类。(jdk1.8无永久代)
判断废弃常量:一般是判断没有该常量的引用。
判断无用的类:要以下三个条件都满足
- 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法
2.3 垃圾回收算法
2.3.1 标记 —— 清除算法
标记需要回收的对象,完成后统一回收。
两个不足:
- 效率不高
- 空间会产生大量碎片
2.3.2 复制算法
把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。
解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。
2.3.3 标记-整理算法
不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。
2.3.4 分代收集算法
根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。
新生代
每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。
老年代
老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。
2.4 HotSpot 的算法实现
待更
2.5 垃圾回收器
收集算法是内存回收的方法论,而垃圾回收器是内存回收的具体实现。
说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。
2.5.1 Serial 收集器
这是一个单线程收集器。只会使用一个 CPU 或一条收集线程去完成收集工作,且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。虚拟机运行在Client模式下的默认新生代收集器。
优点:无线程交互开销,简单高效
缺点: 工作线程会因内存回收停顿
2.5.2 ParNew 收集器
Serial 收集器的多线程版本。
并行:Parallel
指多条垃圾收集线程并行工作,此时用户线程处于等待状态
并发:Concurrent
指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。
2.5.3 Parallel Scavenge 收集器
这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。
作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。
2.5.4 Serial Old 收集器
收集器的老年代版本,单线程,使用 标记 —— 整理。
2.5.5 Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用
标记 —— 整理
2.5.6 CMS 收集器
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。
运作步骤:
初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
并发标记(CMS concurrent mark):进行 GC Roots Tracing
重新标记(CMS remark):修正并发标记期间的变动部分
并发清除(CMS concurrent sweep)
缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除 算法带来的空间碎片
2.5.7 G1 收集器
面向服务端的垃圾回收器。
优点:并行与并发、分代收集、空间整合、可预测停顿。
运作步骤:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
2.6 内存分配与回收策略
2.6.1 对象优先在 Eden 分配
对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。
一般来说 Java 堆的内存模型如下图所示:
新生代 GC (Minor GC)
发生在新生代的垃圾回收动作,频繁,速度快。
老年代 GC (Major GC / Full GC)
发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。
2.6.2 大对象直接进入老年代
大对象指很长的字符串或数组,写程序时候应当避免朝生夕灭的大对象
-XX:PretenureSizeThreshold=? 大于这个设置值得直接分配到老年代 此参数只对Serial和ParNew收集器有效
2.6.3 长期存活的对象将进入老年代
每个对象都会被定义一个对象年龄计数器,每熬过一次Minor GC,年龄增加一岁。
参数-XX:MaxTenuringThreshold=? 默认15
2.6.4 动态对象年龄判定
动态对象年龄判断,主要是被TargetSurvivorRatio=50(默认值)这个参数来控制。而且算的是年龄从小到大的累加和,而不是某个年龄段对象的大小。
扩充:https://blog.youkuaiyun.com/u014493323/article/details/82921740
https://blog.youkuaiyun.com/zero__007/article/details/52797684
2.6.5 空间分配担保
1、准备在新生代进行MinorGC时,首先检查“老年代”最大连续空间区域的大小是否大于新生代所有对象的大小。
2、如果老年代能装下所有新生代对象,MinorGC没有风险,进行MinorGC
3、老年代无法装下,垃圾收集器进行一次预测:根据以往MinorGC过后存活对象的平均数来预测这次MinorGC后存活对象的平均数。
(1)以往平均数小于当前老年代最大的连续空间,就进行MinorGC。
(2)大于,则进行一次Full GC,通过清楚老年代中废弃数据来扩大老年代空闲空间,以便给新生代做担保。
注意事项:
1. 分配担保是老年代为新生代作担保;
2. 新生代中使用“复制”算法实现垃圾回收,老年代中使用“标记-清除”或“标记-整理”算法实现垃圾回收,只有使用“复制”算法的区域才需要分配担保,因此新生代需要分配担保,而老年代不需要分配担保。
3.HandlePromotionFailure 在JDK1.6 update24后不再影响担保策略。
小结:垃圾收集算法、几款jdk1.7的垃圾收集器特点和原作原理、自动内存分配和回收的主要原则
3.虚拟机性能监控与故障处理工具
3.1 概述
处理运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)的工具
3.2JDK命令行工具
3.2.1 jps虚拟进程状况工具
名称定位,显示主类
jps [options] [hostid]
- -q: 只输出LVMID(本地虚拟机唯一ID),省略主类的名称
- -m:输出虚拟机进程启动时的传递给主类main()函数的参数
- -l:输出主类的全名,如果进程执行的是Jar包,输出Jar路径
- -v: 输出虚拟机进程启动时的JVM参数
3.2.2 jstat虚拟机统计信息监视工具
jstat 是用于识别 虚拟机 各种 运行状态信息 的命令行工具。它可以显示 本地 或者 远程虚拟机 进程中的 类装载、内存、垃圾收集、jit 编译 等运行数据。
jps命令格式:
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
option 参数含义
-t:可以在打印的列加上 timestamp 列,用于显示系统运行的时间。
-h 可以在 周期性数据 的时候,可以在指定输出多少行以后输出一次 表头
vmid:进程ID
lines: 表头 与 表头 的间隔行数。
interval: 执行每次的 间隔时间,单位为 毫秒
count: 用于指定输出记录的 次数,缺省则会一直打印。
option 值的选项
class: 显示 类加载 ClassLoad 的相关信息;
compiler: 显示 JIT 编译 的相关信息;
gc: 显示和 gc相关的 堆信息;
gccapacity: 显示 各个代 的 最大、最小容量 以及 使用情况;
gcmetacapacity: 显示 元空间metaspace 的大小;
gcnew: 显示 新生代 信息; gcnewcapacity: 显示 新生代大小 和 使用情况;
gcold: 显示 老年代 和 永久代 的信息;
gcoldcapacity: 显示 老年代 的大小;
gcutil: 显示垃圾回收信息,已经使用空间与总空间的占空比;
gccause: 显示 垃圾回收 的相关信息(同 -gcutil),同时显示 最后一次 或 当前 正在发生的垃圾回收的诱
printcompilation: 输出 JIT 编译 的方法信息
查看GC信息
其中S0,S1表示新生代两个Survivor区,E代表的是新生代的 Eden区,C的 意思是容量,u表示已经使用的意思,O表示 的老年代,M表示方法区。F和Y则 表示 fullGC和 minorGC(即年轻代GC)
3.2.3 jinfo 配置信息查看工具
这个命令作用是实时查看和调整 虚拟机运行参数。
jinfo 命令格式
jinfo [-options] <pid>
3.2.4 jmap java内存映射工具
该 命令主要是来 查看内存使用的详细信息
命令格式
jmap [-options] <pid>
选项参数的意思:
-heap:显示 Java堆中的详细信息
-histo: 显示对象的统计 消息
-clstats:显示 类加载 的统计信息
显示java对中的 详细信息
jmap -heap 2438
3.2.6 jstack 堆栈跟踪工具
该命令用于生成 java 虚拟机当前时刻的 线程快照。线程快照 是当前 java 虚拟机内 每一条线程 正在执行的 方法堆栈 的 集合。生成线程快照的主要目的是定位线程出现 长时间停顿 的原因,如 线程间死锁、死循环、请求外部资源 导致的 长时间等待 等等
命令格式:
jstack [-options] <pid>
option值选项:
- f 当正常输出请求 不被响应 时,强制输出 线程堆栈
- l 输出锁信息
- m显示C++堆栈
显示当前线程快照并显示锁信息
jstack -l 2438
prio:线程的优先级
tid:线程id
nid:操作系统映射的线程id, 非常关键,后面再使用jstack时补充;
0x00007fbdf1920000:表示线程栈的起始地址
3.2.7 HSDIS:JIT生成代码反汇编
处理混合模式的调试,分析程序的执行问题
通过-XX:PrintAssembly指令把动态生成的本地代码还原为汇编代码输出
3.3 JDK的可视化工具
3.3.1 JConsole :Java监视与管理控制台
https://blog.youkuaiyun.com/qq_31156277/article/details/80035430
3.3.2 VisualVM:多合一故障处理工具
https://www.cnblogs.com/xifengxiaoma/p/9402497.html
小结: 随JDK发布的6个命令行工具和两个可视化故障处理工具
4.调优案例分析与实战
4.1案例分析
待填
4.2Eclipse运行速度调优
https://blog.youkuaiyun.com/weixin_41262453/article/details/87370536
4.3idea运行速度调优
书中提到:
- 编译时间和类加载时间优化 :取消字节码验证
- 调整内存设置控制垃圾收集频率 :最大最小参数值一样
- 选择收集器降低延迟: cms收集器
打开ides安装目录里的bin文件夹,打开idea.exe.vmoptions(64位的叫idea64.exe.vmoptions)看到的是idea安装完成后默认的VM参数配置(其实里面还有一些其他的配置,最好不要动它们):
idea2018.3 默认初始配置:
-Xms128m
-Xmx750m
-XX:ReservedCodeCacheSize=240m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50
-ea
-Dsun.io.useCanonCaches=false
-Djava.net.preferIPv4Stack=true
-Djdk.http.auth.tunneling.disabledSchemes=""
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
更改配置为: 具体请根据自己的硬件条件调节分配大小
-server(tips:无需此参数,64位JVM默认server模式,并没有client模式)
-Xms2048m
-Xmx2048m
-Xverify:none 关闭字节码验证 idea代码可信
-XX:+DisableExplicitGC 关闭显示调用System.gc()
-XX:MetaspaceSize=512m
-XX:ReservedCodeCacheSize=240m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50
-ea
-Dsun.io.useCanonCaches=false
-Djava.net.preferIPv4Stack=true
-Djdk.http.auth.tunneling.disabledSchemes=""
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
顺便提一下,可用以下命令来输出一个名为gclog.log的gc日志来查看各种GC的详细信息
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-Xloggc:gclog.log
小结: 虚拟机内存管理与垃圾收集
1-4章 理论知识, 异常现象,代码,工具,案例,实战。
5.类文件结构
6.虚拟机类加载机制
6.1概述:
程序运行期间,描述类数据的Class文件加载到内存,数据校验,转换解析,初始化,形成虚拟机使用的的java类型。
虚拟机与程序生命周期:
- 执行System.exit()
- 程序正常执行结束
- 程序有异常或错误而终止
- 操作系统错误导致java虚拟机结束
6.2类的生命周期(7个阶段):
1.加载:查找并加载类的二进制数据
2.连接:
--验证: 确保被加载的类的正确性
--准备:为类的静态变量分配内存,并将其初始化为默认值。
--解析:把类的符号引用转换为直接引用
3.初始化:为类的静态变量赋予正确的初始值
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)。
以下5种情况立即对类初始化:
- 遇到new,getstatic,putstatic,invokestatic 4个字节码指令
- java.lang.reflect 包对类反射调用的时候
- 父类还未初始化
- 用户指定一个要执行的主类(包含main()方法的类)
- JDK1.7 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄
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!");
}
}
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world!"
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
/**
* output : SuperClass init!
*
* 通过子类引用父类的静态对象不会导致子类的初始化
* 只有直接定义这个字段的类才会被初始化
*/
SuperClass[] sca = new SuperClass[10];
/**
* output :
*
* 通过数组定义来引用类不会触发此类的初始化
* 虚拟机在运行时动态创建了一个数组类
*/
System.out.println(ConstClass.HELLOWORLD);
/**
* output :
*
* 常量在编译阶段会存入调用类的常量池当中,本质上并没有直接引用到定义类常量的类,
* 因此不会触发定义常量的类的初始化。
* “hello world” 在编译期常量传播优化时已经存储到 NotInitialization 常量池中了。
*/
}
}
上述代码情况分别为:子类引用父类静态字段,数组定义引用类,调用类的常量
6.3类的加载过程(具体解释)
6.3.1 加载
- 通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。
数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因数组的类型靠类加载器去创建的,过程如下:
- 如果数组的组件类型是引用类型,那就递归采用类加载加载。
- 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
- 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。
内存中实例的 java.lang.Class 对象存在方法区中。作为程序访问方法区中这些类型数据的外部接口。
加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。
6.3.2 验证
连接的第一步,确保 Class 文件的字节流中的信息符合虚拟机要求。
文件格式验证
- 是否以魔数 0xCAFEBABE 开头
- 主、次版本号是否在当前虚拟机处理范围之内
- 常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
- Class 文件中各个部分集文件本身是否有被删除的附加的其他信息
- ……
只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。
元数据验证
- 此类是否有父类(除 java.lang.Object 之外)
- 这个类的父类是否继承了不允许被继承的类(final 修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)
这一阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。
字节码验证
- 保证任意时刻操作数栈的数据类型与指令代码序列都配合工作(不会出现按照 long 类型读一个 int 型数据)
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
……
这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。
符号引用验证
- 符号引用中通过字符创描述的全限定名是否能找到对应的类
- 在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
……
最后一个阶段的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。
符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。
6.3.3 准备
这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。
public static int value = 123;
这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 123的 putstatic 指令是程序被编译后,存放于 <clinit>() 方法中,所以初始化阶段才会对 value 进行赋值。
基本数据类型的零值
数据类型 | 零值 | 数据类型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | ‘\u0000’ | reference | null |
byte | (byte)0 |
特殊情况:如果类的字段属性表中有 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 123。
6.3.4 解析
这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用
符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量。
直接引用
直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和迅疾的内存布局实现有关
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。
6.3.5 初始化
前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。
6.4 类加载器
通过一个类的全限定名来获取描述此类的二进制字节流。
6.5.1 双亲委派模型
从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)
1,启动类加载器
加载 lib 下或被 -Xbootclasspath 路径下的类
2,扩展类加载器
加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类
3,引用程序类加载器
ClassLoader负责,加载用户路径上所指定的类库。
除顶层启动类加载器之外,其他都有自己的父类加载器。
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。
6.3.2 破坏双亲委派模型
keyword:线程上下文加载器(Thread Context ClassLoader)
小结:类加载5个阶段和工作原理,对虚拟机的意义
7.虚拟机字节码执行引擎
8.类加载及执行子系统的案例与实战
9.早期(编译期)优化
待更新·
10.晚期(运行期)优化
待更新
11.Java内存模型与线程
11.1内存模型
评价一个服务器性能的高低好坏的重要指标:每秒事务处理数(Transactions Per Second,TPS),代表一秒内服务端平均能响应的请求总数,与程序并发能力有着密切关系。
java虚拟机规范定义一种Java内存模型(Java Memory Model ,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,实现java程序在各种平台的能达到一致的内存访问效果。
目的: 定义程序中各个变量的的访问规则。变量包括 实例字段,静态变量,构成数组对象的元素
主内存和工作内存之间的交互 (8种)
操作 | 作用对象 | 解释 |
---|---|---|
lock | 主内存 | 把一个变量标识为一条线程独占的状态 |
unlock | 主内存 | 把一个处于锁定状态的变量释放出来,释放后才可被其他线程锁定 |
read | 主内存 | 把一个变量的值从主内存传输到线程工作内存中,以便 load 操作使用 |
load | 工作内存 | 把 read 操作从主内存中得到的变量值放入工作内存中 |
use | 工作内存 | 把工作内存中一个变量的值传递给执行引擎, 每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作 |
assign | 工作内存 | 把一个从执行引擎接收到的值赋接收到的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 |
store | 工作内存 | 把工作内存中的一个变量的值传送到主内存中,以便 write 操作 |
write | 主内存 | 把 store 操作从工作内存中得到的变量的值放入主内存的变量中 |
规则:
-
不允许read,load和store,write之一单独出现
-
线程不能丢弃最近assign操作,变量在工作内存改变后必须同步到主内存
-
volatile变量
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。
变量被定义为 volatile 的特性:
1,保证此变量对所有线程的可见性。但是操作并非原子操作,并发情况下不安全。
如果不符合
- 运算结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
就要通过加锁(使用 synchronized 或 java.util.concurrent 中的原子类)来保证原子性。
public class VolatileTest {
public static volatile int race = 0;
public static void increase(){
race++;
}
public static final int THREADS_COUNTS = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNTS];
for (int i=0;i<THREADS_COUNTS;i++){
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <10000 ; i++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 2){ //这是idea 所以是2
Thread.yield();
}
System.out.println(race);
}
}
注: 关于在idea中使用Thread.activeCount()的问题
https://blog.youkuaiyun.com/qq_42862882/article/details/89309399
2,禁止指令重排序优化。
通过插入内存屏障保证一致性。
https://blog.youkuaiyun.com/fumitzuki/article/details/81630048
12.线程安全与锁优化
正在更新。。。。
内容均出自周志明老师的《深入理解 Java 虚拟机:JVM高级特性与最佳实践》,建议人手一本纸质版。