JAVA虚拟机(JVM)
时间:2023-06-29 07:45:02 | 来源:网站运营
时间:2023-06-29 07:45:02 来源:网站运营
JAVA虚拟机(JVM):
一、JVM简介
JVM是
Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个
虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言
在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“
一次编译,到处运行”的原因。
二、JVM的生命周期
JVM实例对应了一个独立运行的java程序,它是进程级别。 1. 启动:启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
2. 运行:main()作为该程序初始线程的起点,任何其他线程均由该线程启动。
3. 消亡:当程序中的所有非守护线程都终止时,JVM才退出,若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。
JVM执行引擎实例则对应了属于用户运行程序的线程,它是线程级别。Java的线程分为两种:User Thread(用户线程)、DaemonThread(守护线程)。 只要当前JVM实例中尚存任何一个非守护(用户)线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束是,守护线程随着JVM一同结束工作,Daemon作用是为其他线程提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),他就是一个很称职的守护者。
守护线程与用户线程的
唯一区别是:其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开,当JVM中所有的线程都是守护线程的时候,JVM就可以退出了(如果User Thread全部撤离,那么Daemon Thread也就没什么线程好服务的了,所以虚拟机也就退出了);如果还有一个或以上的非守护线程则不会退出。(以上是针对正常退出,调用System.exit则必定会退出)。
三、JVM体系结构
1. 类装载器(ClassLoader)(用来装载.class文件)。
2. 执行引擎(执行字节码,或者执行本地方法)。
3. 运行时数据区(方法区、堆、Java栈、PC寄存器、本地方法栈)。
JVM内存结构JVM内存结构,主要包括两个子系统和两个组件——
两个子系统分别是Classloader子系统和Executionengine(执行引擎)子系统。
两个组件分别是RuntimeDataArea(运行时数据区域)组件和Nativeinterface(本地接口)组件。
1. Classloader子系统的作用——根据给定的全限定名类名(如java.lang.Object)来装载class文件的内容到Runtimedataarea中的methodarea(方法区域)。Java程序员可以extendsjava.lang.ClassLoader类来写自己的Classloader。
2. Executionengine子系统的作用——执行classes中的指令。任何JVMspecification实现(JVM)的核心都是Executionengine,不同的JDK,例如Sun的JDK和IBM的JDK好坏主要就取决于他们各自实现的Executionengine的好坏。
3. Nativeinterface组件——与nativelibraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的nativeheapOutOfMemory。
4.
RuntimeDataArea组件(主要分为五个部分):Heap(堆)、MethodArea(方法区域)、JavaStack(Java的栈)、ProgramCounter(程序计数器)、Nativemethodstack(本地方法栈)。
四、JVM运行时的数据区
JVM运行时的数据区 1.
Heap(Java堆)—— 被所有线程
共享的一块内存区域,在虚拟机启动时创建;
用来
存储对象实例;
可以通过-Xmx和-Xms控制堆的大小。
OutOfMemoryError异常:当在堆中没有内存完成实例分配,且堆也无法再扩展。
Java堆是垃圾收集器管理的主要区域。Java堆还可以细分为:新生代(New/Young)、旧生代/年老代(Old/Tenured)。
持久代(Permanent)在方法区,不属于Heap。Java堆的细分,新生代:旧生代=1:2、Eden : from : to = 8 : 1 : 1 新生代:新建的对象都由新生代分配内存。常常又被划分为Eden区和Survivor区。Eden空间不足时会把存活的对象转移到Survivor。
新生代的大小可由-Xmn控制,也可用-XX:SurvivorRatio控制Eden和Survivor的比例。
旧生代:存放经过多次垃圾回收仍然存活的对象。
持久代:存放静态文件,如今Java类、方法等。
持久代在方法区,对垃圾回收没有显著影响。
2.
方法区—— 线程间
共享;
用于
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数 据。
OutOfMemoryError异常:当方法区无法满足内存的分配需求时。
运行时常量池—— 方法区的一部分;
用于存放编译期生成的各种字面量与符号引用。
OutOfMemoryError异常:当常量池无法再申请到内存时。
3.
Java虚拟机栈(VMStack)—— 线程
私有,生命周期与线程相同;
存储方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等
信息;
Java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧,每一个方法被调
用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
StackOverflowError异常:当线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError异常:如果栈的扩展时无法申请到足够的内存。
JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量、部分的返回结果以及Stack Frame。其他引用类型的对象在JVM栈上仅存放变量名和指向堆上对象实例的首地址。
4.
本地方法栈(NativeMethodStack)—— 与虚拟机栈相似,主要为虚拟机使用到的Native方法服务,在HotSpot虚拟机中直接
把本地方法栈与虚拟机栈二合一。
5.
程序计数器(Program Counter Register)—— 当前线程所执行的字节码的行号指示器;
当前线程
私有;
不会出现OutOfMemoryError情况。
6.
直接内存(Direct Memory)—— 直接内存并不是虚拟机运行的一部分,也不是Java虚拟机规范中定义的内存区域,但
是这部分内存也
被频繁使用;
NIO可以使用Native函数库直接分配堆外内存,堆中的DirectByteBuffer对象作为这
块内存的引用进行操作;
大小不受Java堆大小的限制,受本机(服务器)内存限制。
OutOfMemoryError异常:系统内存不足时。
7.
总结—— Java对象实例存放在堆中;常量存放在方法区的常量池;虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据放在方法区;以上区域是所有线程共享的。栈是线程私有的,存放该方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。 一个Java程序对应一个JVM,一个方法(线程)对应一个Java栈。
五、Java代码的编译和执行过程
1.
Java代码的编译和执行包括了三个重要机制—— Java源码编译机制(.java源代码文件 -> .class字节码文件);
类加载机制(ClassLoader);
类执行机制(JVM执行引擎)。
2.
Java源码编译机制—— Java源代码是不能被机器识别的,需要先经过编译器编译成JVM可以执行的.class字
节码文件,再由解释器解释运行。即:Java源文件(.java)-- Java编译器 --> Java字节
码文件(.class)-- Java解释器--> 执行——
Java源码编译机制 字节码文件(.class)是平台无关的。
Java中字符只以一种形式存在:Unicode。字符转换发生在JVM和OS交界处 (Reader/Writer)。
最后生成的class文件由以下部分组成——
结构信息。包括class文件格式版本号及各部分的数量与大小的信息;
元数据。对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声
明信息、域与方法声明信息和常量池;
方法信息。对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、
求值栈与局部变量区大小、求值栈的类型记录、调试符号信息。
3.
类加载机制(ClassLoader)—— Java程序并不一个可执行文件,是由多个独立的类文件组成。这些类文件并非一次性全部装入内存,而是依据程序
逐步载入。JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序——
类加载机制 (1)Bootstrap ClassLoader——
JVM的根ClassLoader,由C++实现;
加载Java的核心API:$JAVA_HOME中jre/lib/rt.jar中所有class文件的加载,这个
jar中包含了java规范定义的所有接口以及实现;
JVM启动时即初始化此ClassLoader。
(2)Extension ClassLoader——
加载Java扩展API(lib/ext中的类)。
(3)App ClassLoader——
加载Classpath目录下定义的class。
(4)Custom ClassLoader——
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据J2EE
规范自行实现ClassLoader。
双亲委派机制 JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归。如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
作用:避免重复加载;更安全。如果不是双亲委派,那么用户在自己的classpath编写了一个java.lang.Object的类,那就无法保证Object的唯一性。所以使用双亲委派,即使自己编写了,但是永远都不会被加载运行。
破坏双亲委派机制 双亲委派机制并
不是一种强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。
线程上下文类加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器就是应用程序类加载器。像JDBC就是采用了这种方式。这种行为就是逆向使用了加载器,违背了双亲委派模型的一般性原则。
4.
类执行机制—— 在执行方法时JVM提供了四种指令来执行:
(1)invokestatic:调用类的static方法;
(2)invokevirtual:调用对象实例的方法;
(3)invokeinterface:将属性定义为接口来进行调用;
(4)invokespecial:JVM对于初始化对象(Java构造器的方法为:<init>)
以及调用对象实例中的私有方法时。
Java字节码的执行是由JVM执行引擎来完成——
JVM执行引擎 JVM是基于栈的体系结构来执行class字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。
主要的执行技术有:解释,即时编译,自适应优化、芯片级直接执行(
解释属于第一代JVM,
即时编译JIT属于第二代JVM,
自适应优化,Sun的HotspotJVM采用这种技术,吸取第一代JVM和第二代JVM的经
验,采用两者结合的方式。),
开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
六、JVM垃圾回收(GC)
1.
GC的基本原理:将内存中不再被引用的对象进行回收,GC中用于回收的方法称为收集器。
垃圾:不再被引用的对象。
由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
对新生代的对象的收集称为minor GC;
对旧生代的对象的收集称为Full GC;
程序中主动调用System.gc()的GC为Full GC。
Java垃圾回收是单独的后台线程gc执行的,自动运行无需显示调用。
即使主动调用了java.lang.System.gc(),该方法也只会提醒系统进行垃圾回收,但系统不一定会回应,可能会不予理睬。
2.
判断一块内存空间是否符合回收标准:对象赋予了空值,且之后再未调用(obj = null;);对象赋予了新值,即重新分配了内存空间(obj = new Obj();)。
3.
内存泄漏:程序中保留着对永远不再使用的对象的引用。因此这些对象不回被GC回收,却一直占用内存空间却毫无用处。即
对象是可达的;
对象是无用的。
满足这两个条件即可判定为内存泄漏。
应确保不需要的对象不可达,通常采用将对象字段设置为null的方式,或从容器collection中移除对象。局部变量不再使用时无需显示设置为null,因为对局部变量的引用会随着方法的退出而自动清除。
内存泄露的原因:全局集合;缓存;ClassLoader。
4.
不同的对象引用类型,GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型——
(1)
强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象
引用,GC时才会被回收)。
(2)
软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存
不够用的情况下才会被GC)。
(3)
弱引用:在GC时一定会被GC回收。
(4)
虚引用:由于虚引用只是用来得知对象是否被GC。
七、内存调优
1.
调优目的:
减少GC的频率尤其是Full GC的次数,过多的GC会占用很多系统资源影响吞吐量。特别要关注Full GC,因为它会对整个堆进行整理。
2.
主要手段:JVM调优主要通过配置JVM的参数来提高垃圾回收的速度,合理分配堆内存各部分的比例。
3.
导致Full GC的几种情况和调优策略—— (1)
旧生代空间不足:调优时尽量让对象在新生代GC时被回收、让对象在新生代多
存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象。
(2)
持久代空间不足:增大PermGen空间,避免太多静态对象。
(3)
统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间:控制好新生代
和旧生代的比例。
(4)
System.gc()被显示调用:垃圾回收不要手动触发,尽量依靠JVM自身的机
制。
4.
堆内存比例不良设置会导致什么后果——
新生代设置过小:一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直
接进入旧生代,占据了旧生代剩余空间,诱发Full GC。
新生代设置过大:一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发
Full GC;二是新生代GC耗时大幅度增加。
一般说来新生代占整个堆1/3比较合适。Survivor设置过小:导致对象从eden直接到达旧生代,降低了在新生代的存活时
间。
Survivor设置过大:导致eden过小,增加了GC频率。
5.
JVM提供两种较为简单的GC策略的设置方式—— 吞吐量优先:JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代
的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置。
暂停时间优先:JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧
生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这
个值可由-XX:MaxGCPauseRatio=n来设置。
八、JVM常见配置
1.
堆设置—— -Xms:初始堆大小,
-Xmx:最大堆大小,
-XX:NewSize=n:设置年轻代大小,
-XX:NewRatio=n:设置年轻代和年老代的比值,
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值,
-XX:MaxPermSize=n:设置持久代大小。
2.
收集器设置—— -XX:+UseSerialGC:设置串行收集器,
-XX:+UseParallelGC:设置并行收集器,
-XX:+UseParalledlOldGC:设置并行年老代收集器,
-XX:+UseConcMarkSweepGC:设置并发收集器。
3.
垃圾回收统计信息—— -XX:+PrintGC,
-XX:+PrintGCDetails,
-XX:+PrintGCTimeStamps,
-Xloggc:filename。
4.
并行收集器设置—— -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程
数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间。
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为
1/(1+n)。
5.
并发收集器设置—— -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的
CPU数。并行收集线程数。