1.JVM的生命周期

虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的执行
一个运行中的Java虚拟机有这一个清晰的任务:执行Java程序。
程序开始执行时他才运行,程序结束时他就停止。
执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。
虚拟机的退出
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程终止
某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exi或halt操作
除此之外,JNI(Java Native Interface)规范描述了用JNI iNvocation API来加载或写在Java虚拟机时,Java虚拟机的退出情况。

2.流行虚拟机

JRocket(服务端虚拟机),启动较慢,运行快 -舍弃解释器,使用即时编译器
J9 与IBM耦合
hotspot 当下最流行,解释器与即时编译器配合使用

大纲:
JVM与Java体系结构
类加载子系统
运行时数据区概述及线程
程序计数器
虚拟机栈
本地方法接口
本地方法栈

方法区
直接内存
执行引擎
StringTable
垃圾回收概述
垃圾回收相关算法
垃圾回收相关概念
垃圾回收器

3.类加载子系统:

类加载器子系统负责从文件系统或网络中加载Class文件,class文件在文件开头有特定的文件标识
ClassLoader只负责class文件的加载。至于它是否可以运行,则由Execution Engine决定
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池的信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)
类加载子系统包括三个阶段

类加载阶段(三大加载器(两类)->引导类加载器、【扩展类加载器、系统类加载器】自定义类加载器)
1.通过一个类的全限定名来获取定义该类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

类加载器分类
虚拟机自带的加载器
1.启动类加载器(引导类加载器,Bootstrap ClassLoader)
这个类加载器使用C/C++语言实现的,嵌套在JVM内部。
它用来加载Java的核心库(JAVAHOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容,用于提供JVM自身需要的类)
并不继承java.lang.ClassLoader,没有父加载器。
加载扩展类和应用程序类加载器,并制定他们为他们的父类加载器(扩展类加载器和系统类加载器由Bootstrap classLoader所加载)
出于安全考虑,Bootstrap 启动类加载器只能加载包名为java、javax、sun等开头的类。
2.扩展类加载器(Extension ClassLoader)
Java语言编写,派生与ClassLoader类
父类加载器为启动类加载器
从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
3.系统类加载器(AppClassLoader)
Java语言编写,派生与ClassLoader类
父类加载器为扩展类加载器
它负责加载环境变量classpath或系统属性 java.class.path制定路径下的类库
该类是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
用户自定义类加载器
在Java的日常开发中,类的加载几乎都是由上述3中类加载器相互配合执行的,在必要时,我们还可自定义类加载器,来定制类的加载方式_
目的:

  • 隔离加载类
  • 修改类加载方式
  • 扩展加载源
  • 防止源码泄漏

双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,是一种任务委派模式。
工作原理:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
如果父类还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
如果父类加载器可以完成加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

简要说明下面自定义类因全限定名 同 java.lang.String(无main方法) 会委托至 引导类加载器加载,而不是系统类加载器加载 故 无法加载静态代码块代码(也保证了基础类库不能被恶意篡改的风险)
调用main方法错误
双亲委派机制的优势:
避免类的重复加载
保护程序安全,防止核心API被恶意篡改

其他:两个class对象是否为同一个类存在两个必要条件
类的全限定名相同
加载类的加载器相同
连接阶段(验证、准备、解析)
1.验证:
目的在于确保Class文件的字节流中包含信息符合当前虚拟机的要求,保证被加载的正确性,不会危害虚拟机自身安全
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
2.准备:
为类变量分配内存并且设置该变量的默认初始值,即零值
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会是显示初始化
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
3.解析:
将常量池内的符号引用转换为直接引用的过程
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在(java虚拟机规范)的class文件格式中,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

初始化
1.初始化阶段就是执行类的构造器方法()的过程
此方法不需要定义,是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来【可在字节码文件中查看】
构造器方法中的指令按语句在源文件中出现的顺序执行
2.()不同于类的构造器。(关联:构造器是虚拟机视角下的())
3.若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
4.虚拟机必须保证一个类的()方法在多线程下被同步加锁

4.运行时数据区概述及线程

  • 程序计数器

PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令。

关于程序计数器两个常见问题:

1.使用PC寄存器存储字节码指令地址有什么用呢?、为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来之后,就得知道接着从哪开始继续执行。 JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
2.PC寄存器为什么会被设定为线程私有?

  • 虚拟机栈

