什么是JVM:
一个程序肯定要跟硬件打交道,但硬件肯定没办法直接和程序对接,那这是如何实现的?程序员首先编写java文件(文件后缀是 .java),然后这个java文件被编译成二进制字节码文件(文件后缀是 .class)才能跟JVM打交道,然后JVM再去跟操作系统说我要让某个硬件去干某件事,最后操作系统再去调动硬件
所以这个过程中,JVM所要完成的工作就是执行二进制字节码文件的指令,根据这个指令去调用操作系统
JVM导致java的跨平台性:
知道上面这个之后,我们只需要再知道一个点,就能理解什么是java跨平台性,java为什么能跨平台了。操作系统有很多个(windows、linux、mac...)不同的操作系统对于同一个动作(比如将数据写入硬盘)的指令有可能是不同的,而一个java文件只能编译出一个二进制字节码文件,不过对于不同的操作系统,java提供了相对应的JVM版本,而一个二进制字节码文件就可以跟不同的JVM版本进行对接,保证了一个二进制字节码文件能够在不同的操作系统中都能兼容
JVM运行时数据区
JVM要执行二进制字节码文件,包含着一个很重要的步骤,就是为程序(这里的程序其实就是这个二进制字节码文件),开辟出一块内存空间供程序使用,那这块内存空间到底要存些什么?要怎么开辟呢?
这块空间的名字叫运行时数据区
它有如下几个模块
下面简单(追求的就是一个快速)说明每个模块的作用
方法区
主要用于存储类信息、静态变量、静态方法等
堆
存储几乎所有的对象实例
方法区和堆是所有线程共享的,接下来这三个模块都是线程各自占有的
虚拟机栈
每个方法被执行的时候,虚拟机栈都会创建一个栈帧,这个栈帧会存储这个方法的必要信息,当方法执行结束之后,这个栈帧就会出栈
程序计数器
这个自己要先去简单了解并发,程序计数器简单讲就是要让处理器知道上次执行到了该线程的哪个地方
本地方法栈
对象实例在JVM中的存储
对象实例是一个程序最重要的部分之一,没有哪个正儿八经的程序是没有对象实例的,上面说到,堆区中存储的就是对象实例,那具体是这么存储呢?
堆为对象分配空间的两种方式
假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。但如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称 为“空闲列表”。所以选择哪种分配方式由Java堆是否规整决定
对象在内存中要存储那些信息
对象在堆内存中的存储信息可以划分为三个部分:对象头、实例数据和对齐填充
1、对象头部分包括两类信息。
第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分方称它为“Mark Word”
对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
2、实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
3、对象的第三部分是对齐填充,它没有特别的含义,仅仅起着占位符的作用。这是因为虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
如何在堆中找到对象
创建对象自然是为了后续使用该对象,Java程序会通过栈上的reference数据来操作堆上的具 体对象。而reference数据访问对象的方式是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:


JVM中的堆区分代
堆区分为新生代和老年代,形象化的理解:新生代主要存储刚刚创建出来的对象实例,如果新生代中的对象能经过多次回收(具体看下面讲的JVM垃圾回收机制)的洗礼,就让为它会比较经常被使用,从而进入老年代,顺便提一嘴,还有一个叫永久代的,其实就是方法区
JVM的垃圾回收机制(简称GC)
JVM的垃圾回收机制非常强大,是JVM的一个很重要的功能,而且这也是跟对象实例息息相关的,上面我们讲到了对象实例是怎么储存的和使用的,那么如果对象实例不用了要怎么清除呢?
如何判断对象已经没用了
当JVM认为一个对像已经没用了,就会把这个对象判定为是垃圾,就会去回收它的空间,有两个方法判断一个对像是否已经没用了
1、引用计数法:记录指向该对象的引用数,当该数值为零时就将该对象判定为垃圾
这个方法实现简单,判定效率也高,不过它有个致命的问题,它无法解决相互对象之间相互循环引用的问题,看下面这个例子
public class Test {
public Object object = null;
public static void main(String[] args) {
Test a = new Test();//对象1
Test b = new Test();//对象2
a.object = b;
b.object = a;
a = null;
b = null;
}
}
此时对象1和对象2除了对方指向自己的引用外,没有其他的引用了,这个时候,无论是对象1还是对象2,我们认为都已经没用了,因为程序是找不到它俩的,但是引用计数法无法将它们判定为垃圾,因为它们的被引用数不是为零
正是因为这个缺点,主流的java虚拟机都不会使用该判定方法
2、可达性分析:
选定一些满足特定条件的对象作为根对象(GC Roots),那些与跟对象存在直接或间接引用关系的就是有用的对象,而与根对象没有任何关联的对象,就是垃圾对象(如下图)
这是当今主流的判定机制
GC的分类
Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。
Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。
Full GC是清理整个堆空间,包括年轻代和老年代(Minor GC和Major GC一起执行就是Full GC)
GC和分代的关系
那么现在我们就知道了为什么要分代了:
对象实例一般会首先分配到新生代当中,当新生代当中的空间不够用的时候,就会触发Minor GC,这个时候就会有一些没用的对象实例被清除掉,而有些就会留下来,那些能够挺过一定次数Minor GC的对象,最后就会进入到老年代当中,如果老年代中的空间也不够用了,那么就会进行Major GC
回收算法
我们上面说到GC会对垃圾进行回收,那具体要这么回收呢?这个就是回收算法,目前有三种回收算法,分别是:标记-清除、标记-复制、标记-整理
标记-清除
看下面的示意图,这个代表堆中的某块空间(可以是年轻代或老年代),每个紫色方块就是一个对象,上面我们说,JVM的对象是否存活的判定方法是可达性分析,所有那些没被GC Root引用的就要给标记成垃圾对象,标记完后再统一进行回收,这会造成内存空间碎片化的问题,另外还有执行效率不稳定的问题
标记-复制
将堆区分为两块区域,先只在其中一块区域创建对象,垃圾回收的时候,先标记出那些不要被回收的对象,然后将其复制到另外一块区域中,然后清空原本那块区域,新生代使用的就是标记-复制算法,新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。这里存在一个问题,当存活的对象的总大小大于那块Survivor空间,那就会造成溢出,而那些溢出的对象,会直接进入老年代,这叫分配担保
标记-整理
标记-复制算法存在需要额外空间进行分配担保的问题,新生代有老年代做分配担保,那老年代没人做分配担保就没办法使用标记-整理算法,要使用标记-整理算法,同样是先进行标记,不过不马上进行回收,而是让所有的存活对象都向内存空间一端移动,然后直接清理掉边界以外的堆空间
标记-整理存在一个弊端,在整理的过程中,必须全程暂停用户应用程序,这个被形象地称为“Stop The World”,实际上只要对象的存储地址发生了改变,就会“Stop The World”,所以标记-复制算法也会“Stop The World”
垃圾收集器
上面说的回收算法是理论层面的,接下来讲这些理论的实现者--垃圾收集器
Serial收集器和Serial Old收集器
Serial收集器是作用于新生代的,采用了标记-复制的算法,Serial Old是作用于老年代的,采用鳄
它们都是单线程收集器,且
未完待续...