目录
Java 虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在 Java 虚拟机启动时创建的,并且仅在 Java 虚拟机退出时销毁。其他数据区域按线程计算。每线程数据区域是在创建线程时创建的,并在线程退出时销毁。

程序计数器是一块较小的内存空间,用于记录指令的位置。
java程序编译生成class文件,class文件经由虚拟机运行。JVM工作时,就是根据程序计数器的值,来知道要执行哪一条指令。
因为java是支持多线程的,那么每一个线程都需要维护一个程序计数器,记录当前线程的执行指令的位置
java虚拟机栈和程序计数器一样,都是线程私有的,生命周期和线程相同
java虚拟机栈描述的是方法执行的内存模型:方法运行时,创建栈帧,存储方法运行所需要的各种数据,一个方法的运行到结束,就是创建栈帧到销毁栈帧的过程
如果线程请求的栈深度大于了虚拟机允许的深度,那么就会抛出StackOverFlowError异常,就是我们递归可能出现的栈溢出
栈存放局部变量和方法调用
本地方法栈和虚拟机栈的执行功能是非常相似的,不过就是虚拟机栈执行java方法,而本地方法栈则是为虚拟机使用的Native方法服务
堆是虚拟机中内存最大的一块,是所有线程共享的一块区域。在虚拟机创建时创建,用于存放对象实例,几乎所有的对象实例都要在堆上分配内存。
假设对象不断在创建,并且没有垃圾回收机制来回收,那么堆区就会溢出
堆存放成员变量和new出来的对象
方法区和堆一样,是所有线程共享的区域。用于存储已经被虚拟机加载的类信息,常量,静态变量等
存放类信息,而static修饰的变量也是属于类的,也存放在方法区
方法区的一部分,Class文件中除了类的版本,字段,方法,接口等信息之外,还有一个信息是常量池,用于存放编译期间生成的各种字面常量和符号引用,这部分内容将会在类加载之后进入运行时常量池
java是面向对象的,在语言层次上,我们使用new关键字创建对象,而在虚拟机层次上又是怎样的过程呢?
我们new一个对象,会先在常量池中查看这个参数是否已经被加载过,如果没有,就先要执行类加载过程。
在类加载结束之后,接下来虚拟机会给对象分配内存(对象所需要的空间大小在类加载时确定),也就是在堆中划分出区域来存放对象信息
内存分配成功后,要将分配的内存空间初始化为0(这也就是成员变量不赋值时,默认0的原因)
然后对对象进行必要的设置,例如这个对象是哪一个类的实例,如何找到类的数据信息等,而这些信息保留在对象头当中
对象在内存中存储的布局可以分为:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)
1、存储对象自身的运行数据,例如哈希码,锁标志状态等,这些数据的长度在32位和64位的虚拟机中分别是32bit和64bit,官方称为“Mark Wold”。
2、类型指针:虚拟机通过这个指针来确定当前对象是哪一个类的实例
java内存分配中,程序计数器,本地方法栈,java虚拟机栈都是属于线程的,随线程而灭。
但是java堆和方法区则是所有线程都可以使用的,这里面的对象是运行时生成的,我们要对这里的内容进行回收
垃圾回收(GC)可以实现自动的垃圾回收

我们要释放的,肯定是“死去”的对象,如何分辨对象“是死是活”呢
很多教科书判断对象是否存活的算法如下:
在对象中添加一个引用计数器,每当有一个地方引用到了这个对象,计数器+1,引用失效时,计数器-1,当计数器=0,表示这个对象没有被引用。

优点:一旦这个对象没有被任何对象引用时,就可以被回收了
缺点:要维护引用计数器,浪费开销;无法解决循环引用的问题
主流的java虚拟机没有采用这个方法,因为它无法解决对象之间循环引用的问题
- class Text{
- Text t=null;
- }
-
- //在堆上创建Text()对象,t1引用了这个对象,引用计数器+1 为1
- Text t1=new Text();
- //在堆上创建Text()对象,t2引用了这个对象,引用计数器+1 为1
- Text t2=new Text();
- //t1.t引用了t2实例,也就是说t2实例再次被引用 引用计数器+1 为2
- t1.t=t2;
- //t2.t引用了t1实例,也就是说t1实例再次被引用 引用计数器+1 为2
- t2.t=t1;
-
- //t1不在引用Text对象,引用计数器-1 为1
- t1=null;
- //t2不在引用Text对象,引用计数器-1 为1
- t2=null;
这时,t1和t2的引用计数器都不是0,不可回收,就导致了内存泄漏
(1) 内存泄露:内存泄露是指有的内存地址太过碎片化而无法被利用,我们都知道一个对象创建的时候开辟的内存空间是连续的,所以太过碎片化的内存空间就没办法利用。内存泄露多了也会导致内存溢出。
(2) 内存溢出:内存溢出是指内存已经装满了,无法再装下更多的对象了。
主流的语言都是通过可达性分析来判断对象是否存活的。通过一系列的称为“GC Roots”的对象做为起始点,从这些起点向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明这个对象是不可用的

