第一章:书籍推荐与JVM相关面试题
1、面试题
https://blog.csdn.net/Y0Q2T57s/article/details/80682013#commentBox
https://blog.csdn.net/Javazhoumou/article/details/98954196
2、书籍推荐
https://detail.tmall.com/item.htm?spm=a230r.1.14.6.2dbd1b4e8hduBh&id=18622814595&cm_id=140105335569ed55e27b&abbucket=13
3、三种JVM
Sun公司的HotSpot
BEA公司的JRockit
https://baike.baidu.com/item/JRockit/5079300
IBM公司的J9 VM
http://book.51cto.com/art/201306/399155.htm
第二章:JVM虚拟机架构
第三章:JVM概念
3.1 类加载器
-
从文件系统或者网络加载class信息,把该class信息存放在一块称之为方法区的内存空间
-
双亲委派机制:出问题不要找我,找我的“上司”, 一层一层往上找
双亲委派机制有4种类加载器为:
自定义(UserClassLoader)->应用/系统(App/SystemClassLoader)->扩展类(ExtClassLoader)->启动(BootstrapClassLoader)类加载器。
加载过程简述:
当一个类被编译成.class
字节码文件进入到jvm的类加载器加载,类加载器首先默认按照顺序:自定义类加载器-->应用程序加载器-->拓展类加载器-->启动类加载器
把.class
直接一层一层往上传递委托给最上层的启动类加载器进行加载,如果启动类能加载这个类,那么他就会进行加载,如果不能,则传递委托给拓展类加载器进行加载;拓展类能加载这个类,那么他就会进行加载,如果不能,则传递委托给应用类加载器进行加载…这样一层一层传递委托下来,自定义类加载器是最后一层类加载器
public class Test00 {
public static void main(String[] args) {
Object object = new Object();
Test00 test00 = new Test00();
//System.out.println(object.getClass().getClassLoader());
System.out.println(test00.getClass().getClassLoader().getParent().getParent());
System.out.println(test00.getClass().getClassLoader().getParent());
System.out.println(test00.getClass().getClassLoader());
}
}
//如果打印结果为null,则表示是启动类加载器
- 沙箱安全机制: 保护程序安全 保护JAVA原生的JDK代码
双亲委派优点?
a.安全,可避免用户自己编写的类动态替换Java的核心类,如java.lang.String
b.避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)
问题:可不可以自己写个String类(也是自定义的String为何没加载到?)
不可以。因为在类加载中,会根据双亲委派机制去寻找当前java.lang.String是否已被加载。由于启动类加载器已在启动时候加载了该类,所以不会再次加载,因此使用的String是已在java核心类库加载过的String,而不是新定义的String。
3.2 执行引擎
负责解释JVM内部命令,翻译给操作系统执行
3.3 本地方法接口
Java开发中会碰到声明为 native的方法,如:public native int hashCode;
这是一个native方法。
为什么存在native方法呢?
native是与C++联合开发的时候用的!使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL( Dynamic Link Library ),由java去调用。
Java不是完美的,Java的不足除了体现在运行速度上要比传统的c/C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能
3.4 本地方法栈
内存中的一块区域负责登记 native方法
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而==本地方法栈则是为虚拟机使用到的Native方法服务。==虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
3.5 PC寄存器(程序计数器)
是线程私有的,就是一个指针,指向方法区中的方法字节码,指向下一个方法所在的地址,灰色的这几个(Java栈、本地方法栈、程序计数器)都不会被垃圾回收器回收,因为生命周期短
3.6 方法区
(1)方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。
(2)静态变量(类变量)、常量、类信息(class/构造方法/接口定义)、运行时常量池存在方法区中
(3)类加载器加载的类信息就放到方法区,该区归所有线程共享
(4)实例变量存在堆内存中,和方法区无关
3.7 Java栈
栈也叫栈内存,在线程创建时创建,生命周期跟随线程生命周期,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,是线程私有的。
8种基本类型的变量、对象的引用变量、实例方法 都是在函数的栈内存中分配。
栈存储什么?
栈帧中主要保存3 类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
栈操作(Operand Stack):记录出栈、入栈的操作;
栈帧数据(Frame Data):包括类文件、方法等等
3.8 栈+堆+方法区的交互关系
第四章:堆
4.1 Heap堆
堆 内存逻辑上分为三部分: 新生代 、老年代、 永久区(Java7以前)
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。
当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象实例进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再次垃圾回收,满足一定条件后(年龄增长)再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”
java.lang.OutOfMemoryError: Java heap space
新生代(新生区)+老年代(养老区)+永久代(Java1.7以前)
4.2 永久区(方法区)
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class
、Interface
的元数据(也就是核心类库的类和接口),也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
Jdk1.6及之前: 有永久代,常量池在方法区
Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池在堆
Jdk1.8及之后: 无永久代,常量池在元空间
因为开发JDK那帮人已经变了,有他们自己到理念
第五章:通过程序理解JVM
5.1 堆分配参数
初始堆大小和最大堆大小可以设置为一样,可以减少GC回收次数 提升性能
5.2 堆分配参数
-XX:NewRatio设置新生代和老年代的比例
-XX:SurvivorRatio 设置eden:from:to的比例
5.3 堆溢出处理
Java内存分析工具 https://blog.csdn.net/hpc19950723/article/details/53561659
OutOfMemoryError: Java heap space
idea工具https://blog.csdn.net/wytocsdn/article/details/79258247
5.4 Mat的使用
Shallow heap & Retained heap的区别
https://www.iteye.com/blog/bjyzxxds-1532937
Mat工具详细介绍https://blog.csdn.net/liao0801_123/article/details/82900874
5.5 栈配置
StackOverflowError
栈溢出
public class Test04 {
//-Xss1m
//-Xss5m
//栈调用深度
private static int count;
//32977 默认
//23580 1m
//214449 5m
public static void recursion(){
count++;
recursion();
}
public static void main(String[] args){
try {
recursion();
} catch (Throwable t) {
System.out.println("调用最大深入:" + count);
t.printStackTrace();
}
}
}
5.6 对象创建在eden区
public class Test05 {
public static void main(String[] args) {
//初始的对象在eden区
//例子一 参数:-Xmx64M -Xms64M -XX:+PrintGCDetails
// for(int i=0; i< 5; i++){
// byte[] b = new byte[1024*1024];
// }
//测试进入老年代的对象
//参数:-Xmx1024M -Xms1024M -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails
//-XX:+PrintHeapAtGC
// for(int k = 0; k<20; k++) {
// for(int j = 0; j<300; j++){
// byte[] b = new byte[1024*1024];
// }
// }
//1024*1000<1024*1024
//参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1024000
Map<Integer, byte[]> map=new HashMap<>();
for (int i = 0; i < 5; i++) {
byte[] b=new byte[1024*1024];
map.put(i, b);
}
}
}
5.7 方法区
方法区溢出 OutOfMemoryError:PermGen space
5.8 JVM详细配置
http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
第六章:垃圾回收算法
6.1 可达性分析
从GC Roots的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用,很显然object5相关的引用已经没用了。
6.2 引用计数算法
如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。无法解决循环引用的情况
6.3 标记清除算法
原理:
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被 称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
标记:
从引用根节点开始标记所有被引用的对象。标记的过程其实就是遍历所有的GC Roots, 然后将所有GC Roots可达的对象标记为存活的对象。
清除:
遍历整个堆,把未标记的对象清除。
缺点:
此算法需要暂停整个应用,会产生内存碎片
6.4 标记压缩算法(老年代)
用于老年代进行垃圾回收
6.5 复制算法(新生代)
用于新生代进行垃圾回收
原理:
对象产生(new)的时候在eden区,当eden空间用完时,程序又需要创建对象,这个时候触发JVM垃圾回收,不再被其他对象所引用的对象就会被销毁,然后将存活对象移动到from,下次再触发垃圾回收的时候eden+from作为主战场,存活对象移动到to区(这个时候to变为from) ,原先form变为to(谁空谁为to);
从from到to的过程每次复制一次对象年龄增长一岁,当年龄达到一定年龄(默认15岁)就会移动到养老区;
若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”
交换:
经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。谁空谁为to,
因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲(to)和活动区(from)间,而另外80%的内存(Eden),则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。
缺点 :
1、它浪费了一半的内存,这太要命了。
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
老年代和新生代是否可以交换回收算法?
老年代GC的时候 存活对象较多 如果采用复制算法 效率不高
6.6 分代/分区算法
分代/分区算法也就是新生代和老年代算法的综述(标记压缩算法+复制算法)
第七章:垃圾回收器
垃圾回收收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。
7.1 并行和并发的区别
https://www.cnblogs.com/xc-chejj/p/10813692.html
并发(Concurrent):
在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。如:打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的
并行(Parallel):
当系统有一个以上CPU(或CPU多核)时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)
其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行
总结:
所以并发是在一段时间内宏观上多个程序同时运行,并行是在某一时刻,真正有多个程序在运行。
并行和并发的区别:
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的
只有在多CPU或者一个CPU多核的情况中,才会发生并行。
否则,看似同时发生的事情,其实都是并发执行的
7.2 Serial收集器
Serial是单线程执行垃圾回收的。当需要执行垃圾回收时,程序会暂停一切手上的工作,然后单线程执行垃圾回收。(用于新生代的垃圾回收)
因为新生代的特点是对象存活率低,所以收集算法用的是复制算法,把新生代存活对象复制到老年代,复制的内容不多,性能较好。
7.3 ParNew收集器
ParNew同样用于新生代,是Serial的 多线程版本,并且在 参数 、算法(同样是复制算法)上也完全和Serial相同。
Par是Parallel的缩写,但它的并行仅仅指的是收集多线程并行,并不是收集和原程序可以并行进行。ParNew也是需要暂停程序一切的工作,然后多线程执行垃圾回收。
因为是多线程执行,所以在多CPU下,ParNew效果通常会比Serial好。但如果是单CPU则会因为线程的切换,性能反而更差
7.4 Parallel Scavenge收集器
新生代的收集器,同样用的是复制算法,也是并行多线程收集。与ParNew最大的不同,它关注的是垃圾回收的吞吐量。
这里的吞吐量指的是 总时间与垃圾回收时间的比例
。这个比例越高,证明垃圾回收占整个程序运行的比例越小。
7.5 Serial Old收集器
老年代的收集器,与Serial一样是单线程,不同的是算法用的是标记压缩(Mark-Compact)
因为老年代里面对象的存活率高,如果依旧是用复制算法,需要复制的内容较多,性能较差。并且在极端情况下,当存活为100%时,没有办法用复制算法。所以需要用标记压缩Mark-Compact,以有效地避免这些问题
7.6 Parallel Old收集器
老年代的收集器,是Parallel Scavenge老年代的版本。其中的算法替换成Mark-Compact
7.7 CMS收集器
CMS(Concurrent Mark Sweep)同样是老年代的收集器。它关注的是垃圾回收最短的停顿时间(低停顿),在老年代并不频繁GC的场景下,是比较适用的。命名中用的是concurrent,而不是parallel,说明这个收集器是有与工作执行并发的能力的。MS则说明算法用的是Mark Sweep算法,来看看具体地工作原理,CMS整个过程比之前的收集器要复杂,整个过程分为四步:
初始标记(initial mark)
单线程执行,需要“Stop The World”,但仅仅把GC Roots的直接关联可达的对象给标记一下,由于直接关联对象比较小,所以这里的速度非常快。
并发标记(concurrent mark)
对于初始标记过程所标记的初始标记对象,进行并发追踪标记,此时其他线程仍可以继续工作。此处时间较长,但不停顿。
重新标记(remark)
在并发标记的过程中,由于可能还会产生新的垃圾,所以此时需要重新标记新产生的垃圾。此处执行并行标记,与用户线程不并发,所以依然是“Stop The World”,时间比 初始时间要长一点。
并发清除(concurrent sweep)
并发清除之前所标记的垃圾。其他用户线程仍可以工作,不需要停顿。
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
缺点:
Mark Sweep算法会导致内存碎片比较多
CMS的并发能力依赖于CPU资源,所以在CPU数少和CPU资源紧张的情况下,性能较差;并发清除阶段,用户线程依然在运行,所以依然会产生新的垃圾,此阶段的垃圾并不会在本次GC中回收,而放到下次。所以GC不能等待内存耗尽的时候才进行GC,这样的话会导致并发清除的时候,用户线程可以利用的空间不足。所以这里会浪费一些内存空间给用户线程预留。
有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?
答案其实很简答,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact更适合“Stop the World”这种场景下使用
7.8 G1收集器
G1,GarbageFirst,在JDK1.7版本正式启用,是当时最前沿的垃圾收集器。G1可以说是CMS的终极改进版,解决了CMS内存碎片、更多的内存空间登问题。虽然流程与CMS比较相似,但底层的原理已是完全不同。高效益优先。G1会预测垃圾回收的停顿时间,原理是计算老年代对象的效益率,优先回收最大效益的对象。
堆内存结构的不同。以前的收集器分代是划分新生代、老年代、持久代等。
7.9 ZGC收集器
在JDK11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒 它是一款低停顿 高并发 的收集器。ZGC几乎在所有地方并发执行的,除了初始标记的是STW的。所以停顿时间几乎就耗费在初始标记上,这部分的实际是非常少的。那么其他阶段是怎么做到可以并发执行的呢?ZGC主要新增了两项技术,一个是着色指针Colored Pointer ,另一个是 读屏障Load Barrier 。
着色指针Colored Pointer
ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked1、Marked0(ZGC仅支持64位平台),以标记该指向内存的存储状态。相当于在对象的指针上标注了对象的信息。注意,这里的指针相当于Java术语当中的引用。在这个被指向的内存发生变化的时候(内存在Compact被移动时),颜色就会发生变。
在G1的时候就说到过,Compact阶段是需要STW,否则会影响用户线程执行。那么怎么解决这个问题呢?
读屏障Load Barrier由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色了。那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。
把这两项技术联合下理解,与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。
ZGC虽然目前还在JDK 11还在实验阶段,但由于算法与思想是一个非常大的提升,相信在未来不久会成为主流的GC收集器使用
7.10 AliGC收集器
https://yq.aliyun.com/articles/277268
AliGC是阿里巴巴JVM团队基于G1算法, 面向大堆(LargeHeap)应用场景
指定场景下的对比
7.11 垃圾回收器分类图解