文章目录
JVM概述
JVM(Java Virtual Machine)是Java平台的核心组件,它负责在运行时环境中执行Java字节码。JVM使得Java成为一种“一次编写,到处运行”(Write Once, Run Anywhere)的编程语言,因为无论在哪个平台上,只要安装了支持该平台的JVM,就可以运行Java程序。
以下是关于JVM的一些关键点:
- 跨平台性:JVM是Java跨平台性的关键。Java源代码被编译成字节码(.class文件),这些字节码可以在任何支持JVM的平台上运行。
- 类加载器:JVM使用类加载器(ClassLoader)机制来动态加载Java类到Java运行时环境中。这允许JVM在运行时根据需要加载类。
- 内存管理:JVM管理Java程序的内存,包括堆内存(Heap Memory)和方法区(Method Area)等。堆内存主要用于存储对象实例,而方法区则存储类的元数据、常量池等信息。JVM还提供了垃圾回收机制来自动管理不再使用的内存。
- 执行引擎:JVM的执行引擎负责解释或编译执行Java字节码。一些现代的JVM实现(如HotSpot)采用了即时编译(JIT, Just-In-Time Compilation)技术,将热点代码(经常执行的代码)编译成本地机器码以提高执行效率。
- 本地方法接口(JNI):JVM允许Java代码与本地代码(如C、C++代码)进行交互。这通过JNI(Java Native Interface)实现,它使得Java代码可以调用本地库中的函数。
- 线程和同步:JVM支持多线程并发执行Java程序。它提供了内置的线程同步机制,如synchronized关键字和Lock接口,以确保多个线程之间正确地共享数据和资源。
- 安全管理器:JVM内置了一个安全管理器(SecurityManager),用于实施Java的安全策略。它可以控制对系统资源的访问权限,防止恶意代码的执行。
- 性能调优和监控:JVM提供了丰富的性能调优和监控工具,如jconsole、jvisualvm等,这些工具可以帮助开发人员监控和分析Java程序的运行情况,以便进行性能优化和问题排查。
JVM 组成部分
JVM(Java Virtual Machine)其实是一套标准。通过定义虚拟机,像真实计算机一样,能够运行字节码指令。JVM的好处是可以屏蔽操作系统的细节, 使Java可以一次编写,到处运行。
JVM的运行原理:JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。
Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。
JVM的位置 JVM在操作系统之上,而操作系统在硬件之上
JVM的体系结构
方法区Method Area,它的主要功能是存储运行时常量池、字段和方法的元数据和类的的元数据。
堆heap,对象和数据,主要是用来存储Java对象的实例,也就是我们new的类都存在堆区。
栈stack ,方法,是通过线程的方式运行来加载各种方法
程序计数器,是负责保存每个线程执行的方法的地址
本地方法区,负责加载并运行native类型的方法
程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解
析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳
转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字 节码)等数据
堆:初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要 在堆上分配
栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操 作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所 以还是一个指向地址的指针
本地方法栈:主要为 Native 方法服务
程序计数器:记录当前线程执行的行号
本地方法栈
关键字native修饰的方法,都是Java作用范围不到的,会回去调底层C语言的库(也就是Java调用非Java的东西)
本地方法栈(Native Method Stack)
用来登记native方法
在执行引擎(Execution Engine)执行时,通过本地方法接口(JNI)加载本地方法库中的方法
本地方法接口 :扩展Java的使用,融合不同编程语言为Java所用(最初是调用C,C++)
Thread的start()方法源码中,是调用本地方法start 0()去创建线程
private native void start0();
native方法用的越来越少,除非是有关硬件的应用:
public static native long currentTimeMillis();
程序计数器
每个线程都有一个程序计数器,是线程私有的,指向方法区中的方法字节码
比如在执行引擎每一次要读取下一条指令时+1
非常小的内存,可以忽略不计
方法区
方法区属于共享区域 方法区被所有线程共享
所有定义方法的信息都保留在这个区域
静态变量,方法,静态常量池,类信息(构造方法,接口定义,枚举,注解等)
实例变量(成员变量)在堆内存,与方法区无关
- 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里
进行的 GC 主要是对方法区里的常量池和对类型的卸载 - 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后
的代码等数据。 - 该区域是被线程共享的。
- 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池
具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量
池中。
栈
主管程序运行
生命周期与线程同步
一旦线程结束,栈内存也就释放
因此不存在垃圾回收机制
八大基本类型,对象引用,实例方法
先进后出,后进先出
为什么main方法先执行后结束
栈内存就像一个大桶,main方法最先放进去,然后再放其他方法,方法执行完就自动弹出去,所以main方法最后弹出去
栈存储: 存放对象的引用
栈帧(Java中的方法,JVM的栈帧)主要保存3种数据:
本地变量:输入输出参数,方法内的变量
栈操作:记录入栈,出栈操作(PC寄存器指针)
栈帧数据:类文件,方法
栈的运行原理
栈中的数据以栈帧的格式存在
每一个方法执行的同时就会创建一个栈帧(用于存储局部变量表,操作数据栈,动态链接,方法出口等信息)
每一个方法执行到结束的过程对应着栈帧在JVM中入栈到出栈的操作过程
原理示意图
递归错误——栈溢出
public static void main(String[] args) {
}
public void stack1(){
stack2();
}
public void stack2(){
stack1();
}
栈的空间是有限的,当错误递归撑爆了栈后就会发生错误:栈溢出(StackOverflowError ),不是异常
堆Heap
一个JVM只有一个堆内存,并且堆内存的大小可以调节java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。 java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。 从内存回收角度来看java堆可分为:新生代和老生代。 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了 更好的回收内存,或者更快的分配内存。根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是 可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩 展时,将会抛出OutOfMemoryError异常。
堆内存:
● 类加载器读取类文件后,一般会把类,引用方法(指向方法区),运行时常量池,实例变量放在堆内存
● new产生的所有对象都在堆内存
● 数组
三个区
● 新生代
○ 伊甸园区(eden)
■ 所有的对象都是伊甸园区new出来的
○ 幸存to区( Survive to)
○ 幸存from区( Survive from)
○ 内存占比–>eden:Survive to:Survive from=8:1:1
● 老年代
● 永久代
○ 常驻内存,用来存放JDK自身携带的Class对象,存储的是Java运行时的一些环境
○ JDK 6之前:永久代,常量池在方法区
○ JDK 7 : 永久代,慢慢退化,去永久代,字符串常量池在堆中
○ JDK 8后 :无永久代,方法,常量池在元空间,元空间仍与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中
○ 不存在垃圾回收,关闭JVM就会释放这个区域的内存
○ 什么情况下永久区会崩:
■ 一个启动类加载大量第三方jar包
■ tomcat部署太多应用
■ 大量动态生成反射类,不断被加载直到内存满,就出现OOM
元空间:逻辑上存在堆,物理上不存在堆
GC垃圾回收主要在伊甸园区,养老区
堆栈区别
堆 heap内存用来存放 ,由new创建的对象和数组 堆是先进先出,后进后出
栈 stack 是栈内存用来存放方法或者局部变量等 栈 后进先出,先进后出
JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
功能方面:堆是用来存放对象的,栈是用来执行程序的。
共享性:堆是线程共享的,栈是线程私有的。
空间大小:堆大小远远大于栈。
VM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而
对象总是在堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。
- 申请方式
stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间
heap:需要程序员自己申请,并指明大小,在 c 中 malloc 函数,对于 Java 需要手动 new Object()的形式开辟 - 申请后系统的响应
stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 - 申请大小的限制
stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(也有的说是 1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 - 申请效率的比较:
stack:由系统自动分配,速度较快。但程序员是无法控制的。
heap:由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。 - heap 和 stack 中的存储内容
stack: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,
然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 - 数据结构层面的区别
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第 1 个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。
虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
JVM 运行时数据区
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这 些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区 域则是依赖线程的启动和结束而建立和销毁。
为什么要线程计数器?因为线程是不具备记忆功能
Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创 建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息; 栈帧就是Java虚拟机栈中的下一个单位
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实 例都在这里分配内存;
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的
代码等数据。
JVM由那些部分组成,运行流程是什么
JVM包含两个子系统和两个组件: 两个子系统为Class loader(类装载)、Execution engine(执行引
擎); 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到
Runtime data area中的method area。
Execution engine(执行引擎):执行classes中的指令。
Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
流程 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到
内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一 套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎
(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要 调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
JVM 为什么使用元空间替换了永久代
我们都知道Java8以及以后的版本中,JVM运行时数据区的结构都在慢慢调整和优化。
但实际上这些变化,对于业务开发的小伙伴来说,没有任何影响。
在Hotspot虚拟机中,方法区的实现是在永久代里面,它里面主要存储运行时常量池、Klass类元信息等。永久代属于JVM运行时内存中的一块存储空间,我们可以通过-XX:PermSize来设置永久代的大小。当内存不够的时候,会触发垃圾回收。
在JDK1.8里面,JVM运行时数据区是这样的。
在Hotspot虚拟机中,取消了永久代,由元空间来实现方法区的数据存储。
元空间不属于JVM内存,而是直接使用本地内存,因此不需要考虑GC问题。
默认情况下元空间是可以无限制的使用本地内存的,但是我们也可以使用JVM参数来限制内存使用大小。
我认为有三个方面的原因:
在1.7版本里面,永久代内存是有上限的,虽然我们可以通过参数来设置,但是JVM加载的class总数、大小是很难确定的。所以很容易出现OOM问题。但是元空间是存储在本地内存里面,内存上限比较大,可以很好的避免这个问题。
永久代的对象是通过FullGC进行垃圾收集,也就是和老年代同时实现垃圾收集。替换成元空间以后,简化了Full GC。可以在不进行暂停的情况下并发地释放类数据,同时也提升了GC的性能Oracle要合并Hotspot和JRockit的代码,而JRockit没有永久代。
JVM分代年龄为什么是15次?可以25次吗?
首先,在JVM的heap内存里面,分为Eden Space、Survivor Space、Old Generation。
当我们在Java里面使用new关键字创建一个对象的时候,JVM会在Eden Space分配一块内存空间来存储这个对象。
当Eden Space的内存空间不足的时候,会触发Young GC进行对象回收。
那些因为存在引用关系而无法回收的对象,JVM会把它们转移到Survivor Space。
Survivor Space内部又分为From区和To区,刚从Eden区转移过来的对象会分配到From区,每经历一次Young GC,这些没有办法被回收的对象就会在From区和To区来回移动,每移动一次,这个对象的GC年龄就加1。默认情况下GC年龄达到15的时候,JVM就会把这个对象移动到Old Generation。
其次呢,一个对象的GC年龄,是存储在对象头里面的,一个Java对象在JVM内存中的布局由三个部分组成,分别是对象头、实例数据、对齐填充。而对象头里面有4个bit位来存储GC年龄
而4个bit位能够存储的最大数值是15,所以从这个角度来说,JVM分代年龄之所以设置成15次是因为它最大能够存储的数值就是15。
虽然JVM提供了参数来设置分代年龄的大小,但是这个大小不能超过15。
而从设计角度来看,当一个对象触发了最大值15次gc,还没有办法被回收,就只能移动到old
generation了。另外,设计者还引入了动态对象年龄判断的方式来决定把对象转移到old generation,也就是说不管这个对象的gc年龄是否达到了15次,只要满足动态年龄判断的依据,也同样会转移到old generation。
Java是如何实现的平台无关
Java 实现平台无关主要依赖于以下两个核心机制:
- Java 虚拟机(Java Virtual Machine,JVM):Java 源代码被编译成字节码(Bytecode),而不是直接编译成特定硬件平台的机器码。字节码是一种中间语言,它可以在任何支持相应 JVM 的平台上运行。每个平台都有自己的 JVM 实现,负责将字节码转化为具体平台的机器码执行。这样,Java 程序只需要编写一次,就可以在不同的操作系统和硬件平台上运行。
- Java 标准库(Java Standard Library):Java 提供了一个强大且完善的标准库,包含了大量的类和方法,用于处理各种常见的任务和功能。这些类库提供了丰富的 API 接口,使得开发人员能够方便地使用各种操作系统和硬件平台的功能,如文件操作、网络通信、图形界面等。通过使用标准库提供的接口,Java 程序可以在不同平台上执行相同的操作。
总结起来,Java 实现平台无关的关键在于字节码和 JVM。Java 通过将源代码编译成字节码,再由 JVM 在目标平台上解释执行或即时编译成本地机器码,从而实现了一次编写,到处运行的特性。而标准库提供了丰富的跨平台功能,使得开发人员能够轻松地编写可在不同平台上运行的代码。这种设计使得 Java 成为一种高度可移植和跨平台的编程语言。
JIT(Just-In-Time)编译器
JIT(Just-In-Time)编译器是一种动态编译技术,它在程序运行时将字节码直接编译成本地机器码,以提升程序的执行效率。JIT 编译器通常与解释器结合使用,可以在运行时根据实际的代码执行情况做出优化决策。
JIT(即时编译器)是一种在运行时将字节码直接编译成机器码的编译器。它是Java虚拟机(JVM)的一部分,用于提高Java程序的执行性能。
在Java中,源代码首先被编译成字节码,然后由JVM解释执行字节码。但是,解释执行字节码的速度相对较慢。为了提高性能,JIT编译器会动态地将频繁执行的字节码转换成本地机器码,以便更快地执行。
JIT编译器的工作原理是:当字节码被多次执行时,JIT编译器会监测这些热点代码(Hot Spot),并将其编译成机器码。一旦代码被编译成机器码,后续的执行将直接使用机器码,从而加快程序的执行速度。
JIT编译器的优势在于它能够根据实际的执行情况来进行优化,只编译那些频繁执行的代码,避免了对所有字节码都进行编译的开销。这种动态编译和优化的方式使得Java程序在运行时能够实现接近原生代码的性能。
需要注意的是,JIT编译器的工作可能需要一定时间,因此在应用程序刚启动时,性能可能会比较低。但随着时间的推移,JIT编译器会逐渐优化热点代码,提高程序的性能。
以下是 JIT 编译器的一些优化技术:
- 热点代码识别:JIT 编译器会监测程序中频繁执行的代码段,即所谓的热点代码。通过收集运行时的统计信息,JIT 编译器能够确定哪些代码是热点代码,并对其进行优化。
- 即时编译:JIT 编译器在运行时即时将热点代码编译成本地机器码,替代原始的解释执行方式。这样可以避免解释器的解释开销,提高代码的执行速度。
- 延迟编译:JIT 编译器并不会立即对所有的代码进行编译,而是根据需要进行延迟编译。它会收集足够的运行时信息来判断代码的热度,并决定是否值得对其进行编译优化。
- 内联优化:JIT 编译器会对频繁调用的方法进行内联优化。即将方法调用处的代码直接替换成被调用方法的实际代码,避免了方法调用的开销。
- 逃逸分析:JIT 编译器会进行逃逸分析,分析对象的生命周期和作用域,确定是否可以将对象分配在栈上而不是堆上,从而减少垃圾回收的压力。
- 数值优化:JIT 编译器会对数值计算进行优化,如常量折叠、循环展开、公共子表达式消除等,以减少运算的开销和提高执行效率。
JIT 编译器的优化技术可以根据具体的实现和编译器策略有所差异,但总体目标都是通过动态地将热点代码编译成本地机器码,以提高程序的执行性能和效率。这种即时编译的方式使得 Java 程序在运行时能够获得接近原生语言的执行速度。