在java中,可以作为GC Roots的对象包括以下几种:
1、虚拟机栈(栈帧中的本地变量表)引用的对象
2、方法区中类静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中Native方法引用的对象
java虚拟机规范中说过:可以不要求在方法区实现垃圾收集,因为在方法区实现垃圾回收的效率很低
方法区回收主要是两部分内容:废弃常量和无用的类
废弃常量和回收java堆中的对象非常相似,只要常量池中的常量,没有被引用,那么在回收时就可以被回收了
判定一个类是无用的类,条件比较苛刻:
1、java堆中不存在这个类的实例
2、加载这个类的ClassLoader已经被回收
3、这个类的Class对象没有被访问,无法通过反射来引用这个类
类卸载的效率很低,虚拟机可以回收,但是不代表会进行回收
首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象
标记的过程就是可达性算法
stop-the-Word:在标记时需要暂停JVM用户进程

问题就是:1、效率问题:标记和清除过程效率都很低 2、会产生大量不连续的内存片段
将可用内存按照容量划分为大小相等的两块,每次只能使用其中一块,当这一块的内存用完了,将还存活的对象复制到另一块上,将使用过的内存一次性清理掉
这样每次清理内存,都是整个半区清理,不会产生大量的不连续片段

缺点:可用内存逻辑上只有一半了;如果要复制的内存过大,就会产生很大的消耗
不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界之外的内存
分代收集算法一般可以分为新生代和老年代;新生代中,每一次垃圾收集时如果大量对象死去了,选择复制算法;老年代中因为对象存活率高,就选择标记-整理或者标记-清除算法
持久代中主要存放java类的类信息,和垃圾回收的java对象关系不大,但是年轻代和老年代对垃圾回收的影响是比较大的

1、几乎所有刚刚生成的对象都是放在年轻代的,年轻代就是为了尽可能快速的收集那些生命周期短的对象
年轻代分三个区,一个Eden区,两个Survivor区,大部分对象在Eden区生成
当Eden区满时,还存活的对象被复制到一个Survivior区,当这个Survivor区满时,这个区的存活对象被复制到另一个Survivor区,这个Survivor区也满了时,从第一个Survivior区复制过来的并且仍然存活的对象,将被复制到老年代
2、老年代:年轻代经过了N次垃圾回收中仍然存活的对象,就会被放到老年代,所以老年代里存放的都是一些生命周期长的对象
3、大对象(需要大量连续内存的java对象)可能直接进入老年代
收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商,不同版本的虚拟机所提供的垃圾收集器都可能有很大区别
最基本,发展历史最悠久的收集器,曾经是虚拟机新生代收集的唯一选择。它是一个单线程的收集器,但是这个“单线程”的意义并不仅仅说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作。更重要的是,它进行垃圾收集时,必须暂停其他所有的工作线程
SWT就是虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对于用户来说是非常不好的
ParNew收集器是Serial收集器的多线程版本
一个新生代收集器,多线程的收集器。和其他收集器的关注点不同,其他收集器目的是尽可能缩短垃圾回收时用户线程的停顿时间,而Paraller Scavenge收集器目的在于达到一个可控制的吞吐量(吞吐量=用户代码的执行时间/(用户代码的执行时间+垃圾回收时间)),也即是运行用户代码的时间和CPU消耗时间的比值
停顿时间短,越适合与用户交互的程序;高吞吐量适用于高效率利用cpu的场景
Serial收集器的老年代版本,单线程收集器,使用“标记-整理”算法
Paraller Scavenge收集器的老年代版本,多线程收集器,使用“标记-整理”算法
CMS收集器是一种以获取最短停顿时间为目标的收集器,目前很大一部分的java应用集中在互联网站或者B/S系统的服务器上
CMS收集器是基于“标记-清除”算法实现的
当代收集器技术发展的最前沿成果之一
面向服务端应用的垃圾收集器
对于java虚拟机而言,执行的是.class文件
class文件是一种特定的二进制文件格式,内包含了java虚拟机指令集和符号表以及若干其他辅助信息
一个class文件对应着唯一一个类或者接口的定义信息
class文件是一组以8字节为基础单位的二进制流
根据java虚拟机规定,Class文件格式采用类似于c语言结构体的伪结构存储数据,这种伪结构只有两种类型:无符号数和表
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,习惯性以_info结尾,用于描述有层次关系的复合结构数据

