1. JVM 生命周期
- 启动。启动一个 Java 程序时,一个 JVM 实例就产生了,任何一个拥有 public static void main(String[] args)函数的 class 都可以作为 JVM 实例运行的起点。
- 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。
- 消亡。当程序中的所有非守护线程都终止时,JVM 才退出;若安全管理器允许,程序也可以使用 Runtime 类或者 System.exit()来退出。
一个运行中的 Java 虚拟机有着一个清晰的任务:执行 Java 程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的 Java 虚拟机。 Java 虚拟机总是开始于一个 main()方法,这个方法必须是公有、返回 void、直接受一个字符串数组。在程序执行时,你必须给 Java 虚拟机指明这个包换 main()方法的类名。main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。
Java 中的线程分为两种:守护线程 (daemon)和普通线程(non-daemon)。守护线程是 Java 虚拟机自己使用的线程,比如负责垃圾收集的线程就是一个守护线程。当然,你也可以把自己的程序设置为守护线程。包含 main()方法的初始线程不是守护线程。
只要 Java 虚拟机中还有普通的线程在执行,Java 虚拟机就不会停止。如果有足够的权限,你可以调用 exit()方法终止程序。
2. JVM 体系结构
1) 类装载器(ClassLoader)(用来装载.class 文件)
2) 执行引擎(执行字节码,或者执行本地方法)
3) 运行时数据区(方法区、堆、java 栈、PC 寄存器、本地方法栈)
3. JVM 运行时数据区
3.1 Java 堆(Heap)
- 被所有线程共享的一块内存区域,在虚拟机启动时创建
- 用来存储对象实例
- 可以通过-Xmx 和-Xms 控制堆的大小
- OutOfMemoryError 异常:当在堆中没有内存完成实例分配,且堆也无法再扩展时。
java 堆是垃圾收集器管理的主要区域。java 堆还可以细分为:新生代(New/Young)、旧生代/年老代(Old/Tenured)。持久代(Permanent)在方法区,不属于 Heap。
**新生代:**新建的对象都由新生代分配内存。常常又被划分为 Eden 区和 Survivor 区。Eden 空间不足时会把存活的对象转移到 Survivor。新生代的大小可由-Xmn 控制,也可用-XX:SurvivorRatio 控制 Eden 和 Survivor 的比例。
**旧生代:**存放经过多次垃圾回收仍然存活的对象。
持久代:存放静态文件,如今 Java 类、方法等。持久代在方法区,对垃圾回收没有显著影响。
3.2 方法区
-
线程间共享
-
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
-
OutOfMemoryError 异常:当方法区无法满足内存的分配需求时
-
运行时常量池
- 方法区的一部分
- 用于存放编译期生成的各种字面量与符号引用,如 String 类型常量就存放在常量池
- OutOfMemoryError 异常:当常量池无法再申请到内存时
3.3 java 虚拟机栈(VM Stack)
- 线程私有,生命周期与线程相同
- 存储方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。
- java 方法执行的内存模型,每个方法执行的同时都会创建一个栈帧,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- StackOverflowError 异常:当线程请求的栈深度大于虚拟机所允许的深度
- OutOfMemoryError 异常:如果栈的扩展时无法申请到足够的内存
JVM 栈是线程私有的,每个线程创建的同时都会创建 JVM 栈,JVM 栈中存放的为当前线程中局部基本类型的变量、部分的返回结果以及 Stack Frame。其他引用类型的对象在 JVM 栈上仅存放变量名和指向堆上对象实例的首地址。
3.4 本地方法栈(Native Method Stack)
- 与虚拟机栈相似,主要为虚拟机使用到的 Native 方法服务,在 HotSpot 虚拟机中直接把本地方法栈与虚拟机栈二合一
3.5 程序计数器(Program Counter Register)
- 当前线程所执行的字节码的行号指示器
- 当前线程私有
- 不会出现 OutOfMemoryError 情况
3.6 直接内存(Direct Memory)
- 直接内存并不是虚拟机运行的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用
- NIO 可以使用 Native 函数库直接分配堆外内存,堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作
- 大小不受 Java 堆大小的限制,受本机(服务器)内存限制
- OutOfMemoryError 异常:系统内存不足时
**总结:**Java 对象实例存放在堆中;常量存放在方法区的常量池;虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据放在方法区;以上区域是所有线程共享的。栈是线程私有的,存放该方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。
一个 Java 程序对应一个 JVM,一个方法(线程)对应一个 Java 栈。
4. Java 代码的编译和执行过程
Java 代码的编译和执行包括了三个重要机制:
(1)Java 源码编译机制(.java 源代码文件 -> .class 字节码文件)
(2)类加载机制(ClassLoader)
(3)类执行机制(JVM 执行引擎)
4.1 Java 源码编译机制
Java 源代码是不能被机器识别的,需要先经过编译器编译成 JVM 可以执行的.class 字节码文件,再由解释器解释运行。即:Java 源文件(.java) -- Java 编译器 --> Java 字节码文件 (.class) -- Java 解释器 --> 执行。流程图如下:
字节码文件(.class)是平台无关的。
Java 中字符只以一种形式存在:Unicode。字符转换发生在 JVM 和 OS 交界处(Reader/Writer)。
最后生成的 class 文件由以下部分组成:
- 结构信息。包括 class 文件格式版本号及各部分的数量与大小的信息
- 元数据。对应于 Java 源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
- 方法信息。对应 Java 源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息
4.2 类加载机制(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
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLoader 逐层检查,只要某个 classloader 已加载就视为已加载此类,保证此类只所有 ClassLoader 加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
双亲委派机制
JVM 在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归。如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
作用:1)避免重复加载;2)更安全。如果不是双亲委派,那么用户在自己的 classpath 编写了一个 java.lang.Object 的类,那就无法保证 Object 的唯一性。所以使用双亲委派,即使自己编写了,但是永远都不会被加载运行。
破坏双亲委派机制
双亲委派机制并不是一种强制性的约束模型,而是 Java 设计者推荐给开发者的类加载器实现方式。
** 线程上下文类加载器**,这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器就是应用程序类加载器。像 JDBC 就是采用了这种方式。这种行为就是逆向使用了加载器,违背了双亲委派模型的一般性原则。
4.3 类执行机制
Java 字节码的执行是由 JVM 执行引擎来完成,流程图如下所示:
JVM 是基于栈的体系结构来执行 class 字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。
主要的执行技术:解释,即时编译,自适应优化、芯片级直接执行
- 解释属于第一代 JVM,
- 即时编译 JIT 属于第二代 JVM,
- 自适应优化(目前 Sun 的 HotspotJVM 采用这种技术)则吸取第一代 JVM 和第二代 JVM 的经验,采用两者结合的方式
开始对所有的代码都采取解释执行的方式,并监视代码执行情况。对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行。
5. JVM 垃圾回收(GC)
**GC 的基本原理:**将内存中不再被引用的对象进行回收,GC 中用于回收的方法称为收集器。垃圾:不再被引用的对象。
由于 GC 需要消耗一些资源和时间,Java 在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短 GC 对应用造成的暂停。
- 对新生代的对象的收集称为 minor GC;
- 对旧生代的对象的收集称为 Full GC;
- 程序中主动调用 System.gc()的 GC 为 Full GC。
Java 垃圾回收是单独的后台线程 gc 执行的,自动运行无需显示调用。即使主动调用了 java.lang.System.gc(),该方法也只会提醒系统进行垃圾回收,但系统不一定会回应,可能会不予理睬。
判断一块内存空间是否符合回收标准:
(1)对象赋予了空值,且之后再未调用(obj = null;)
(2)对象赋予了新值,即重新分配了内存空间(obj = new Obj();)
**内存泄漏:**程序中保留着对永远不再使用的对象的引用。因此这些对象不回被 GC 回收,却一直占用内存空间却毫无用处。即:1)对象是可达的;2)对象是无用的。满足这两个条件即可判定为内存泄漏。
应确保不需要的对象不可达,通常采用将对象字段设置为 null 的方式,或从容器 collection 中移除对象。局部变量不再使用时无需显示设置为 null,因为对局部变量的引用会随着方法的退出而自动清除。
**内存泄露的原因:**1)全局集合;2)缓存;3)ClassLoader
6. 内存调优
**调优目的:**减少 GC 的频率尤其是 Full GC 的次数,过多的 GC 会占用很多系统资源影响吞吐量。特别要关注 Full GC,因为它会对整个堆进行整理。
**主要手段:**JVM 调优主要通过配置 JVM 的参数来提高垃圾回收的速度,合理分配堆内存各部分的比例。
导致 Full GC 的几种情况和调优策略:
- 旧生代空间不足
调优时尽量让对象在新生代 GC 时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 - 持久代(Pemanet Generation)空间不足
增大 Perm Gen 空间,**避免太多静态对象 ** - 统计得到的 GC 后晋升到旧生代的平均大小大于旧生代剩余空间
**控制好新生代和旧生代的比例 ** - System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠 JVM 自身的机制
堆内存比例不良设置会导致什么后果:
1)新生代设置过小
一是新生代 GC 次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发 Full GC
2)新生代设置过大
一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发 Full GC;二是新生代 GC 耗时大幅度增加
一般说来新生代占整个堆 1/3 比较合适
3)Survivor 设置过小
导致对象从 eden 直接到达旧生代,降低了在新生代的存活时间
4)Survivor 设置过大
导致 eden 过小,增加了 GC 频率
另外,通过-XX:MaxTenuringThreshold=n 来控制新生代存活时间,尽量让对象在新生代被回收
JVM 提供两种较为简单的 GC 策略的设置方式:
1)吞吐量优先
JVM 以吞吐量为指标,自行选择相应的 GC 策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n 来设置
2)暂停时间优先
JVM 以暂停时间为指标,自行选择相应的 GC 策略及控制新生代与旧生代的大小比例,尽量保证每次 GC 造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n 来设置
JVM 常见配置
- 堆设置
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -XX:NewSize=n:设置年轻代大小
- -XX:NewRatio=n:设置年轻代和年老代的比值。如:为 3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1/4
- -XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如:3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5
- -XX:MaxPermSize=n:设置持久代大小
- 收集器设置
- -XX:+UseSerialGC:设置串行收集器
- -XX:+UseParallelGC:设置并行收集器
- -XX:+UseParalledlOldGC:设置并行年老代收集器
- -XX:+UseConcMarkSweepGC:设置并发收集器
- 垃圾回收统计信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
- 并行收集器设置
- -XX:ParallelGCThreads=n:设置并行收集器收集时使用的 CPU 数。并行收集线程数。
- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n)
- 并发收集器设置
- -XX:+CMSIncrementalMode:设置为增量模式。适用于单 CPU 情况。
- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的 CPU 数。并行收集线程数。
参考链接:
Jvm工作原理学习笔记 - Java开发 - 开发语言与工具 - 深度开源
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于