内存中的栈和堆:
栈是运行时的单位,而堆是存储的单位。
栈解决程序运行的问题,程序如何执行,如何处理数据;堆解决的数据存储的问题,数据怎么放,放在哪。

Java虚拟机栈是什么?
Java虚拟机栈,早期也叫Java栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次次的Java方法调用
是线程私有的
生命周期:
同线程的生命周期
作用:
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈的优点:
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
JVM直接对Java栈的操作只有两种
每个方法执行,伴随着进栈(push)
执行结束后的出栈工作(pop)
对于栈来说,不存在垃圾回收问题
栈帧的内部结构:
每个栈帧中存储着:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)(或表达式栈)
  • 动态链接( Linking)(或指向运行时常量池的方法引用)
  • 方法返回地址(Return Address)(或方法正常退出和异常退出的定义)
  • 一些附加信息

局部变量表(Local Variables)
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用、以及returnAddress类型。
由于局部变量表是建立在线程上的栈上,是线程的私有数据,不存在数据安全问题。
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
(行号表 Start PC[指令行号] LineNumber[代码行号])

关于Slot的理解
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
局部变量表,最基本的存储单元是Slot(变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用数据类型(reference),returnAddress类型的变量。
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可,(比如 访问long或者double类型的变量)
如果当前帧是由构造方法或者实例方法创建的那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。【静态方法中无法使用this关键字:this关键字在该方法的局部变量表中不存在】

slot重复利用
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期的局部变量的槽位,从而达到节约资源的目的。

静态变量与局部变量的对比
变量的分类
按照数据类型分:① 基本数据类型 ② 引用数据类型
按照在类中声明的位置分:① 成员变量:在使用前都经过默认初始化赋值
类变量:linking的prepare阶段给类变量赋默认值 ——> initial阶段,进行对静态代码块进行赋值
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
② 局部变量:在使用前,必须进行显示赋值! 否则,编译不通过
局部变量表中的变量也是重要 垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

操作数栈(Operand Stack)
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈。
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。
比如:执行复制、交换、求和等操作

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
栈中的任何一个元素都是可以任意的Java数据类型。
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问。

动态链接(Dynamic Linking)
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是将这些符号引用转换为调用方法的直接引用

方法返回地址(Return Address)
存放调用该方法的pc寄存器的值
一个方法的结束有两种方式:
正常执行完成
出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回改方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,及调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

问题:方法内部定义的局部变量一定线程安全嘛?

不一定
① 线程安全
② 线程不安全 (有返回值,此返回值可能会被其他线程方法抢占)
③ 线程安全 s1在方法内部已经消亡,s1.toString()相当于重新new一个字符串,相对于s1是线程安全,s1.toString()可能不安全。

=============================================================================

Java堆

堆的核心概述:
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间
堆内存大小可以调节
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
所有的线程共享Java堆,在这里还可以划分线程私有缓冲区(Thread Local Allocation Buffer,TLAB)

设置堆大小 -Xms600m -Xmx600m
设置栈大小 -Xss

年轻代和老年代:
在HotSpot中,Eden空间和另外两个Survivor空间缺省所占比例默认为8:1:1
开发人员可以通过选项“-XX:SurvivorRatio” 调整这个空间比例。比如-XX:SurvivorRatio=8
几乎所有的Java对象都是在Eden区被new出来的
绝大部分的Java对象的销毁都在新生代进行了
可以使用“-Xmn”设置新生代最大内存大小 一般使用默认值就可以

-XX:NewRatio: 设置新生代与老年代的比例,默认值是2
-XX:SurvivorRatio: 设置新生代中Eden区与Survivor区的比例
-XX:-UserAdaptiveSizePolicy 关闭自适应的内存分配策略(暂时用不到)
-Xmn:设置新生代大小 (一般不设置)

年轻代和老年代:

  1. new的对象先放伊甸园区,此区有大小限制
  2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区不在被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
  3. 然后将伊甸园区剩余的对象移动到幸存者0区
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区
  5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区
  6. 可以设置次数 经历多少轮回收后幸存下的对象进入老年区 称为阈值 默认是15
    1. 设置参数:-XX:MaxTenuringThreashold=进行设置

总结:
针对幸存者S0,S1区的总结:复制之后有交换,谁空谁是to区
关于垃圾回收:频繁在新生区收集,很少在老年区收集,几乎不在元空间(Meta Space)收集

模拟堆溢出

Minor GC、Major GC、 Full GC

JVM在进行GC时,并非对上面三个内存区域(新生代、老年代、方法区)一起回收的,大部分时候回收的都是指新生代
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大类型:一种是部分收集(partial GC),一种是整堆收集(Full GC)
部分收集:不是完整收集整个Java堆的垃圾收集,其中又分为:
新生代收集(Minor GC/ Young GC):只是新生代的垃圾收集
老年代收集(Major GC/ Old GC):只是老年代的垃圾收集
目前,只有CMS GC会有单独收集老年代的行为
注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是来年代回收还是整堆回收
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
目前,只有G1 GC会有这种行为
整堆回收(Full GC):收集整个Java堆和方法区的垃圾收集

年轻代GC(Minor GC)触发机制:
当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden区满,Survivor区满不会引发GC(每次Minor GC会清理年轻代的内存)
因为Java对象大多都具备朝生夕灭的特征,所以 Minor GC 非常频繁,一般回收速度也比较快。
Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行

老年代GC(Major GC)触发机制:
指发生在老年代的GC,对象从老年代消失时,我们说“Major GC” 或 “Full GC”发生了
出现了Major GC,经常伴随至少一次Minor GC (但非绝对的,在Parallel Scavenge收集策略里就有直接进行Major GC的策略选择过程)
也就是在老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC
Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
如果Major GC后,内存还不足,就报OOM了

堆是对象分配的唯一空间吗?

堆并不是对象分配的唯一空间。此外,还存在栈上分配和标量替换。
如何将堆上的对象分配到栈,需要使用逃逸分析手段。
这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:
当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。(栈上分配)
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

逃逸分析:代码优化
使用逃逸分析,编译器可以对代码做如下优化:
一、栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
二、同步策略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
三、分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

5.对象实例化

1.对象实例化的几种方式

1.通过new关键字 2.Class的newInstance(),反射方式,权限必须是public 3.Constructor的newInstance(Xxx),反射方式,可以空参或有参,没权限要求 4.使用clone() 5.使用反序列化 6.第三方库的Objenesis

2.对象实例化步骤

对象创建的步骤.xmind

对象分配内存流程
JVM - 图1

6.垃圾收集篇

1.概念

  • 什么是垃圾?

垃圾是指在程序运行过程中没有任何指针执行的对象,这个对象就是需要被回收的垃圾。

  • 什么是内存溢出?

如果不及时对内存中的垃圾进行清理,这些垃圾对象所占的内存空间就会一直保留到应用程序结束,被占用的空间无法被其他对象使用,就会造成内存溢出。

  • 什么是内存泄漏?

如果对象已经属于在程序中不再使用,但是不符合垃圾回收的标准仍然存在内存中,就造成了内存泄漏,久而久之就会引发内存溢出问题。

2.判断对象是否存活

引用计数器、可达性分析

1.引用计数器
为每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。(无法解决循环引用的问题)
2.可达性分析
可达性分析算法是以根对象集合(GC ROOTS)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。

什么是GC ROOTS?

1.虚拟机栈中引用的对象(各个线程被调用的方法中使用到的参数、局部变量等。) 2.本地方法栈内JNI(本地方法)引用的对象 3.方法区中静态属性引用的对象 4.方法区中常量引用的对象(字符串常量池里的引用) 5.所有被同步锁synchronized持有的对象 6.java虚拟机内部的引用(基本数据类型对应的class对象,一些常驻的异常对象。如:NullPointerException、OutOfMemoryError,系统类加载器) 7.反应java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
分代局部收集时,例如YoungGC时,老年代的引用也作为GC ROOTS节点

进行可达性分析就必须保证在一个能保障一致性的快照中进行。这也是导致GC进行时必须“stop the world”的一个重要原因

3.对象的finalization机制

java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个finalize()方法。

finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件、套接字、数据库连接等。

finalize()方法可以对对象进行“复活”,但是只能一次,因为垃圾收集器只能调用一次finalize()方法,当下次进行垃圾收集(被第一次标记不可达)则对象就一定会被清理。

1.具体过程

判定一个对象objA是否可回收,至少要经历两次标记过程:
1.如果对象objA到GC ROOTS没有引用链,则进行第一次标记。
2.进行筛选,判断此对象是否有必要执行finalize()方法
1)如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机被视为没有必要执行,objA被视为不可触及的(不可达)。
2)如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动床技安的、低优先级的Finalizer线程触发其finalize()方法执行。
3)finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记,如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移除“即将回收”集合,之后,对象会再次出现没有引用存在的情况,在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,finalize方法只会被调用一次。