1、magic:每个Class文件的头四个字节称为魔数(magic number ),用于确定这个class文件是否是可以被虚拟机接收的class文件
class文件的魔数是0xcafe babe

2、接着魔数的四个字节是版本号;minor version是次版本号,major version是主版本号。主要版本号和次要版本号共同决定了文件格式的版本。如果文件的主要版本号为 M,次要版本号为 m,则我们将其文件格式的版本表示为 M.m。
3、常量池:接下来的是常量池入口。由于常量池中常量的个数是不确定的,所以设置 constant_pool_count,用于记录常量个数(为了使用0表述“不引用任何常量池”的意义,这个计数是从1开始的)
常量池主要存放两大常量:字面量和符号引用
字面量类似于我们常说的final修饰的常量,字面值等
符号引用则属于编译原理的概念,包含三种:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符
常量池中的每一项常量都是一个表
constant_pool[constant_pool_count-1];
这是一个结构表 ,表示各种字符串常量、类和接口名称、字段名以及结构及其子结构中引用的其他常量。
jdk1.7之后已经有了14种表结构了,每一种表结构表示不同含义,例如constant_utf8_info表示utf-8编码的字符串,constant_integer_info表示整型字面量……,这14种表的第一位都是一个u1的标志位(tag),代表当前常量属于哪一种常量类型
4、访问标志
access_flags,用于识别一些类或者接口的访问信息:包括这个class是类还是接口,是否是public的,是否是abstract的,是否被声明final……
5、类索引,父类索引、接口索引集合
类索引this_class,父类索引super_class是u2类型的数据(因为java只支持单继承)
而接口索引集合 interfaces[interfaces_count]是u2类型的集合,interfaces_count用于记录集合数据的多少(一个类实现多个接口)
这几个属性,描述了类的继承关系
6、字段表集合
字段表 fields[fields_count];这些结构表示由此类或接口类型声明的所有字段,包括类变量和实例变量。
7、方法名集合
记录方法的相关信息 methods[methods_count];
这些结构表示此类或接口类型声明的所有方法,包括实例方法、类方法、实例初始化方法 以及任何类或接口初始化方法
8、属性表集合
class文件、字段表、方法表都可以携带属性表,用于描述其他信息
虚拟机要将class文件内的信息加载进入内存,对数据进行校验,解析,初始化,最终形成可以被虚拟机使用的java类型数据,这些过程就是类加载
类从被加载到虚拟机内存开始,到卸载出内存,分为7个阶段
加载-验证-准备-解析-初始化-使用-卸载,其中验证,准备,解析统称为连接
java虚拟机没有强制要求什么时候进行加载,但是在初始化时有要求
1、new一个类,或者访问类的静态方法时,如果没有初始化,那么先初始化
2、使用反射时,如果类没有使用过,要进行初始化
3、执行某一个类的main方法时,初始化
4、初始化子类时,发现父类没有初始化,先初始化父类
完成三件事情
1、通过类的全限定名获取定义此类的二进制字节流
2、将这个字节流的静态存储结构转化为方法区的运行时数据结构
3、内存中生成一个Class对象
相当于找到class对象,读取.class文件数据,利用这些数据初始化出一个class对象
第一步:验证
确保class文件的信息符合虚拟机要求并且不会对虚拟机造成攻击
第二步:准备
正式给类变量分配内存并且设置初始值(0,null)
第三步:解析
虚拟机将常量池中的符号引用替换为直接引用
根据程序员设置的值来初始化类变量
java虚拟机的角度来说,只会存在两种不同的类加载器
1、启动类加载器(Bootstrap ClassLoader)使用c/c++实现,是虚拟机的一部分
2、所有其他的类加载器,由java实现,独立于虚拟机之外
在开发人员的角度看来,主要分为3种
1、启动类加载器(Bootstrap ClassLoader):负责加载java标准库内的类
2、扩展类加载器(Extension ClassLoader):负责加载jdk扩展的类
3、应用程序类加载器(Application ClassLoader):负责加载当前项目目录中的类
下图展示的这种类加载器之间的层次关系,称之为类加载器的双亲委派模型

如果一个类加载器收到了类加载的请求,它不会先自己尝试加载这个类,而是将这个请求委派给父类加载器,每层都会如此,最终所有的请求都会交给启动类加载器;只有父类加载器反馈自己无法完成加载请求,子加载器才会尝试加载
这样设计的好处就是,如果用户自己编写了一个和标准库同名的类,比如Object类,无论哪一个类加载器要加载这个类,都会交给启动类加载器,这样加载的都会是标准库的Object类;如果不使用双亲委派模型,那么可能类加载器加载的是我们编写的Object类,这样系统中就会存在我们写的Object类和标准库的Object类,程序会混乱