能力有限,难免有错
jvm地图:
注定要用到 要从代码变成运行的程序
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。(不会调用clinit方法和init方法)
过程:
加载:根据字节码在java堆中生成一个代表这个类的java.lang.Class对象。不是在方法区
**启动类加载器(引导类加载器,Bootstrap ClassLoader)**这个类加载使用C/C++语言实现的,嵌套在JVM内部。
JAVA_HOME/lib
目录下的可以被虚拟机识别(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容)的字节码文件。java.lang.ClassLoader
类扩展类加载器(Extension ClassLoader)
JAVA_HOME/lib/ext
目录下的的字节码文件。sun.misc.Launcher
类 此类继承于启动类加载器ClassLoader
。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。应用程序类加载器
ClassPath
路径下的字节码 也就是用户自己写的类。sun.misc.Launcher.AppClassLoader
类 此类继承于扩展类加载器Launcher
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
启动类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),保证对java核心源代码的保护
tomcat 用自定义类加载器的核心原因,为了实现类加载的隔离, tomcat也打破了双亲委派机制,为了加载不同版本的web包。
怎么打破双亲委派模型?
打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和fifindClass方法。
补充:
为什么会有符号引用?
JAVA中符号引用的出现是非常自然的,因为在类没有加载的时候,也不能确保其调用的资源被加载,更何况还有可能调用自身的方法或者字段.
就算能确保,其调用的资源也不会每次在程序启动时,都加载在同一个地址.
简而言之,在编译阶段,字节码文件根本不知道这些资源在哪,所以根本没办法使用直接引用,于是只能使用符号引用代替.
此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
若该类具有父类,JVM会保证子类的clinit()执行前,父类的clinit()依据执行完毕。
**clinit是class类构造器对静态变量,静态代码块进行初始化。**顺序是由语句在源文件中出现的顺序所决定的,
public class Test{
static{
i=0;//给变量赋值可以正常编译通过
System.out.print(i);//这句编译器会提示"非法向前引用"
}
static int i=1;//它在后面
}
一个进程对应一个jvm实例,一个运行时数据区,又包含多个线程,
在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
方法区包含:
运行时常量池
自动和方法数据
构造函数和普通方法的字节码内容
一些特殊方法
推荐阅读
永久代和元空间都是对JVM规范中方法区的实现
在Java1.8中,HotSpot虚拟机已经将方法区(永久代)移除,取而代之的就是元空间。
方法区是JVM 所有线程共享的、用于存储类的信息、常量池、方法数据、方法代码等。
它存储已被Java虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码等。(《深入理解JVM》书中描述)
运行时常量池属于方法区的一部分,现在方法区移动到元空间,元空间在本地内存,和堆内存是分开的
运行时常量池用来动态获取类信息,包括:class文件元信息描述、编译后的代码数据、引用类型数据、类文件常量池等。
元空间并不在虚拟机中,而是使用本地内存 没有了内存的限制
运行时常量池就是将编译后的类信息放入方法区中,也就是说它是方法区的一部分。
运行时常量池用来动态获取类信息,包括:class文件元信息描述、编译后的代码数据、引用类型数据、类文件常量池等。
运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中。每个class都有一个运行时常量池,类在解析之后将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
运行时常量池用来动态获取类信息,包括:class文件元信息描述、编译后的代码数据、引用类型数据、类文件常量池等。
JVM系统线程(后台线程)
GC线程,编译线程,信号调度线程
方法区,你可以把它放在堆里,也可以不把它放在堆里,这个jvm的specification没有做强制要求,由具体的jvm实现自行决定,在open jdk和oracle jdk的jvm hotspot的实现中,方法区在元空间metaspace里面,不在heap堆里面,且会动态增减,有gc回收内存垃圾
其中新生代可以分为伊甸园区(Eden)、幸存区0(from)和幸存区1(to)
几乎所有的Java对象都是在Eden区被new出来的,绝大部分的Java对象都销毁在新生代了(IBM公司的专门研究表明,新生代80%的对象都是“朝生夕死”的),
oom异常:java.lang.OutOfMemoryError: Java heap space
调优:设置堆大小 -Xms10m -Xmx10m (默认大小10m 最大10m)
-XX:NewRatio=x 表示老年代/新生代 默认为2
作用
- PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
- 特点(是线程私有的 、不会存在内存溢出)
- 意:在物理上实现程序计数器是在寄存器实现的,整个cpu中最快的一个执行单元
- 是唯一一个在java虚拟机规范中没有OOM的区域
使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?
- 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的。
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
每个栈帧中存储着:
局部变量表(Local Variables) 操作数栈(operand Stack)(或表达式栈) 动态链接(DynamicLinking)(或指向运行时常量池的方法引用) 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义) 一些附加信息
生命周期和线程一致
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈是一种快速有效的分配存储方式,访问速度仅次于pc计数器。
JVM直接对Java栈的操作只有两个:
- 每个方法执行,伴随着进栈(入栈、压栈)
- 执行结束后的出栈工作
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
比如:执行复制、交换、求和等操作
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接:
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
早期绑定:
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
虚方法和非虚方法:
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。其他方法称为虚方法。
- 在类加载的解析阶段就可以进行解析 (复习一下流程:加载 验证 准备 解析 初始化)
类是一个接口,大家都知道,接口是可以被多个类实现的,编译器并不知道调的是哪个实现类的方法,自然而然就是属于编译期确定不下来的方法,即虚方法了
java类在继承中,在上转型中,java类对象实际调用的方法是子类重写的方法;也就是编译器和jvm调用的不是同一个类的方法;
也就是虚方法 需要动态链接,只能够在程序运行期将调用的方法的符号转换为直接引用
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表 (virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。 (初始化阶段完成)
指向实际调用
存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,
另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
StackOverflowError 异常:
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常
运行时数据区 | 是否存在Error | 是否存在GC |
---|---|---|
程序计数器 | 否 | 否 |
虚拟机栈 | 是(SOE) | 否 |
本地方法栈 | 是 | 否 |
方法区 | 是(OOM) | 是 |
堆 | 是(OOM) | 是 |
Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
简单地讲,一个Native Method是一个Java调用非Java代码的接囗。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制
并不提供实现体(有些像定义一个Java interface)
why?
有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意
what? 相关推荐:
java 真的可以开启线程吗?
不能!!!
在Thread.start中调用了 本地方法,底层的C++。Java无法直接操作硬件。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表和其他辅助信息
那么,如果想让一个Java程序运行起来、执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者
执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
java代码的执行分类:第一种是将源代码编译成字节码文件,然后再运行时通过解释器将字节码文件转为机器码执行
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
现在普遍使用的模板解释器,模板解释器将每一 条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。
HotSpot VM是目前市面上高性能虛拟机的代表作之一。它采用解释器与即时编译器并存的架构。在Java虛拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。 在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++程序一较高下的地步。
有些开发人员会感觉到诧异,既然HotSpotVM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内部就不包含解释器,字节码全部都依靠即时编译器编译后执行。
首先明确: 当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。 编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,执行效率高。
所以: 尽管JRockitVM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一一个平衡点。在此模式下,当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”。
这是指虚拟机的后端运行期编译器
区别于:
java代码的执行分类:第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术(JIT,Just In Time)将方法编译成机器码后再执行。
补充:
从低到高
机器码
- 各种用二进制编码方式表示的指令,叫做机器指令码。开始,人们就用它采编写程序,这就是机器语言。
- 机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错。
- 用它编写的程序一经输入计算机,CPU直接读取运行,因此和其他语言编的程序相比,执行速度最快。
- 机器指令与CPU紧密相关,所以不同种类的CPU所对应的机器指令也就不同。
指令
- 由于机器码是有0和1组成的二进制序列,可读性实在太差,于是人们发明了指令。
- 指令就是把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写,如mov,inc等),可读性稍好
- 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同,所以不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同。
指令集
- 不同的硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。
- 如常见的
- x86指令集,对应的是x86架构的平台
- ARM指令集,对应的是ARM架构的平台
汇编语言
- 由于指令的可读性还是太差,于是人们又发明了汇编语言。
- 在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。
- 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
- 由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行。
高级语言
- 为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言更接近人的语言
- 当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。
GC的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zJP9G7Zm-1677416869285)(…/…/…/tools/Typora/upload/image-20230226111927298.png)]
总而言之就是老年区满了 触发的GC
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
1.标记清除
cms使用过 现在没有垃圾回收器 使用标记清除 ,因为产生大量不连续的内存碎片,后续申请新的连续内存很不方便。
2.标记整理
标记清除后进行整理
适用于老年代
缺点: 效率低
3.标记复制
没有清除的动作了。
缺点:多占用了内存
实用于新生代 因为存活对象比较少,
分代回收思想:
根据这两代对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略
标记:
从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
GC Roots 到底是什么东西呢,哪些对象可以作为 GC Root 呢?
详细例子
便于记忆,两栈两方法
比较简单,对每一个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
实现简单,垃圾对象便于辨识;但是不能解决循环依赖
相关:
java的四种引用类型代码
下面是这个过程:
白色最后被回收
回收线程和用户线程并发的问题
new的对象先放伊甸园区。此区有大小限制。
当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。将伊甸园中的剩余对象移动到幸存者0区。
然后加载新的对象放到伊甸园区
如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
啥时候能去老年区呢?可以设置次数。默认是15次。·可以设置参数:
-XX:MaxTenuringThreshold=进行设置。
在老年区,相对悠闲。当老年区内存不足时,再次触发GC:Major GC,进行老年区的内存清理。
若老年区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常
总结:
清空伊甸园和from, 清理后都复制到to
针对幸存者s0,s1区:复制之后有交换,谁空谁是to 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。
属于并发 并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。
9废弃,14移除,要响应时间可用
G1 (Garbage一First) 垃圾回收器是在Java7 update4之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。 与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time) ,同时兼顾良好的吞吐量。 官方给G1设定的目标是在延迟可控(STW)的情况下获得尽可能高的吞吐量
,所以才担当起“全功能收集器”的重任与期望。
参考地址