【一、JVM相关知识】
1. JVM的一些概念
JVM的种类有很多,常见的三大虚拟机有HotSpot、JRockit和IBM J9。我们一般泛指的也是用的最广泛的JVM就是HotSpot,它也是Sun/Oracle JDK和Open JDK的默认JVM。
JVM虚拟机包含类装载子系统、运行时数据区(JVM内存模型)和字节码执行引擎。
JVM内存模型则由堆、方法区(元空间)、(线程)栈、本地方法栈、和程序计数器组成。
一个.java文件在JVM中的执行流程:(如上图)
首先JVM把我们在开发工具编写的Java代码(.java文件)转化为字节码(.class文件),然后由类加载器将字节码加载到内存中(即运行时数据区),然后再由字节码执行引擎把字节码(可以理解成是JVM指令集,类似汇编语言)解析成操作系统可执行的二进制机器码,交由CPU分配线程执行逻辑。
2. JVM内存模型
首先要明确,Java内存模型和JVM内存模型这两个是完全不同的概念。
JVM内存模型具体指的是JVM运行时的数据分区。
Java内存模型(JMM)是一种规范,目的是解决多线程并发编程通过内存共享进行通信时,本地内存数据不一致、编译器对代码执行重排序、处理器对代码乱序执行等带来的问题,即保证内存共享的正确性(可见性、有序性、原子性)。
2.1 方法区(元空间)存放常量+静态变量+类元信息。
2.2 本地方法会用native关键字标志,底层是用C++实现的,所以本地方法栈和线程栈的区别就是一个是C++实现的栈一个是Java自己实现的栈。
2.3 每个Java线程会分配一个程序计数器、本地方法栈和线程栈,线程栈(也叫虚拟机栈)中根据线程调用的方法划分不同的栈帧区域。
(拓展1:线程栈也有可能存放java实例对象。比如:方法中创建了一个对象类型的局部变量,本来这个对象的引用只归属于该线程,但是如果return的时候返回的是实例对象,则发生了引用逃逸,该对象实例可能被其他方法或者变量修改。发生引用逃逸的好处是能够是对象直接在栈上分配从而减轻堆分配对象的压力,降低GC次数;不好的地方则是局部变量管理混乱,容易导致业务逻辑出错。
拓展2:java的对象实例在堆里,c++的对象实例在方法区;虚拟机栈的对象引用指向堆,堆的对象(Java Instance)引用指向方法区(C++ Instance)。)
每个栈帧区域包含局部变量表、操作数栈、动态链接和方法出口:
----局部变量表存储基本类型的变量和对象引用,而对象实例的地址统一都放到堆内存里;
----操作数栈则是临时缓存要做计算处理的变量;
----动态链接是,比如根据一个方法名(JVM识别为符号)找到方法区域对应的内存地址;
----方法出口则是记录代码执行完毕的条件。
2.4 堆内存被划分为两个不同的区域,年轻代Young和老年代Old,年轻代又划分为Eden和两个Survivor区。JVM堆中的GC又分为Minor GC(YGC)和Major GC(OGC)。
(注:永久代Perm是JDK7的特性,存放的是应用元数据(应用中使用的类和方法),JDK8后被取消。永久代是JVM方法区的实现方式之一,JDK8起,被元空间(与堆不相连的本地空间)取而代之。)
简单地讲,JVM触发垃圾回收的流程是这样的:
新创建出来的对象都会被放到Eden区,当Eden区满了以后,会触发Minor GC,JVM根据可达性分析算法,标记那些有引用的对象,并复制到正在活动的survivor区s0(此时s1为非活动状态),然后将那些未被标记的无效对象清除,释放空间。每经历过一次Minor GC存活下来的对象,年龄会+1。当第二次触发Minor GC的时候,会把Eden区和活跃状态s0区的有效对象进行标记并复制到s1,此时s1变为活动状态,s0变为非活动状态,然后将未标记的无效对象清除。
一般的GC算法实现中,当对象的年龄达到15时,会被送往老年代。老年代的对象只有当触发Major GC或Full GC时,才会被清除。
Full GC是重量级的垃圾回收,严重影响程序运行。当小幅度的GC调整都没办法腾出足够的资源空间或者老年代的空间无法满足后续较大的对象存入时,就会触发Full GC操作,停止所有在堆中运行的线程并执行清除动作。
3. JVM垃圾回收
问题一:JVM是如何找到垃圾的? ------根可达算法(Root Search)
(引用计数算法无法解决循环引用的问题,可达性分析算法成为主流)
问题二:那么JVM如何判定对象的起点(GC Roots) ?
- 所有被同步锁持有的对象,比如被 synchronize 持有的对象;
- 字符串常量池里的引用(String Table);
- 类型为引用类型的静态变量;
- 虚拟机栈中引用对象;
- 本地方法栈中的引用对象。
问题三:JVM找到垃圾后是如何清除的?(核心的三种思想)
按照推出的时间顺序:标记清除——>拷贝清除——>标记整理/压缩
在分代算法上的实现:
年轻代gc算法:Serial、Parallel Scanvenge、ParNew
拷贝清除------解决了空间碎片化的问题,但是会对空间有所浪费,效率高。
老年代gc算法:Serial Old、Parallel Old、CMS
标记清除------简单但会导致内存空间碎片化。
标记整理------将打标记的垃圾对象清除以后,再把剩余碎片的对象都统一往边界挪,腾出连续的内存空间,效率比copy略低。但这适用于OGC,因为老年代存活大量对象,拷贝反而效率低。
问题四:现行的垃圾回收算法有哪些?怎么组合使用?
从Java推出依赖,GC算法从最初的单线程序列化,到并行序列化,再到并发,以及比较新的G1的并行于并发,随着硬件的内存空间越来越大所以GC算法也是在不断地跟随演进。
分代算法:(ygc和ogc需要组合) 序列化单线程(stop the world) Serial+Serial Old 序列化多线程(stop the world) Parallel Scanvenge+Parallel Old
(jdk1.8默认使用的gc算法)CMS并发回收(三色标记算法,即对象的三种状态) Serial+CMS 或者 ParNew+CMS
容易产生浮动垃圾,且在最后的remark阶段依然会产生stw。非分代算法:(无需组合策略) G1(三色标记优化+SATB) 可以看作是对CMS算法的改进。
物理上分区,逻辑上分代。分Region回收,优先回收花费时间少垃圾比例高的region。
调优变得简单,G1可以自己动态指定年轻代与老年代的占比。ZGC 彻底不分代
问题五:为什么年轻代中的eden区和s0、s1的空间比例的默认分配是8:1:1 ?
------根据统计学的概念,ygc(minor gc)一般能回收 90% 的对象。
问题六:对象什么情况下会进入老年代?
其实从两方面来理解,触发对象进入老年代的条件一个是年龄,另一个则是内存空间。
- 对象经过几次gc,熬到设定的年龄阈值,就会进入老年代。
在PS+PO的垃圾回收算法中默认阈值是15,CMS算法中默认阈值是6。
(可通过参数-XX:MaxTenuringThreshold配置)- 根据动态年龄判定规则,在一次YGC后,如果某一个Survivor区域中的几个年龄对象加起来超过了该survivor区内存的一半,那么就从最小的年龄加起,比如年龄1+年龄2+年龄3的对象大小总和超过了该survivor区内存的一半,此时年龄大于3的对象就会晋升到老年代。
- 大对象超出了JVM设置的限定值,就会直接分配到老年代。
(通过参数-XX:PretenureSizeThreshold配置)- YGC后,存活下来的对象太多,survivor区放不下去了就直接移到老年代。
问题七:说说内存泄漏?
可以理解成c++中的野指针使无用对象一直有被引用导致内存空间无法释放,即某块内存被遗漏回收了,容易引发OOM内存溢出。
发生内存泄漏的根本原因是,如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。
一些容易发生内存泄漏的情景:
- 全局变量指向了方法中new的一个对象实例,在使用完后没有让该全局变量指向null。(某种程度上也可以称之为引用逃逸,解决的办法就是尽量减小对象的作用域或手动设置null)
- 数据库连接、socket连接、io连接等对象在使用完后没有显示调用close方法释放资源。
- 单例模式的对象持有其他对象的引用。
- 内部类和外部模块的引用。
- ......
4. JVM调优
JVM关于垃圾回收的调优主要关注的两个指标:吞吐量、响应时间。
4.1 JVM常用调优参数
JVM 调优主要是根据实际的硬件配置信息重新设置 JVM 参数来进行调优的,例如,硬件的内存配置很高,但 JVM 因为是默认参数,所以最大内存和初始化堆内存很小,这样就不能更好地利用本地的硬件优势了。因此,需要调整这些参数,让 JVM 在固定的配置下发挥最大的价值。
-Xmx,设置最大堆内存大小;
-Xms,设置初始堆内存大小;
-XX:MaxNewSize,设置新生代的最大内存;
-XX:MaxTenuringThreshold,设置新生代对象经过一定的次数晋升到老生代;
-XX:PretrnureSizeThreshold,设置大对象的值,超过这个值的对象会直接进入老生代;
-XX:NewRatio,设置分代垃圾回收器新生代和老生代内存占比;
-XX:SurvivorRatio,设置新生代 Eden、Form Survivor、To Survivor 占比。
我们要根据自己的业务场景和硬件配置来设置这些值。例如,当我们的业务场景会有很多大的临时对象产生时,因为这些大对象只有很短的生命周期,因此需要把“-XX:MaxNewSize”的值设置的尽量大一些,否则就会造成大量短生命周期的大对象进入老生代,从而很快消耗掉了老生代的内存,这样就会频繁地触发 full gc,从而影响了业务的正常运行。
4.2 JVM问题排查工具
6 个基本命令行工具:jps、jstat、jinfo、jmap、jhat、jstack
2 个视图排查工具:JConsole 和 JVisualVM
这些工具都在jdk的bin目录下可以找的到,具体使用参考:Java 源码剖析第 26 讲
5. 对象的引用(强软弱虚)
强引用:
------Object object=new Object()
一个对象只要有强引用存在就不会被GC。
软引用:
------SoftReference类
如果一个对象具有软引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这
个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合
使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列。
弱引用:
------WeakReference类
被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否
足够,弱引用所引用的对象都会被回收掉。弱引用也是和一个引用队列联合使用,如果
弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。
若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get
方法就会返回null。
虚引用:
------PhantomReference类
虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之
后收到一个通知,虚引用不能通过get方法获得其指向的对象。