学习java学到最后都要很熟悉jvm,懂得了java的运行机制,查找问题就会很快了,一般我们都是会用,还到不了精通的地步,虽说我也是个多年的java工程师,但也没有很好的掌握jvm,最近换了一家公司,之前的算是被雪藏三年,一直未于外界接触,现在我要重新拾起来,不能满足只是会用java,也要懂得其运行原理。
学习jvm我们要知道它有哪几部分构成,各部分作用是什么:
- 运行时数据区域
- 程序计数器
- java虚拟机栈
- 本地方法栈
- java堆
- 方法区
- 运行时常量池
- 对象的创建
程序计数器
指令执行的指向,是一块很小的内存空间,每个线程都有一个独立的程序计数器,互不影响,独立存储;如果执行native方法,则计数器为空;这个内存空间不存在OutOfMemoryError的情况。
Java虚拟机栈
它与程序计数器一样,是线程私有的,生命周期也同线程相同。它是java方法执行的内存模型,每个方法在执行时会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用到执行完成的过程就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
java虚拟机规范中对这个区域定了两种异常情况:1.线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
2.虚拟机动态扩展时,无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈与虚拟机栈所发挥的作用非常相似,区别就是虚拟机栈执行java方法,本地方法栈则为虚拟机使用native方法,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常
java堆
java堆是jvm所管理的内存中最大的一块,java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据组都在这里分配内存。但随着jit编译器的发展与逃逸分析技术成熟,在堆上分配也不那么绝对了。
java堆是垃圾回收的主要区域,也称为GC堆。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以java堆中细分为:新生代和老年代,再细分为:Eden空间,FromSurvivor空间,To Survivor空间等。无论哪个区域,存储的仍是对象实例,进一步划分的目的是为了更好的回收内存、更快的分配内存。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区
方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。虽然jvm规范把方法区描述为堆的一个逻辑部分,但它有一个别名Non-Heap,目的就是与堆分开。
java方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。
运行时常量池
运行时常量池是方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
常量池是java方法区的一部分,无法满足内存分配需求时,将会抛出OutOfMemoryError异常。
对象的创建:
虚拟机遇到一条new指令时,首先要检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有就执行相应的类加载过程。
可达性算法:
算法的基本思想就是通过GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象没有到gc roots没有任何引用链相连时,则证明对象是不可用的。
引用类型:
强引用:Object object = new Object();这类的引用,只要强引用还存在,gc永运不会回收掉被引用对象。
软引用:描述一些还有用但并非必需的对象。在系统未发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这些回收没有足够的内存,才会抛出内存溢出异常。
弱引用:也是用来描述非必需对象的,比前两者更弱,被弱引用的关联对象只能生存到下一次垃圾收集发生之前。当gc时,无论内存是否足够,都会回收被弱引用关联的对象。
虚引用:最弱的引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
对象何时回收:
对象回收至少要经历两次标记过程,如果对象在进行可达性分析后发现没有gc roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,条件就是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行"。
如果这个对象没有必要执行finalize()方法,那会被放在F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里的执行是触发方法,并不承诺会等待它运行结束,这样是因为在finalize()中万一执行缓慢或死循环了,那F-Queue就会永久处于等待,甚至内存回收系统崩溃。如果对象在finalize()中逃过,那它就不会被回收。
无用的类:
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收器算法:
标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收。两个不足:1.效率不高2.空间问题,这种会产生大量不连续的内存碎片,如果有大对象分配时,无法找到连续内存而不得不提前触发另一次的垃圾收集动作。
复制算法:为了解决效率问题,一种称为“复制”的收集算法出现了,它把内存按容量划分为大小相等的两块,每次使用其中一块,当这块用完了就把对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。现在的商业虚拟机都采用这种算法,但并不是按1:1的比例来划分内存空间的,它是将内存分为一个较大的Eden空间和两个较小的Survivor空间,每次只使用Eden和一个Survivor。默认是8:1:1。
标记-整理算法:算法同第一种,但后续不是直接对可回收的对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法:一般将java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。