首先来看异常的根节点
Throwable是所有异常的根,java.lang.Throwable
Error是错误,java.lang.Error
Error类体系描述了Java运行系统中的内部错误以及资源耗尽的情形.这种异常会导致JVM中断,必须人为处理
java虚拟机中发生的,不需要程序猿try-catch或者抛出
StackOutFlowError(栈溢出)和OutOfMemoryError(堆溢出),都属于Error,中间层是VirtualMachineError
Exception是异常,java.lang.Exception 继承 Throwable
IOException(必须捕获异常)
FileNotFoundException
EOFException
RuntimeException(可捕获异常)
NullPointerException
ClassNotFoundException
...
自定异常
总结
OutOfMemoryError
这类错误基本是内存不够用导致的。具体举例如下 (1)一次性从数据库获取了太多的数据,此时可能导致内存不够用。 (2)循环中产生了大量的对象,对象实例是需要在堆中创建的,于是堆空间不够用了。 (3)对象的引用没有被释放,导致JVM没有回收。 (4)启动参数内存值设定的过小。 (5)多线程中创建的线程过多。【特别是线程调用栈设置比较大时,容易出现这种情况】
StackOutFlowError
这类异常主要原因是线程请求的栈深度大于虚拟机所允许的最大深度。
内存异常的产生部分源于设计上的不完善,比如没有针对数据量做评估,一次性获取大量的数据放入到内存中;编写代码中不好的习惯,在循环中大量的实例化对象;递归中死循环等等。
1、java内存区域
1.1运行时数据区域:java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁时间,有的区域随着虚拟机进程的启动而存在,有些区域依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范SE 7版》的规定,Java虚拟机所管理的内容将会包括以下几个运行时数据区域,如果所示:
Java虚拟机运行时数据区
1.2、程序计数器:程序计数器是一块较小的空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。Java虚拟机是多线程轮流切换执行的,所以程序计数器是每个线程所独有的,各个线程之间的计数器互不影响,独立存储,我们称这内存区域为“线程私有”的内存。改内存区域是java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。
1.3、Java虚拟机栈:java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。在java虚拟机规范中,对这个区域规定了两种异常状况,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError;如果虚拟机可以动态扩展,如果扩展是无法申请到足够的内存,就会抛出OutOfMemoryError异常。
1.4、本地方法栈:本地方栈的作用与虚拟机栈发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。Native方法常用于两种情况:一是在方法中调用一些不是java语言写的代码,二是在方法中用java语言直接操纵计算机的硬件。
1.5、java堆:java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存;Java堆是垃圾收集器管理的主要区域,也叫GC堆;java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
1.6、方法区:方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
1.7、运行时常量池:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,java语言并不要求常量一定只有编译器才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
1.8、直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常。在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在java堆和Navtive堆中来回复制数据。
2.HotSpot虚拟机对象探秘
(基于HotSpot和常用的内存区域java堆为例,探讨HostSpot虚拟机在java堆中对象分配,布局和访问的过程)
2.1对象的创建:当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来,其中有“指针碰撞”和“空闲列表”两种方式。出开如何划分可用空间之外,还有另外一个需要考虑的问题就是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现在正给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理--实际上虚拟机采用CAS配上失败重试的方法保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程需要分配内存,就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
内存分配完毕后,虚拟机需要将分配到内存的空间都初始化为零值(不包括对象头),以保证对象实例字段在java代码中可以不赋初值就直接使用。
接下来就是对对象进行必要的设置,如改对象是哪个类的实例、如何找到类的元数据、对象的哈希码、独享的GC分代年龄等信息。
从虚拟机角度来看,一个新的对象已经产生,但从程序的角度来讲,对象的创建才开始----即执行
2.2对象的内存布局:在HotSpot虚拟机中,独享在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充。
对象头:HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称他为“Mark Word”。另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。
实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各个类型的字段内容。这部分内容的存储顺序会受到虚拟机分配策略参数和字段在java源代码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles 、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Points),从分配策略可以看出,相同宽度的字段总是被分配到一起。
填充对齐:这一部分并非必须存在,它仅仅是起占位符的作用,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全8位字节。
2.3对象的访问定位:在java程序中需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在java虚拟机中值规范了一个指向对象的引用,并没有去定义这个引用应该以何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
句柄访问方式:在java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含对象实例数据与类型数据各自的具体地址信息,如图:
通过句柄访问对象
直接指针访问:在java堆中的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的就是对象的地址,如图:
通过指针直接访问对象
这两种对象在访问时各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象移动(GC中)时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针对位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是一种非常可观的执行成本。Sun HotSpot中采用第二种。
3.OtuOfMemoryError异常
3.1java堆溢出:java堆用于存储对象实例,只要不停地创建对象,病情保证GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大的容量限制后就会产生内存溢出异常。eg:
public class HeapOOM{
static class OOMObject{
}
public static void main(String[] args){
List
while(true){
list.add(new OOMObject());
}
}
}
java堆内存的OOM异常是实际应用中常见的内存溢出异常情况。当出现java堆内存溢出情况,异常堆栈信息“java.lang.OutOfMemaryError”会跟着进一步提示“Java heap space”。要解决这个区域异常,一般手电是先通过内存映像分析工具对Dump出来的堆转储快照进行分析,区分是内存泄露还是内存溢出。
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。
如果是不存在泄露,换句话说内存中的对象确实还活着,那就应当检查虚拟机的堆参数与机器物理内存对比是否还可以调大,从代码上检查是否存在某些对象的生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
3.2虚拟机栈和本地方法栈溢出
关于虚拟机和本地方法栈,在java虚拟机规范中描述了两种异常:
1)如果把线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOutflowError异常。
2)如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里把异常分成两种,看似更加严谨,但却存在着一些相互重叠的地方,当栈空间无法继续分配时,到底是内存太小还是已使用的栈空间太大,其本质上只是对同一件事的两种描述。对于HotSpot虚拟机来说,并不区别本地方法栈和虚拟机栈。
3.3方法区和运行时常量池溢出
3.4本机直接内存溢出
什么是java栈深度?
java栈深度有什么用?
我们知道java栈是java虚拟机的一个重要的组成部分,在栈里进行线程操作,存放方法参数等等。
栈在初始化过后是有一定的大小的。
public class Test{
private int count = 0;
public void testAdd(){
count ++;
testAdd();
}
public void test(){
try{
testAdd();
}catch(Throwable e){
System.out.println(e);
System.out.println("栈深度:"+count);
}
}
public static void main(String [] args){
new Test().test();
}
}
运行程序,可以看到栈深度:
栈深度:11114
栈的高度称为栈的深度,栈深度受栈帧大小影响。
我们知道,在栈中存放局部变量,参数,运行中间结果等。
通过-Xss可以设置栈的大小:
。
----------改变传入参数--增加传入参数,观察栈深度变化。
public void testAdd(int a,int b,int c){
count ++;
testAdd(a,b,c);
}
运行程序:
结果-栈深度:6632
----------增加局部变量:
public void testAdd(int a,int b,int c){
int d=0;
long h=9l;
count ++;
testAdd(a,b,c);
}
结果-栈深度-5928.
由此可以看出,局部变量表内容越多,栈帧越大,栈深度越小。
知道了栈深度,该怎么用呢?对JVM调优有什么用呢?
当JVM我们定义的方法参数和局部变量过多,字节过大,考虑到可能会导致栈深度多小,可能使程序出现错误。
这个时候就需要手动的增加栈的深度,避免出错。
而且当看到StackOverFlow的时候我们也可以知道可能是栈溢出造成的错误。知道如果去解决。这才是最重要的。
对于JVM来说,每个Java文件都会被编译成.class文件,里面的方法,常量等等,都会被包括在内,但是运行时,如何知道目前线程运行到哪一行命令,就需要程序计数器指出来。在JVM中,多线程是通过线程轮流切换CPU的使用时间来实现,每个线程都会有一小块区域,用来记录当前所执行到的命令的位置,如果不这样的话,那么线程暂停以后,就没法知道目前命令执行到哪一条了。很好理解,这块区域属于线程的私有区域,不允许其他线程访问。
所需要注意的是,程序计数器只记录Java方法,也就是说,它只记录虚拟机自己的字节指令,对于Native方法,这个计数器的值为空。这块区域是唯一没有规定任何OutOfMemory异常的地方。
2. Java虚拟机栈(线程私有)
这个栈是真正用来执行Java方法的区域,每当执行到一个Java方法的时候,就会创建一个栈帧,这个栈帧里面会记录局部变量、操作数栈、动态链接、方法出口和其他附加信息等等,每个方法从开始执行到执行完就是一个出栈到入栈的过程。
在这个区域,如果线程规定了栈的深度,那么在执行方法时超出了栈的最大深度,那么就会出现StackOutflowError异常,如果虚拟机栈可以扩展,那么在扩展的时候分配不出内存,就会抛出OutOfMemoryError异常。当前大部分的虚拟机都是可以扩展的。
3. Java堆(线程共享)
对于大多数应用来说,这一块是Java内存分配管理中最大的一块,它是所有线程共享的,主要用于存储对象实例和数组。这一区域会涉及到Java的内存回收机制Garbage Colleted Heap,这个区域在不同的内存垃圾回收机制下面,会呈现不同的内存空间(不同是指是否规则,回收后是否会进行内存空间整理),当内存空间无法分配的时候抛出OutOfMemoryError
4. 本地方法栈(线程私有)
与Java虚拟机栈类似,区别在于本地方法栈用于执行Native方法,Java虚拟机栈却是执行Java字节码的地方。在虚拟机规范中没有规定对此块内存区域应该如何使用,各种虚拟机可以自由实现它。跟Java虚拟机栈一样,会抛出两种异常。
5. 方法区(线程共享)
各个线程共享,用于储存被加载的类信息、常量、静态变量、即时编译器编译的代码等数据。GC对这块内存的收集是必要而且条件苛刻的,回收主要是针对常量池和对类型的卸载。会出现OutOfMemoryError异常。
6. 运行时常量池(线程共享)
这块区域是方法去的一部分,.class文件中会有这些信息,用于存放编译时生成的各种字面量和对象的引用,在类加载完成后,放入方法区运行时常量池存放。另外一个特征就是具备动态性,所有常量并不要求在编译期产生,在运行时产生的常量也可以放入该区域,用的最多的就是String.intern()方法http://bbs.csdn.net/topics/190153906
7. 直接内存
在Java1.4中加入了NIO(New Input/Output),引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数直接在堆外面分配内存,然后用一个储存在堆里面的DirectByteBuffer对象作为这块内存的引用,能提高IO性能,可能会出现OutOfMemoryError异常。
首先,"java的类在第一次需要创建类的实例(对象)时被加载"这个说的不对
java中类被使用就就会时就会被加载到内存(比如反射等)
然后回答你的问题。
首先要介绍下相关知识(基础知识纯属拷贝):
首先来了解一下jvm(java虚拟机)中的几个比较重要的内存区域
方法区:在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
常量池:常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
堆区:用于存放类的对象实例。
栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。
除了以上四个内存区域之外,jvm中的运行时内存区域还包括本地方法栈和程序计数器,这两个区域与java类的生命周期关系不是很大,在这里就不说了,感兴趣可以自己百度一下。
其实类在JVM里面有以下几个阶段:
加载 -- 连接 -- 初始化 -- 使用 -- 卸载
主要给你说明卸载:
在类使用完之后可能会被卸载,可能性如下:
如果有下面的情况,类就会被卸载:
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有任何地方被引用,无e79fa5e98193e58685e5aeb931333332643262法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。