达到的目标
知道Java虚拟机的生存周期
知道JVM的体系结构
知道JVM体系结构中的各个部分
能对JVM有个大致清晰的了解

内容
JVM的生命周期
JVM的体系结构
JVM类加载器
JVM执行引擎
JVM运行时数据区

  • JVM的生命周期

一、首先分析两个概念
JVM实例JVM执行引擎实例
(1)JVM实例对应了一个独立运行的java程序
它是进程级别
(2)JVM执行引擎实例则对应了属于用户运行程序的线程
它是线程级别的

二、JVM的生命周期
(1)JVM实例的诞生
当启动一个Java程序时,一个JVM实例就产生了,
任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点
(2)JVM实例的运行
main()作为该程序初始线程的起点,任何其他线程均由该线程启动。
JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,
守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。
(3)JVM实例的消亡
当程序中的所有非守护线程都终止时,JVM才退出;
若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。

  • JVM的体系结构

JVM的体系结构

JVM的内部体系结构分为三部分,
(1)类装载器(ClassLoader)子系统
作用: 用来装载.class文件
(2)执行引擎
作用:执行字节码,或者执行本地方法
(3)运行时数据区
方法区,堆,java栈,PC寄存器,本地方法栈

具体划分为如下5个内存空间:(非常重要)
程序计数器:保证线程切换后能恢复到原来的执行位置

虚拟机栈:(栈内存)为虚拟机执行java方法服务:方法被调用时创建栈帧–>局部变量表->局部变量、对象引用

本地方法栈:为虚拟机执使用到的Native方法服务

堆内存:存放所有new出来的东西

方法区:存储被虚拟机加载的类信息、常量、静态常量、静态方法等。

运行时常量池(方法区的一部分)

  • 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在运行时将数据划分为了6个区域来存储,而不仅仅是大家熟知的Heap区域,这6个区域图示如下:
运行时数据区

第一块: PC寄存器
PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。
第二块:JVM栈
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame、函数的参数值,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址
第三块:堆(Heap)
Heap是大家最为熟悉的区域,它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。

JVM将Heap分为New Generation和Old Generation(或Tenured Generation)两块来 进行管理:
Heap

(1)New Generation
又称为新生代,程序中新建的对象都将分配到新生代中,
新生代又由Eden Space和两块Survivor Space构成,可通过-Xmn参数来指定其大小
(2) Old Generation
又称为旧生代,用于存放程序中经过几次垃圾回收还存活的对象,
例如缓存的对象等,旧生代所占用的内存大小即为-Xmx指定的大小减去-Xmn指定的大小。

对堆的解释:
(1)堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
(2)鉴于上面的原因,Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间,这块空间又称为TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
(3)TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效,
但这种方法同时也带来了两个问题,一是空间的浪费,二是对象内存的回收上仍然没法做到像Stack那么高效,同时也会增加回收时的资源的消耗,可通过在启动参数上增加-XX:+PrintTLAB来查看TLAB这块的使用情况。

第四块:方法区域(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的体系结构之内存回收

一、 JVM中自动的对象内存回收机制称为:GC(Garbage Collection)
1、 GC的基本原理:
为将内存中不再被使用的对象进行回收,GC中用于回收内存中不被使用的对象的方法称为收集器,
由于GC需要消耗一些资源和时间的,Java在对对象的生命周期特征进行分析后,
在V 1.2以上的版本采用了分代的方式来进行对象的收集,即按照新生代、旧生代的方式来对对象进行收集,
以尽可能的缩短GC对应用造成的暂停
(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只需扫描卡表中标识为脏状态的卡中的对象即可,
图示如下:
卡片标记算法

1、跟踪收集器在扫描时最重要的是要根据这些对象是否被引用来标识其状态
2、JVM中将对象的引用分为了四种类型,不同的对象引用类型会造成GC采用不同的方法进行回收:
(1)强引用:默认情况下,对象采用的均为强引用 (这个对象的实例没有其他对象引用,GC时才会被回收)
(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用 (只有在内存不够用的情况下才会被GC)
(3)弱引用:在GC时一定会被GC回收
(4)虚引用:由于虚引用只是用来得知对象是否被GC