JVM的生命周期##
首先分析一下JVM实例和JVM执行引擎实例的区别
JVM实例:JVM实例对应了一个独立运行的java程序
它是进程级别的
JVM执行引擎实例则对应了属于用户运行程序的线程
它是线程级别的
JVM实例的诞生##
当启动一个Java程序时,一个JVM实例就产生了
JVM实例的运行##
main()作为该程序初始线程的起点,任何其他线程均由该线程启动。
JVM内部有两种线程:守护线程和非守护线程
main()属于非守护线程, 守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程
JVM实例的消亡
当程序中的所有非守护线程都终止时,JVM才退出;
若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出
JVM的体系结构##
JVM的内部体系结构分为三部分:##
(1)类加载器(ClassLoader)子系统
作用:用来装载.class文件
(2)执行引擎
作用:执行字节码,或者执行本地方法
(3)运行时数据区
方法区,堆,PC寄存器,本地方法栈
JVM的类加载器##
JVM将整个类加载过程划分为三个步骤:
(1)装载
装载过程负责找到二进制字节码并加载至JVM中
JVM通过类名、类所在的包名通过ClassLoader来完成类的加载
同样,也采用以上三个元素来标识一个被加载了的类:类名+包名+ClassLoader实例ID。
(2)链接
链接过程负责对二进制字节码的格式进行校验、 初始化装载类中的静态变量以及解析类中调用的接口、类。
在完成了校验后,JVM初始化类中的静态变量,并将其值赋为默认值。
最后一步为对类中的所有属性、方法进行验证, 以确保其需要调用的属性、方法存在,以及具备应的权限(例如public、private域权限等), 会造成NoSuchMethodError、NoSuchFieldError等错误信息。
(3)初始化
初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化
在四种情况下初始化过程会被触发执行:
1.调用了new;
2.反射调用了类中的方法;
3.子类调用了初始化;
4.JVM启动过程中指定的初始化类。
JVM执行引擎##
JVM通过执行引擎来完成字节码的执行,在执行过程中JVM采用的是自己的一套指令系统,每个线程在创建后,都会产生一个程序计数器(pc)和栈(Stack),其中程序计数器中存放了下一条将要执行的指令,Stack中存放Stack Frame,表示的为当前正在执行的方法,每个方法的执行都会产生Stack Frame,Stack Frame中存放了传递给方法的参数、方法内的局部变量以及操作数栈,操作数栈用于存放指令运算的中间结果,指令负责从操作数栈中弹出参与运算的操作数,指令执行完毕后再将计算结果压回到操作数栈,当方法执行完毕后则从Stack中弹出,继续其他方法的执行。
JVM运行时数据区##
JVM在运行时将数据划分为了6个区域来存储,而不仅仅是大家熟知的Heap区域,这6个区域图示如下:
第一块: PC寄存器
PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息
第二块:JVM栈
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量,部分返回结果以及函数的参数值,非基本类型的对象的JVM栈上仅存放一个指向堆上的地址
第三块:堆(Heap)
Heap是大家最为熟悉的区域,它是JVM用来存储对象实例以及数组值的区域,可以认为java中所有通过New创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收
第四块:方法区域(Method Area)
(1)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,可见方法区域的重要性,同样,方法区域也是全局共享的,在一定的条件下它也会被GC;当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
(2)在Sun JDK中这块区域对应的为Permanet Generation,又称为持久代,默认为64M,可通过-XX:PermSize以及-XX:MaxPermSize来指定其大小。
第五块:运行时常量池(Runtime Constant Pool)
类似C中的符号表,存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。
第六块:本地方法堆栈(Native Method Stacks)
JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态
JVM的垃圾回收问题##
GC的基本原理:
为将内存中不再被使用的对象进行回收,GC中用于回收内存中不被使用的对象的方法称为收集器
由于GC需要消耗一些资源和时间的,Java在对对象的生命周期特征进行分析后,采用了分代的方式来进行对象的收集,即按照新生代,旧生代的方式来对对象进行收集
(1)对新生代的对象的收集称为Minor GC
(2)对旧生代的对象的收集称为Full GC
(3)程序中主动调用System.gc()强制执行的GC为Full GC
JVM中自动内存回收机制
(1)引用计数收集器
原理:引用计数是标识Heap中对象状态最明显的一种方法
引用计数的方法简单来说就是对每一个对象都提供一个关联的引用计数,以此来标识该对象是否被使用,当这个计数为零时,说明这个对象已经不再被使用了
优点:
引用计数的好处是可以不用暂停应用,当计数变零时,即可将此对象的内存空间回收,但它需要给每个对象附加一个关联引用计数
缺点:
引用计数无法解决循环引用的问题,因此JVM并没有采用引用计数
(2)跟踪收集器
原理:
跟踪收集器的方法为停止应用的工作,然后开始跟踪对象,跟踪时从对象根开始沿着引用跟踪,直到检查完所有的对象
根对象的来源主要有三种:
1.被加载的类的常量池中的对象引用
2.传到本地方法中,没有被本地方法"释放"的对象引用
3.虚拟机运行时数据区从垃圾收集器的堆中分配的部分
存在问题:
跟踪收集器采用的均为扫描的方法,
但JVM将Heap分为了新生代和旧生代, 在进行minor GC时需要扫描是否有旧生代引用了新生代中的对象, 但又不可能每次minor GC都扫描整个旧生代中的对象,
因此JVM采用了一种称为卡片标记(Card Marking)的算法来避免这种现象。
(3)卡片标记算法
卡片标记的算法为将旧生代以某个大小(例如512字节)进行划分,划分出来的每个区域称为卡片, JVM采用卡表维护卡的状态,每张卡片在卡表中占用一个字节的标识(有些JVM实现可能会不同), 当Java代码执行过程中发现旧生代的对象引用或释放了对于新生代对象的引用时, 就相应的修改卡表中卡的状态,每次Minor GC只需扫描卡表中标识为脏状态的卡中的对象即可
JVM中将对象的引用分为四种类型,不同的对象引用类型会造成GC采用不同的方法进行回收:
(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收)
(2)软引用:软引用是java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)
(3)弱引用:在GC时一定会被GC回收
(4)虚引用:由于虚引用只是用来得知对象是否被GC