类加载的五个过程
类的生命周期(7个):加载、验证、准备、解析、初始化、使用、卸载
Java虚拟机规范中严格规定了有且只有4中情况必须对类进行初始化:
例子:
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类随着它的类加载器一起具备了某种带优先级的层次关系。如果没有使用双亲委派模型,而是由各类加载器自行加载,假如用户自己编写一个Object类放在ClassPath中,那么系统将会出现多个不同的Object类,Java体系中最基础的行为也就无法保证,应用程序将变得一片混乱。
ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。
- BootStrapClassLoader:C++编写,加载核心库java.*
- ExtClassLoader:Java编写,加载扩展库javax.*
- AppClassLoader:Java编写,加载程序所在目录
- 自定义ClassLoader:Java编写,定制化加载
自底向上检查类是否已经加载,自顶向下尝试加载类。
实现双亲委派模型的代码都集中在java.lang.ClassLoader
的loadClass()
方法中:
首先会检查请求加载的类是否已经被加载过;
若没有被加载过:
递归调用父类加载器的loadClass();
父类加载器为空后就使用启动类加载器加载;
如果父类加载器和启动类加载器均无法加载请求,则调用自身的加载功能。
Java类伴随其类加载器具备了带有优先级的层次关系,确保了在各种加载环境的加载顺序。
保证了运行的安全性,防止不可信类扮演可信任的类。
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应用类加载器默认的加载顺序是:
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++。
新生代(Young):HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1
。
老年代(Old):在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。
新版(jdk1.8)已经删除永久代。
垃圾回收需要完成的三件事:①哪些内存需要回收;②什么时候回收;③如何回收。
注意:引用计数算法有一个比较大的问题,那就是它不能处理环形数据。即如果有两个对象相互引用,那么这两个对象计数器始终不为0,也就不能被回收。
Java语言中可以作为GC Roots的对象包括以下几种:
(新生代使用)
(老年代使用)
- 并行与并发:无需停顿Java线程来执行GC动作。
- 分代收集:可独立管理整个GC堆。
- 空间整合:运行期间不会产生内存空间碎片。
- 可预测的停顿:除了低停顿,还能建立可预测的停顿时间模型。
Minor GC与Full GC触发条件:
?
当通过系统可达性分析发现,GCRoot节点到该对象不可达的时候,是否对象就会被回收呢,答案是不一定的,这时候它暂时处于缓刑阶段,至少要经过两次的标记的过程,才真正宣告一个对象的死亡,第一次是当系统检测到该对象到GCRoot节点不可达的时候,进行第一次的标记,然后系统就会检查该对象有没有覆盖finalize方法,如果有的话便会执行finalize方法,如果该对象在finalize方法中与任何一个对象进行关联的话便可以不会被回收。
- 1.如果对象提升到老年代的速度太快,而CMS收集器不能保持足够多的可用空间时,就会导致老年代的运行空间不足。
- 2.当老年代的碎片化达到某种程度,使得没有足够空间容纳从新生代提升上来的对象时,也会发生并发模式失败。
也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运行使用。
在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当提高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。
在JDK1.6中,CMS收集器的启动阀值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
所以说参数-XX:CMSInitiatingOccupancyFraction设置的太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。
虚拟机设置者还提供了另一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。
错误
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中,方法区被实现在永久代中,错误原因同上。