1、程序计数器:一块较小的内存空间,唯一一个不会OOM的区域。字节码解释器工作时通过改变该计数器的值来选取下一条需要执行的字节码指令,属于线程私有,如果执行的是Native方法,则计数器数值为空。
2、Java虚拟机栈:线程私有,描述的是Java方法执行的内存模型。每个方法被执行的同时会创建一个栈帧,用于存储局部变量表、操作栈、动态链接和方法出口等信息。方法开始执行到结束对应栈帧入栈和出栈。
a、局部变量表存放了基本类型数据、对象引用和returnAddress类型。
基本数据类型:boolean byte short int long float double char。
对象引用:不等同与对象本身,可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象有关的位置。Sun HotSpot使用直接指针引用。
returnAddress:指向一条字节码指令的位置,目前已经很少见了,很古老的Java虚拟机用这些指令实现异常处理,现在已经被异常表代替。
b、该区域规定了两种异常情况:StackOverflowError和OutOfMemoryError。
StackOverflowError:线程请求的栈深度大于虚拟机允许的深度。
OutOfMemoryError:扩展时无法申请足够的内存。
3、本地方法栈:线程私有,和Java虚拟机栈作用类似,区别在于服务使用到的Native方法。
4、Java堆:是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,被所有线程共享,其作用是存放对象实例。
a、Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
b、大小可以扩展,通过-Xmx和-Xms分别设置最大最小内存。
c、内存不足会抛出OutOfMemoryError。
5、方法区:被所有线程共享,其作用是存储已被虚拟机加载的类信息、常量、静态变量、即时编辑后的代码等数据。
a、GC行为在这个区域比较少见,这个区域的内存回收目标主要是针对常量池的回收和类型的卸载。
b、方法区无法满足内存分配需求时,抛出OutOfMemoryError。
c、运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后存放到常量池。运行时常量池具有动态性,运行期间也可以将新的常量放入池中(如String.intern())
6、直接内存:不是虚拟机运行时数据区的一部分,在NIO中引入了基于channel和缓冲区的I/O方式,可以使用Native函数直接分配堆外内存。直接内存的分配受机器总内存的限制,动态扩展时可能出现OutOfMemoryError。
1、根搜索算法:通过一系列名为"GC Roots"对象作为起始点,从这些节点向下搜索,搜索走过的路径叫做引用链。如果一个对象到GC Roots没有引用链相连,则对象是不可用的,需要被GC。
GC Roots对象包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法JNI中引用的对象
2、引用类型分为:强引用、软引用、弱引用和虚引用。
强引用:普遍存在的类似Object o = new Object()的引用,只要引用还存在就不会被GC。
软引用:堆内存不足时会回收。
弱引用:下一次GC时回收。
虚引用:唯一目的是能在这个对象被回收时收到一个系统通知,一个对象是否有虚引用的存在不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。
3、一个对象GC Root不可达时,将会进行一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。
1、没有覆盖过该方法的对象,或者已经执行过finalize()的对象会被回收,finalize()只会被执行一次。
2、需要执行finalize()的对象会被放在名为F-QUEUE的队列中,稍后GC将对F-QUEUE中的对象进行第二次小规模标记。如果对象重新建立了引用链,将不会被回收。
标记清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收。其缺点是效率不高,同时会产生大量不连续的空间碎片。
复制算法:将内存分为两部分,GC时将存活的对象复制到另一部分,然后将已使用的内存空间一次清理掉。不会产生空间碎片,效率高,代价是可用内存变小。一般采用这种算法回收新生代。
标记整理算法:让所有的对象都向一端移动,然后直接清理掉端边界以外的部分,一般采用这种算法回收老年代,因为老年代对象存活率高。
分代收集算法:根据对象存活周期的不同将内存分为几块(新生代和老年代),根据各个年代特点采用最适当的收集算法。
Serial收集器:最基本、最悠久的单线程收集器,使用复制算法的收集器。会造成“stop the world”,即垃圾收集时所有的工作线程都会暂停等它收集结束。优点是与其他收集器相比,有更高单线程手机效率,是运行在Client模式下的默认新生代收集器。
ParNew收集器:Serial收集器的多线程版本。
Parallel Scavenge收集器:使用复制算法的并行多线程收集器收集器,其目标是达到一个可控制的吞吐量(CPU运行用户代码的时间和CPU总消耗时间的比值),适用于运算多的场景。
Serial Old收集器:Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。
CMS收集器:使用标记清除算法,以获取最短回收停顿时间为目的的收集器,其优点是并发收集和低停顿,适用于对响应速度要求高的场景。缺点是对CPU资源比较敏感、无法处理浮动垃圾以及标记清除算法会产生空间碎片。
G1收集器:当今收集器技术较为前沿的成果,基于标记整理算法,可以精确控制停顿,实现在不牺牲吞吐量的前提下完成地停顿的内存回收。
参数 | 描述 |
---|---|
UseSerialGC | client模式下默认值,使用Serial + Serial Old组合 |
UseParNewGC | 使用ParNew + Serial Old组合 |
UseConcMarkSweepGC | 使用ParNew + CMS + Serial Old组合,Serial Old作为CMS出现Concurrent Mode Failure的后备收集器 |
UseParallelGC | Server模式下的默认值,使用Parallel Scavenge + Serial Old组合 |
UseParallelOldGC | Server模式下的默认值,使用Parallel Scavenge + Parallel Old组合 |
SuvivorRatio | 新生代中Eden区域和Survivor区域的容量比值,默认为8,代表Eden:Survivor=8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小 |
MaxTenuringThreshold | 直接晋升到老年代的对象年龄,每个对象坚持过一次Minor GC后年龄加1 |
UseAdaptiveSizePolicy | 动态调整堆中各个区域大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应对新生代整个Eden和Survivor区所有对象存活的情况 |
ParallelGCThread | 设置并行GC时进行回收的线程数 |
GCTimeRatio | GC时间占总时间的比率(1/(1+值)),默认为99,即运行1%的GC时间 |
MaxGCPauseMillis | 设置GC的最大停顿时间 |
CMSInitiatingOccupancyFraction | 设置CMS在老年代空间被使用多少后触发垃圾收集,默认值为68% |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理 |
对象优先在Eden分配。
大对象直接进入老年代。大对象是指需要大量连续空间的Java对象,如很长的字符串及数组。
长期存活的对象进入老年代。
动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
空间分配担保:发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于则直接进行一次Full GC;如果小于,则查看HandlePromotionFailure是否运行担保失败,允许则进行MinorGC,
不允许也要进行Full GC。
类从加载到虚拟机内存开始到卸载出内存为止,它的生命周期包括:加载、连接、初始化、使用、卸载,其中连接阶段包括验证、准备和解析。
准备阶段是正式为类变量(static修饰)分配内存并设置初始值的阶段,这些内存都将在方法区中进行分配。
例:public static int value = 123
准备阶段会将value初始化为0,将value赋值为123是在类初始化的时候进行,参照类初始化的第一种情形。
如果是static final修饰,则在准备阶段就被初始化为123。
虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。
当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
初始化阶段是执行类构造器
有且只有四种情况会触发类的初始化:
1、使用new关键字实例化对象、读取或者设置一个类的静态字段(被final字段修饰、已在编译期把结果放入常量池的静态字段除外)以及调用一个类的静态方法时。
2、使用反射包的方法对类进行反射调用时。
3、当初始化一个类时,如果父类没有初始化则触发父类初始化。
4、包含main方法的类。
下列情形属于被动引用并不会触发类的初始化:
1、通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义该字段的类才会被初始化,因此当我们通过子类来引用父类中定义的静态字段时,只会触发父类的初始化,而不会触发子类的初始化。
2、通过数组定义来引用类,不会触发此类的初始化。
3、常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
如果一个类加载器收到了类加载的请求,它首先会委派给父类加载器加载,每一层加载器都是如此,只有当父加载器反馈无法自己完成这个加载请求,子加载器才会尝试自己去加载。
如果方法中后面的代码有一个非常耗时的操作,而前面又定义了大量占用内存的变量,那么在方法结束之前,由于slot没有被释放,大变量不会被gc,这时候可以手动设置变量为null。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化成为直接引用。另外一部分会在每次的运行期间转化为直接引用,这部分称为动态连接。
用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理表来确定的,栈帧中一般不会保存这部分信息。
Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件中存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。
在编译期间确定下来的一类方法调用称为解析。
Java虚拟机提供了5条方法调用字节码指令:
- invokestatic:调用静态方法。
- invokespecial:调用实例构造器方法、私有方法和父类方法。
- invokevirtual:调用所有的虚方法。
- invokeinterface:调用接口方法,会在运行期再确定一个实现此接口的对象。
- invokdynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,该指令的分派逻辑由用户所设定的引导方法决定。
非虚方法:解析阶段确定的方法,包括invokestatic和invokespecial指令调用的方法,即静态方法、私有方法、实例构造器和父类方法4类,还包括final修饰的方法。
虚方法:非虚方法之外的方法。
静态分派:所有依赖静态类型来定位方法执行版本的分派动作,典型应用是方法重载。静态分派发生在编译期,因此确定静态分派的动实际上不是由虚拟机决定的。
例如:Man和Woman类继承Human类
Human man = new Man()
Human woman = new Woman()
称Human为变量的静态类型或外观类型(左边的类型),Man或者Woman为变量的实际类型(右边的类型)。
如果一个类中有三个重载方法sayHello(Human human),sayHello(Man man),sayHello(Woman woman),传入上述实际类型为Man的Human对象,那么实际执行的是sayHello(Human human)方法。
动态分派:在运行期根据实际类型确定方法执行版本的分派动作,典型应用是方法重写。
invokevirtual指令的多态查找:
1、找到操作数栈顶的第一个元素所指向的对象实际类型,记做C;
2、如果在C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,通过则返回这个方法的直接引用,不通过则返回IllegalAccessError异常。
3、否则,按照继承关系从上往下依次对C的各个父类进行第二步的搜索和验证过程。
4、如果始终没有找到合适的方法,则抛出AbstractMethodError异常。
单分派和多分派:方法的接收者(执行方法的对象)和参数称为方法的宗量,单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。Java语言属于静态多分派和动态单分派的语言。
静态分派 编译器选择的依据方法接收者的静态类型和方法参数,因此属于多分派;
动态分派 编译器选择的依据只有方法的接收者,属于单分派。
Javac编译器工作:程序源码 -> 词法分析 -> 单词流 -> 语法分析 -> 抽象语法树 -> 字节码指令流。
Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流的指令大部分都是零地址指令,依赖操作数栈进行工作。
虚拟机:解释执行,也可能通过即时编译器编译执行,由虚拟机决定。
下面的八个操作都具有原子性:
把一个变量从主内存复制到工作内存,要按顺序执行read和load操作,相反要按顺序执行store和write操作。JMM要求上述两种操作必须按顺序执行,没有保证必须是连续执行。
1、保证修饰的变量对所有线程的可见性。当一条线程修改了这个变量的值,新值对于其它所有线程来说是立即得知的。
2、禁止指令重排序优化。普通变量仅仅会保证在该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
JMM对volatile变量定义的特殊规则
1、在工作内存中,每次使用变量前都必须从主内存中刷新最新的值,用于保证看见其他线程对变量所做的修改后的值(变量的use动作必须与load和read动作连续出现)。
2、在工作内存中,每次修改变量后都必须立刻同步回主内存中,用于保证看见其他线程对变量所做的修改后的值(变量的assign动作必须与store和write动作连续出现)。
3、volatile变量不会被指令重排序。
原子性:由JMM直接保证的原子性变量操作包括read、write、load、store、use和assign,我们大致可以认为基本数据类型的访问读写是原子性的。synchronized块之间的操作也具有原子性,其编译后的字节码指令对应为monitorenter和monitorexit,这两个指令会隐式使用lock和unlock操作。
可见性:当一条线程修改了这个变量的值,新值对于其它所有线程来说是立即得知的。除了volatile之外,synchronized和final也可以实现可见性。
synchronized的可见性是由“unlock操作前,必须把变量同步回主内存中”这条规则获得的。
final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看见final字段的值。
JMM中存在一些天生的先行发生关系,无需任何同步器协助。如果两个操作之间的关系不在此列,并且无法根据这些规则推导的话,虚拟机就可以对它们进行随意的重排序。
程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。(实际上在一个线程内如果前后两个操作没有依赖关系,JVM也会对这两个操作重排序,但是单线程的执行结果不能被改变,所以这种重排我们是无感知的,并不违反程序次序规则)
管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作,后面指的是时间上的先后顺序。
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,后面指的是时间上的先后顺序。
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(理解是先中断了线程才能检测到中断事件)。
对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
线程是CPU调度的基本单位,线程的引入可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度。
内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。程序一般使用内核线程的一种高级接口——轻量级进程(LWP),每个LWP进程都由一个内核线程支持,这种模型称为一对一线程模型。
每个LWP都是一个独立的调度单元,即时有一个LWP阻塞了也不会影响整个进程继续工作。但是,由于基于内核线程实现,各种进程操作都需要进行系统调用,调用代价相对较高;另外,LWP要消耗一定内核资源,因此一个系统支持的轻量级线程是有限的。
狭义的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到线程存在。这种线程不需要切换到内核态,因此操作可以是非常快且低消耗的,也可以支持更大规模的线程数量。这种模型称为一对多的线程模型。
劣势在于所有的线程操作都需要用户自己处理,程序实现起来比较复杂。JDK1.2之前使用用户线程,后面被替换成基于操作系统原生线程模型来实现。
将内核线程和用户线程一起使用的方式,用户线程还是完全建立在用户空间中,其调度通过LWP来完成,这种模型称为多对多的线程模型。
按照线程安全程度由强到弱来排序,可以将Java语言中各种操作共享的数据分为五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
最基本的同步手段是synchronized关键字,编译后同步块的前后会形成monitorenter和monitorexit两个字节码指令,这两个指令需要一个引用类型参数来指明要锁定和解锁的对象。
synchronized同步块对同一条线程来说是可重入的,执行monitorenter计算器加1,执行monitorexit计数器减1,计数器为0释放锁。
同步块在执行完之前会阻塞后面其他线程的进入,由于Java的线程是映射到系统的原生线程上的,会涉及到用户态到核心态的切换,因此synchronized是一个重量级的操作。
可重入代码:也叫做纯代码,可以在代码执行的任何时候中断,而在控制权返回后原来的程序不会出现任何错误。可重入代码有一些共同的特征,如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数传入、不调用非可重入的方法等。
线程本地存储:将共享变量的范围限制在同一个线程之内,如ThreadLocal。