赞
踩
JVM内存布局
差异主要在于方法区。
1. 使用程序计数器存储字节码指令地址的作用/为什么使用程序计数器记录当前线程的执行地址?
因为CPU需要不停地切换各个线程,而切换回来以后,必须知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么字节码指令。
2. 程序计数器为什么设计成线程私有?
所谓的多线程并发,在一个特定时间段只会执行其中一个线程的方法(CPU时间片),CPU会不停地做任务切换,必然导致经常中断和恢复。为了能够准确地记录各个线程下一条要执行的字节码指令的地址,最好的的办法就是为每个线程都分配一个独立的程序计数器,各个线程进行独立计算,不会互相干扰。
最基本的存储单元是slot变量槽
32位(byte、short、char、boolean存储前被转换为int)占用一个slot,64位类型(long和double)占用两个slot。如果访问64bit的局部变量,只需使用第一个索引。
JVM为局部变量表中的每个Slot分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
当一个实例方法(非静态)被调用,局部变量按顺序被复制到局部变量表中的每一个slot上。
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this,会存放在索引为0的slot处,其余的参数表顺序继续排列。静态方法不能使用this,因为this变量不存在于当前方法的局部变量表(this是属于对象的,而静态方法不用对象就可以调用,而实例方法必须用对象调用)。
槽位是可以重复的,如果一个局部变量过了其作用域,那么其作用域之后申明的新的局部变量就有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
局部变量VS静态变量(类变量)
变量按照数据类型分为基本数据类型和引用数据类型;
按照声明的位置分为成员变量和局部变量;
成员变量分为静态变量和实例变量;
成员变量:在使用前经历过初始化过程(类加载中的初始化)
静态变量:链接的准备阶段给类变量默认赋值,初始化阶段显示赋值,即静态代码块赋值
实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
局部变量:在使用前,必须显式赋值,否则编译不通过
栈顶缓存技术ToS(Top of Stack Cashing):
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理CPU的寄存器中,依此降低对内存的读写次数,提升执行引擎的执行效率。
常量池VS运行时常量池
常量池在字节码文件中,运行时常量池在运行时的方法区中。
为什么需要常量池?
常量池提供一些符号和常量,便于指令的识别。
没有也行,但是很占内存。
方法的调用:
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接-早期绑定-非虚方法
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行时期间保持不变,这种情况下,将调用方的符号引用转为直接引用的过程称为静态链接。构造函数?
动态链接-晚期绑定-虚方法-体现 ”多态“特性
如果被调用的方法无法再编译期被确定下来,只能在运行期将调用的方法的符号引用转为直接引用,这种引用转换过程具备动态性,因此被称为动态链接。需要实例化对象调用?
方法的绑定:描述程序中变量或方法在什么时间确定其地址或实现的方式
绑定是一个字段、方法、或者类在符号引用被替换为直接引用的过程。仅仅发生一次。
早期绑定:
被调用的目标方法如果在编译期可知,且运行期保持不变。静态链接通常是早期绑定。
晚期绑定:
被调用的方法在编译期无法被确定,只能够在程序运行期根据实际的类型绑定相关的方法。动态链接通常是晚期绑定的。
虚方法VS非虚方法:描述方法是否在运行时根据对象的实际类型来决定调用哪个方法
Java中任何一个普通方法都具备虚函数的特征(运行期确认,具备晚期绑定的特点)。如果在java程序中,不希望某个方法拥有虚函数的特征,则可以使用关键字final来标记这个方法。
非虚方法:
如果方法在编译期就确定了具体的调用版本,在运行时不可变。这样的方法称为非虚方法。静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法。(不能被重写的)
虚方法:
其他方法称为虚方法(会被重写的方法)。虚方法的调用是在运行时确定的,根据对象的实际类型决定调用哪个派生类的重写方法。
子类对象的多态性的使用前提:①类的继承关系 ②方法的重写
方法调用指令
1. 普通调用指令
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用<init>方法,私有及父类方法,解析阶段确定唯一方法版本
invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法,会被重写
shouFinal(); // invokevirtual,虽然是invokevirtual,但被final修饰,是非虚方法。
super.showFinal(); // invokespecial,调用父类
showCommon(); // invokevirtual,子类未重写该方法,调用的是父类的方法,但没有显示的super., 所以还是invokevirtual
四条普通指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。
2. 动态调用指令:JDK1.7新增,为了实现动态类型语言而做的改进
invokedynamic:动态解析出需要调用的方法,然后执行
直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式
静态语言和动态语言
区别在于对类型的检查是编译器还是运行期,满足编译期就是静态类型语言,反之就是动态类型语言。
静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息。变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
Java是静态类型语言,编译时报错。invokedynamic动态调用指令增加了动态语言的特性。
python和JavaScript是动态类型语言。
方法重写的本质
- 找到操作数栈顶所执行的对象的实际类型,记做C
- 如果在类型C中找到与常量池中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不通过,则返回java.lang.IllegalAccessError非法访问异常
- 否则,按照继承关系从下往上依次对C的各个父类进行上一步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
IllegalAccessError:程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变
AbstractMethodError:调用了抽象类方法
虚方法表:更快的找到调用哪一个方法
面向对象的编程中,会很频繁的使用动态分配,如果每次动态分配的过程都要重新在类的方法元数据中搜索合适的目标的话,就可能影响到执行效率。
因此为了提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找。非虚方法不会出现在表中,因为非虚方法已经可以确定。
每个类都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表会在类加载的链接阶段被创建,并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法也初始化完毕。
Dog重写的两个方法指向自身,其他指向父类。
无论哪种方式退出,方法退出后都会返回该方法被调用的位置。
方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
异常退出的,返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息。
上述异常处理表表示:字节码指令4~8行出现异常,如果没有处理,goto到16行return返回结束;如果处理,按target-11行处理
执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口 。
在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
返回指令包括ireturn-boolean,byte,char,short,和int类型、lreturn-long类型、freturn-float类型、dreturn-double类型、areturn-引用类型。另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
本地方法:
就是一个Java调用非Java代码的接口,就是一个Java方法,该方法的实现由非Java语言实现。
在定义一个native method时,并不提供实现体(有点像抽象方法),因为其实现体是由非iava语言在外面实现的。native不能用abstract一起使用,因为abstract抽象方法没有方法体,而native是有方法体的。
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
目前使用较少,除非是与硬件相关。
为什么使用本地方法?
在有些层面任务中,Java实现并不容易,或对效率很在意。
与Java环境外交互
与操作系统交互
例如与操作系统底层或硬件交换信息时的情况
例如启动一个线程
内存中的栈与堆:
栈是运行时的单位,而堆是存储的单位。
栈解决程序如何执行,如何处理数据。堆解决的是数据存储问题,即数据怎么放,放在哪里。
通常将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾会后清理完堆区后,不需要重新分隔计算堆区的大小,从而提高性能
为什么要分代?
其实不分代完全可以,分代的唯一理由就是优化GC性能。
如果没有分代,那所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
Minor GC和Major GC区别:
Minor GC:简单理解就是发生在年轻代的GC。三步(复制--清空--互换)
Minor GC的触发条件为:
当产生一个新对象,新对象优先在Eden区分配。如果Eden区放不下这个对象,虚拟机会使用复制算法发生一次Minor GC,清除掉无用对象,同时将存活对象移动到Survivor的其中一个区(fromspace区或者tospace区)。
虚拟机会给每个对象定义一个对象年龄(Age)计数器,对象在Survivor区中每“熬过”一次GC,年龄就会+1。待到年龄到达一定岁数(默认是15岁),虚拟机就会将对象移动到年老代。
如果新生对象在Eden区无法分配空间时,此时发生Minor GC。发生MinorGC,对象会从Eden区进入Survivor区,如果Survivor区放不下从Eden区过来的对象时,此时会使用分配担保机制将对象直接移动到年老代。
1.第一次Yong GC(Minor GC)后,Eden区还存活的对象复制到Surviver区的“To”区,“From”区还存活的对象也复制到“To”区,
2.再清空Eden区和From区,这样就等于“From”区完全是空的了,而“To”区也不会有内存碎片产生,
3.等到第二次Yong GC时,“From”区和“To”区角色互换,很好的解决了内存碎片的问题
Major GC的触发条件:
Major GC又称为Full GC。当年老代空间不够用的时候,虚拟机会使用“标记—清除”或者“标记—整理”算法清理出连续的内存空间,分配对象使用。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC 前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。
当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM (Out of Memory)异常。
指内存的永久保存区域,主要存放Class 和Meta (元数据)的信息,Class在被加载的时候被放入永久区域,它和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class 的增多而胀满,最终抛出OOM异常。
JAVA8与元数据 :
在Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入native memory,字符串池和类的静态变量放入java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
1、new的对象先放在Eden区,此区有大小限制
2、当创建新对象,Eden空间填满时,会触发Minor GC,将Eden不再被其他对象引用的对象进行销毁,将Eden中剩余的对象移到Survivor S0区,Eden区被清空,再加载新的对象放到Eden区
3、当Eden区又满了,再次触发垃圾回收,此时将Eden区和Survivor S0区中没有被回收的对象,就会放到Survivor S1区
4、再次经历垃圾回收,又会将幸存者重新放回Survivor S0区,依次类推
5、可以设置年龄计数器,每一次幸存年龄+1,默认是15次,超过15次,则会将幸存者区幸存下来的转去老年区:-XX:MaxTenuringThreshold=N进行设置
垃圾回收频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间搜集
Survivor区满了不会触发minor GC,但不代表没有GC。
超大对象eden放不下,就要看Old区大小是否可以放下。old区也放不下,需要FullGC(MajorGC)
为什么使用TLAB:Thread Local Allocation Buffer
堆区是线程共享区域,任何线程都可以访问到堆区的共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
-XX:TLABWasteTargetPercent 设置TLAB空间所占用Eden空间的百分比大小
堆是分配对象的唯一选择吗
随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术,将会导致一些微秒变化,所有对象分配到堆上渐渐变得不那么绝对了。
有一种特殊情况,如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样无需堆上分配,也不需要垃圾回收了,也是最常见的堆外存储技术
逃逸分析的基本行为就是分析对象动态作用域。
将堆分配转为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
如果一个对象被发现只能从一个线程被访问到,对于这个对象的操作可以不考虑同步。
JIT编译器可以借助逃逸分析来判断同步块所使用的的锁对象,是否只能够被一个线程访问,而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候,就会取消对这部分代码的同步。这样就大大提高并发性和性能,这个取消同步的过程就叫同步省略,也叫锁消除
分离对象:有的对象可能不需要作为一个连续的内存结构存在,也可以被访问到,那么对象的部分(或全部)可以不存储在内存。而是存储在CPU寄存器中
标量是指一个无法再分解的更小的数据的数据。Java中原始数据类型就是标量
可以分解的数据叫聚合量,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量
当xms、xmx、xmn、newratio确定,survivorratio设置的过大(即Eden区很大,survivor区很小),会导致,minor GC时,survivor区放不下而将对象存到老年代,失去了minor GC和分代的意义。survivorratio设置的过小(即Eden区很小,survivor区很大),会导致eden区很快存满,频繁的进行minor GC,影响用户进程,STW的时间变多。
空间分配担保:jdk7此参数失效
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间,是否大于新生代所有对象的总空间:
- 如果大于,则此次MinorGC是安全的
- 如果小于,则查看-XX:HandlePromotionFailure设置是否允许担保失败
true:
继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
大于,则尝试进行一次MinorGC,但是这次MinorGC依然是有风险的
小于,则改为进行一次FullGC
- false
则改为进行一次FullGC
jdk7此参数失效,规则改为只要老年代的连续空间大于新生代对象总大小,或者历次晋升的平均大小,就会进行MinorGC,否则进行FullGC
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。