深入理解Java虚拟机:JVM高级特性与最佳实践
阅读笔记(内存原理、异常处理):
1.
Jvm运行时,内存划分如图所示:
2.
程序计数器:
Jvm将这个计数看作当前线程执行某条字节码的行数,会根据计数器的值来选取需要执行的操作语句。这个属于线程私有,不可共享,如果共享会导致计数混乱,无法准确的执行当前线程需要执行的语句。
该区域不会出现任何OutOfMemoryError的情况。
3.
虚拟机栈
经常说到的栈内存就是指虚拟机栈。Java中每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
4.
本地方法栈
虚拟机栈用来执行java方法,而本地方法栈用来执行本地方法。
抛出异常的情况和虚拟机栈一样。
5.
堆
是jvm中内存最大、线程共享的一块区域。唯一的目的是存储对象实例。这里也是垃圾收集器主要收集的区域。由于现代垃圾收集器都采用的是分代收集算法,所以java堆也分为新生代和老年代。
可以通过参数-Xmx(jvm最大可用内存)和-Xms(jvm初始内存)来调整堆内存,如果扩大至无法继续扩展时,会出现OutOfMemoryError的错误。
6.
方法区
Jvm中内存共享的一片区域,用来存储类信息、常量、静态变量、class文件。垃圾收集器也会对这部分区域进行回收,比如常量池的清理和类型的卸载,但是效果不理想。
方法区内存不够用的时候,也会抛出OutOfMemoryError错误。
7.
对象创建
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,加载检查之后会在堆中划分出一定的内存。
在完成new指令之后,紧接着会调用<init>方法将对象初始化,这时一个完整的对象才算创建了出来。
8.
对象的访问
在sun的jdk中采用的是指针访问方式,在reference中直接存储了对象的地址。
9.
OutOfMemoryError错误
Java堆内存的OutOfMemoryError异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
当产生的对象过多时,会出现这个错误信息。解决办法:调整虚拟机堆参数(-Xmx和-Xms)。
Java方法区会存储类信息、常量、静态变量等。如果产生了大量类,比如某个ssh项目因为加载了框架和大量jar包,这样class文件都会载入内存的方法区,这样如果出现内存无法继续扩展的情况,也会出现java.lang.OutOfMemoryError,然后紧跟着PermGen
space信息。通过-XX:PermSize和-XX:MaxPermSize可以限制方法区大小。
10. StackOverflow错误
Java中栈内存溢出,通常是由于栈深度超过限制深度,导致出现该问题。很常见的情况是,使用递归的时候,不小心忘了指定递归结束的时刻,导致递归深度超过限制深度,出现栈内存溢出。
11. 判断对象是否存活的算法
主流jvm虚拟机不会采用引用计数法,因为有种情况它无法判断是否存活。比如互成环路的几个对象,没有其他对象引用他们了,这样按道理来说是死对象,可以被回收,但是按照引用计数法的判定,他们计数还不为0,也就是还不能被回收。
主流商用jvm目前采用的是可达性算法。利用有向图的原理,从根节点(原点)GC ROOT可达的所有其他节点表示目前仍然被引用,无法被回收。而从它无法达到的对象节点(距离为∞)则表示该对象可以被回收。GC ROOT表示虚拟机栈中引用指向的对象或者方法区中的常量对象和静态对象,或者jni(java本地方法)中引用的对象。
12. 引用
引用代表的是数据A中存储了一个地址,这个地址指向另一快内存的起始地址,这个A就是一个引用。
强引用:无论何时都不会被回收。普遍存在,如Object o=new Object();
弱引用:当垃圾收集器工作时,就会回收弱引用指向的对象。
13. 回收流程
回收时系统会执行System.gc(),找到不可达对象,标记该对象为待回收对象,并查看该对象是否覆盖finalize()方法,如果覆盖了并且该方法之前没有被调用过,然后将他放到一个队列中,排着长队等着finalize()回收。即使进入finalize()方法,也不能保证该方法可以回收对象,因为对象可以在该方法里面关联其他引用,完成自救。综合而言,finalize()每次执行消耗成本高,但不能保证回收对象,所以效果并不好,并不推荐手动调用finalize()方法。
14. 垃圾收集算法
主流jvm都采用分代收集算法(将java堆分而治之):
新生代:大量对象死亡,少量存活,采用复制算法
java堆分成
老年代:少量死亡,大量存活,采用标记整理法或标记清除法
复制算法:将内存划分为两片区域A和B,每次都只占用A部分。这样A放了所有对象,B空着。每次收集的时候标记A中的存活部分,复制到B中,然后统一清除A中的内容即可。缺点:内存占用高!适用:新生代。
标记清除算法:标记所有垃圾对象,完成标记后统一清除这些垃圾对象。缺点:效率低,产生碎片多。适用:老年代。
标记整理算法:标记所有垃圾对象,将存活对象移动到一边,清掉另一边的垃圾对象。适用:老年代。