文章目录
Java虚拟机知识点总结
Java的平台无关性即一次编译处处执行如何实现
- Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
为什么JVM不直接将源码解析成机器码去执行
- 需要准备工作:每次执行都需要各种检查
- 兼容性:也可以将别的语言解析成字节码
JVM如何加载.class文件/类加载机制
类加载的五个过程
- 加载:类加载器获二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象。
- 验证:验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求。
- 准备:为类变量分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配。
- 解析:将常量池内的符号引用替换为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析。
- 初始化:执行类中定义的Java程序代码(字节码)。
类的生命周期(7个):加载、验证、准备、解析、初始化、使用、卸载
Java虚拟机中哪几种情况会对类进行初始化
Java虚拟机规范中严格规定了有且只有4中情况必须对类进行初始化:
- 1.使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。
- 2.通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。
- 3.当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化,因此,通过其子类来引用父类中定义的静态变量,只会触发父类的初始化而不会触发子类的初始化。
- 4.当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。
例子:
class ConstClass {
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORLD = "hello world";
}
public class Test {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);// 调用类常量
}
}
输出结果:
hello world
可以知道这里并没有触发ConstClass类的初始化。因为调用的是常量(对应第一条)。
Java类加载器及如何加载类
类加载器是实现通过一个类的全限定名来获取描述此类的二进制文件流的代码模块。类的加载是通过双亲委派模型来完成的,双亲委派模型即为下图所示的类加载器之间的层次关系。
双亲委派模型的工作过程是:如果一个类加载器接收到类加载的请求,它会先把这个请求委派给父加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。可以得知,所有的加载请求最终都会传送到启动类加载器中。
使用双亲委派模型组织类加载器之间的关系有一个显而易见的好处:Java类随着它的类加载器一起具备了某种带优先级的层次关系。如果没有使用双亲委派模型,而是由各类加载器自行加载,假如用户自己编写一个Object类放在ClassPath中,那么系统将会出现多个不同的Object类,Java体系中最基础的行为也就无法保证,应用程序将变得一片混乱。
- 谈谈ClassLoader
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
- ClassLoader的种类
- BootStrapClassLoader:C++编写,加载核心库java.*
- ExtClassLoader:Java编写,加载扩展库javax.*
- AppClassLoader:Java编写,加载程序所在目录
- 自定义ClassLoader:Java编写,定制化加载
自底向上检查类是否已经加载,自顶向下尝试加载类。
双亲委派模型工作工程:
- 当Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
- 当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
- 如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
- 如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
- 如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
- 如果均加载失败,就会抛出ClassNotFoundException异常。
双亲委派模型的实现过程:
实现双亲委派模型的代码都集中在java.lang.ClassLoader
的loadClass()
方法中:
首先会检查请求加载的类是否已经被加载过;
若没有被加载过:
递归调用父类加载器的loadClass();
父类加载器为空后就使用启动类加载器加载;
如果父类加载器和启动类加载器均无法加载请求,则调用自身的加载功能。
双亲委派模型的优点:
Java类伴随其类加载器具备了带有优先级的层次关系,确保了在各种加载环境的加载顺序。
保证了运行的安全性,防止不可信类扮演可信任的类。
Tomcat类加载器
Tomcat类加载器也是破坏了双亲委派机制的
Tomcat作为一个web容器需要解决下面几个问题:
- 1.部署在同一个Web容器上的两个web应用程序所使用的Java类库可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。
- 2.部署同一个Web容器上的两个web应用程序所使用的Java类库可以互相共享。这个需求也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录上,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到web容器的内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。
- 3.Web容器需要尽可能的保证自身的安全不受部署的Web应用程序影响。目前,有很多主流的Java Web容器自身也是使用Java语言来实现的。因此,Web容器本身也有类库依赖的问题,一般来说,基于安全考虑,容器所使用的类库应该与应用程序所使用的类库互相独立。
Tomcat类加载架构如下图:
Web应用类加载器默认的加载顺序是:
- 先从缓存中加载;
- 如果没有,则从JVM的BootStrap类加载器加载;
- 如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib 的顺序);
- 如果没有,则从父类加载器加载,以加载顺序是(AppClassLoader、Common、Shard)。
类的加载方式
- 隐式加载:new
- 显式加载:loadClass,forName等
loadClass和forName的区别
- Class.forName得到的class是已经初始化完成的
- Classloader.loadClass得到的class是还没有链接的(只完成了加载阶段,没有到链接阶段)
内存分配
JVM内存分配
-
Java堆:Java虚拟机所管理的内存中最大的一块,唯一的目的是存放对象实例。由于是垃圾收集器管理的主要区域,因此有时候也被称作GC堆。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
Java堆可以处于物理上不连续的内存空间中,在实现时,既可以实现成固定大小额,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。
-
方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。别名 Non-Heap (非堆)。
很多人更愿意把方法区称为“永久代”,本质上两者并不定价,仅仅是因为HotSpot将GC分代收集扩展至方法区,或者说使用永久代来实现方法区,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。可是这样很容易遇到内存溢出问题(永久代有 -XX:MaxPermSize 的上限),所以在1.7中开始了去永久代的工作,在JDK1.7及以后,JVM已经将运行时常量池从方法区中移了出来,在JVM堆开辟了一块区域存放常量池。HotSpot虚拟机在1.8之后已经取消了永久代,改为元空间,类的元信息被存储在元空间中。元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
字面量(literal)是用于表达源代码中一个固定值的表示法(notation). 几乎所有计算机编程语言都具有对基本值的字面量表示, 诸如: 整数, 浮点数以及字符串;
符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能够无歧义的定位到目标即可. 例如, 在Java中, 一个Java类将会编译成一个class文件. 在编译时, Java类并不知道所引用的类的实际地址, 因此只能使用符号引用来代替. 比如org.simple.People类引用了org.simple.Language类, 在编译时People类并不知道Language类的实际内存地址, 因此只能使用符号org.simple.Language来表示Language类的地址. -
程序计数器:当前线程所执行字节码的行号指示器。每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Underfined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
-
Java虚拟机栈:用于描述Java方法执行的模型。每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成,对应于一个栈帧在虚拟机栈中从入栈到出栈。
在Java虚拟机规范中,对虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
-
本地方法栈:与虚拟机栈作用相似,只不过虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,比如在Java中调用C/C++。
JVM内存分代机制,各代特点,分代回收优点
-
新生代(Young):HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为
8:1:1
。 -
老年代(Old):在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
-
永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。
新版(jdk1.8)已经删除永久代。
- 新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的Header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
常用的内存调试工具
- JConsole(可视化工具,可排查死锁、生成Heap Dump文件等)
- Jmap
- jstack
垃圾回收
垃圾回收需要完成的三件事:①哪些内存需要回收;②什么时候回收;③如何回收。
哪些内存需要回收
- 堆
- 方法区
什么时候回收
- 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1,每当一个引用失效时,计数器减1,任何时刻计数器为0则代表对象不被引用。
注意:引用计数算法有一个比较大的问题,那就是它不能处理环形数据。即如果有两个对象相互引用,那么这两个对象计数器始终不为0,也就不能被回收。
- 可达性分析算法:设立若干根对象(GC Roots),每个对象都是一个子节点。从根向下搜索所走过的路径叫引用链,当一个对象到根无任何引用链相连,证明此对象不可用。
Java语言中可以作为GC Roots的对象包括以下几种:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
如何回收
- 标记-清除算法:先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
- 复制算法:将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将存活的对象复制到另一块,最后将此块内存一次性清理掉。
(新生代使用)
- 标记-整理算法:先标记所有需要回收的对象,然后让所有存活的对象向一端移动,最后直接清理掉边界以外的另一端内存。
(老年代使用)
- 分代收集算法:把Java堆分为新生代和老年代。新生代中只有少量对象会存活,就选用复制算法;老年代中对象存活率较高,选用标记-XX算法。
垃圾收集器有哪些
- Serial收集器:单线程收集器。收集垃圾时必须暂停其他所有工作线程,直到它收集结束。
- Parnew收集器:Serial收集器多线程版本。
- Parallel Scavenge收集器:使用复制算法的新生代收集器。
- Serial Old收集器:使用标记-整理算法的老年代单线程收集器。
- Parallel Old收集器:使用标记-整理算法的老年代多线程收集器。
- CMS收集器:基于标记-清除算法的低停顿并发收集器。运作步骤为①初始标记②并发标记③重新标记④并发清除。
- G1收集器:最前沿的面向服务端应用的垃圾收集器。运作步骤为①初始标记②并发标记③最终标记④筛选回收。G1收集器有以下特点
- 并行与并发:无需停顿Java线程来执行GC动作。
- 分代收集:可独立管理整个GC堆。
- 空间整合:运行期间不会产生内存空间碎片。
- 可预测的停顿:除了低停顿,还能建立可预测的停顿时间模型。
Minor GC与Full GC
- 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
- 老年代 GC(Major GC / Full GC):指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次 Minor GC(非绝对),MajorGC 的速度一般会比 Minor GC 慢10倍以上。
Minor GC与Full GC触发条件:
- Minor GC触发条件:
- 当Eden区没有足够的空间进行分配时
- 老年代最大可用连续空间大于Minor GC历次晋升到老年代对象的平均大小
?
- Full GC触发条件:
- 调用System.gc()时(系统建议执行Full GC,但是不必然执行)
- 老年代空间不足时
- 方法区空间不足时
- 老年代最大可用连续空间小于Minor GC历次晋升到老年代对象的平均大小
-CMS GC在垃圾回收的时候,当对象从Eden区进入Survivor区,Survivor区空间不足需要放入老年代,而老年代空间也不足时
GC自我拯救(可达性分析算法发现一个对象不可达时会立刻回收吗)
当通过系统可达性分析发现,GCRoot节点到该对象不可达的时候,是否对象就会被回收呢,答案是不一定的,这时候它暂时处于缓刑阶段,至少要经过两次的标记的过程,才真正宣告一个对象的死亡,第一次是当系统检测到该对象到GCRoot节点不可达的时候,进行第一次的标记,然后系统就会检查该对象有没有覆盖finalize方法,如果有的话便会执行finalize方法,如果该对象在finalize方法中与任何一个对象进行关联的话便可以不会被回收。
使用CMS收集器时怎样会出现Concurrent Mode Failue
- 1.如果对象提升到老年代的速度太快,而CMS收集器不能保持足够多的可用空间时,就会导致老年代的运行空间不足。
- 2.当老年代的碎片化达到某种程度,使得没有足够空间容纳从新生代提升上来的对象时,也会发生并发模式失败。
CMS收集器预留空间大小(即老年代使用达到多少时会开启Full GC)
也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运行使用。
在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当提高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。
在JDK1.6中,CMS收集器的启动阀值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
所以说参数-XX:CMSInitiatingOccupancyFraction设置的太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
CMS如何处理空间碎片
CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
虚拟机设置者还提供了另一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。
常见OOM及原因
堆溢出
错误
java.lang.OutOfMemoryError:Java heap space
这是最常见的OOM原因。
堆中主要存放各种对象实例,还有常量池等结构。当JVM发现堆中没有足够的空间分配给新对象时,抛出该异常。具体来讲,在刚发现空间不足时,会先进行一次Full GC,如果GC后还是空间不足,再抛出异常。
原因主要有
- 无法在Java堆中分配对象
- 应用程序保存了无法被GC回收的对象
- 应用程序过度使用finalizer
解决
- 使用内存映像分析工具(如 Eclipse Memory Analyzer 或者 Jprofiler)对Dump出来的堆存储快照进行分析,分析清楚是内存泄漏还是内存溢出。
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,修复应用程序中的内存泄漏。
- 如果不存在泄漏,先检查代码是否存在死循环,递归等,再考虑使用 -Xmx 增大堆大小。
Demo
import java.util.ArrayList;
import java.util.List;
/**
* JVM配置参数
* -Xms20m JVM初始分配的内存20m
* -Xmx20m JVM最大可用内存为20m
* -XX:+HeapDumpOnOutOfMemoryError 当JVM发生OOM时,自动生成DUMP文件
* -XX:HeapDumpPath=/Users/binzhang/Desktop/dump/ 生成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());
}
}
}
栈溢出
错误
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
- 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
栈溢出原因
- 在单个线程下,栈帧太大,或者虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出StackOverflowError异常。
- 不断地建立线程的方式会导致内存溢出。
栈溢出排查解决思路
- 查找关键报错信息,确定是StackOverflowError还是OutOfMemoryError。
- 如果是StackOverflowError,检查代码是否递归调用方法等。
- 如果是OutOfMemoryError(Unable to create new native thread),检查是否有死循环创建线程等,通过-Xss降低的每个线程栈大小的容量。
demo
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
/**
* -Xss2M
*/
public class JavaVMStackOOM {
private void dontStop(){
while(true){
}
}
public void stackLeakByThread(){
while(true){
Thread thread = new Thread(new Runnable(){
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
错误信息
方法区溢出
方法区,(又叫永久代,JDK8后,元空间替换了永久代),用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。运行时产生大量的类,会填满方法区,造成溢出。
方法区溢出原因
- 使用CGLib生成了大量的代理类,导致方法区被撑爆
- 在Java7之前,频繁的错误使用String.intern方法
- 大量jsp和动态产生jsp
- 应用长时间运行,没有重启
方法区溢出排查解决思路
- 检查是否永久代空间设置的过小
- 检查代码是否频繁错误的使用String.intern方法
- 检查是否跟jsp有关
- 检查是否使用CGLib生成了大量的代理类
- 重启大法,重启JVM
demo
Caused by: java.lang.OutOfMemoryError: Metaspace
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* jdk8以上的话,
* 虚拟机参数:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) throws Throwable{
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
java.lang.OutOfMemoryError:Permgen space
jdk7中,方法区被实现在永久代中,错误原因同上。