JVM系统结构
1. JVM系统结构
类加载子系统
只负责加载字节码文件即class文件,至于能否运行则由执行引擎决定。
运行时数据区
线程私有:程序计数器、本地方法栈、虚拟机栈
线程共享:堆、方法区、常量池
执行引擎
解释字节码文件给操作系统,即将JVM指令集翻译为操作系统指令集
本地库接口、本地方法栈、本地方法库
本地方法栈:登记所有被native修饰的本地方法。
本地库接口:根据本地方法栈的本地方法生成对应的本地接口。
本地方法库:在执行时会根据本地接口去本地方法库中加载对应的实现方法。
JVM运行程序
1. Java文件编译(javac.exe)
解析与填充符号表
词法分析和语法分析
注解处理过程
在initPorcessAnnotations()初始化插入式注解处理器
在processAnnotations()处理注解
字节码生成
将前面各个步骤所生成的信息转化成字节码
上述3个处理过程里,执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号,从总体来看,三者之间的关系与交互顺序如图所示。
2. JVM装入
准备JVM
java.exe查找JRE并装入环境。
java.exe通过LoadJavaJVM来装入JVM文件。
java.exe通过CreateJavaVM初始化JVM。
加载java程序
如果运行jar的时候,java.exe调用GetMainClassName()获得主方法类。
如果直接加载class文件,则直接通过LoadClass()加载该主类。
3. Java类加载
加载
在方法区中生成类对应的Class对象。
验证
文件格式验证、元数据验证、字节码验证和符号引用验证。
准备
为类的静态变量分配内存并设置初始值。
解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
执行类构造器 < clinit >() 方法的过程,< clinit >() 是Javac编译器的自动生成物。
4. Java对象实例化
执行
类构造函数对应在字节码文件中的 < init >() 方法
5. Java程序怎么运行的
计算机要运行程序需要先将 class 文件加载到内存中,但是class文件是JVM定义的一套指令集规范,需要特定的命令解释器(执行引擎)将字节码翻译成特定的操作系统指令集交给 CPU 去执
行。
Java对象回收
GC如何判断对象可以被回收
- 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收
- 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象
可达性分析算法
- 可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会
- 当对象编程(GC Roots)不可达时,GC会判断该对象是否执行过finalize方法,若执行了则直接回收。否则,若对象未执行过finalized方法,将其放入F-Queue队列。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则对象复活。
四种JVM的垃圾回收算法
标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
堆可以分为新生代和老年代
1. 新生代
用来存放新生的对象,如果Eden区放不下新生对象,虚拟机发生一次Minor GC
- 第一次Minor GC后,Eden区还存活的对象 + “From”区还存活的对象,复制到Surviver区的“To”区,再清空Eden区和From区
- 第二次Minor GC时,“From”区和“To”区角色互换,Eden区还存活的对象 + “To”区还存活的对象,复制到Surviver区的“From”区,再清空Eden区和To区
2. 老年代
- 存放应用程序中生命周期长的内存对象。虚拟机会给每个对象定义一个对象年龄(Age)计数器,对象在Survivor区中每“熬过”一次GC,年龄就会+1。待到年龄到达一定岁数(默认是15岁),虚拟机就会将对象移动到年老代。
- Major GC采用标记整理算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM (Out of Memory)异常。
3. 新生代 + 老年代
- new新对象放入堆中
- 如果在Eden区没有空间,那就发生Minor GC,保留存活的对象
- 当新生代仍然没有空间,那就将部分年龄大的新生代对象放入老年代
- 当老年代仍然没有空间,那就发生Major GC,保留存活的对象
- Major GC之后仍然没有空间,就会抛出OOM
3. 方法区、永久代、元空间
《Java虚拟机规范》只是规定了有方法区
这么个概念,但是在不同的 JVM 上方法区的实现了不同的方法区
, 大多数用的JVM都是Sun公司的HotSpot。HotSpot使用永久代
来实现方法区。 对于Java8, 元空间
取代了永久代
。
4. 方法到底存储在方法区还是栈区?
方法编译出来的字节码存放在方法区中,但是方法的局部变量和参数变量存放在栈区(栈帧)
双亲委派模型
1. 三个主要的ClassLoader
BootStrapClassLoader
:在规定的sun.mic.boot.class
路径加载类ExtClassLoader
:在规定的java.ext.dirs
路径加载类AppClassLoader
:在规定的java.class.path
路径加载类
2. 双亲委派模型
AppClassLoader
查找类时,先看看缓存是否有,缓存有则从缓存中获取,否则委托给父加载器ExtClassLoader
ExtClassLoader
查找类时,先看看缓存是否有,缓存有则从缓存中获取,否则委托给父加载器BootStrapClassLoader
ExtClassLoader
查找类时,先看看缓存是否有,缓存有则从缓存中获取,否则sun.mic.boot.class
路径查找,查找成功则加载类,否则让子类BootStrapClassLoader
自己加载BootStrapClassLoader
在java.ext.dirs
路径查找,查找成功则加载类,否则让子类AppClassLoader
自己加载AppClassLoader
在java.class.path
路径查找,如果找到就加载类,否则就抛出异常。
JVM沙箱安全机制
省流:保证JVM内部不被加载的代码破坏,也保证JVM机器资源不被JVM里运行的程序破坏
1. 组件
类加载体系结构
双亲委派模型,上面已经提到了,这里不再赘述。
class文件校验器
1. 检查class文件的结构是否正确。比较典型的就是,检查class文件是否以OxCAFEBABE打头
2. 检查它是否符合java语言特性里的编译规则。比如发现一个类的超类不是Object就抛出异常。
3. 检查字节码是否能被JVM安全的执行,而不会导致JVM崩溃。
4. 符号引用验证。
内置于Java虚拟机(及语言)的安全特性
1. 结构化内存访问(不使用指针,一定程度上让黑客无法篡改内存数据)
2. 自动垃圾收集
3. 数组边界检查
4. 空引用检查
5. 数据类型安全
保证机器资源的安全
当前最新的安全机制实现,引入了域 (Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。
OOM分析与调优
1. 为什么是会发生OOM
内存泄漏
:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。因为申请者不用了,而又不能被虚拟机分配给别人用
内存溢出
:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出内存泄漏持续存在,最后一定会溢出,两者是因果关系
2. 常见的OOM
方法区溢出
- 报错信息:java.lang.OutOfMemoryError: PermGen space
- 常见问题:一般出现于大量Class对象,或者方法区过小
栈区溢出
- 报错信息:java.lang.StackOverflowError
- 常见问题:一般是由于程序中存在 死循环或者深度递归调用 造成的,或者栈区过小
堆区溢出
- 报错信息:java.lang.OutOfMemoryError: Java heap space
- 常见问题:内存泄漏、内存溢出
3. 分析OOM发生的原因
如果不是因为内存不足,而是因为JVM内部出现了其他“毛病”而导致的OOM,那就需要跟踪了。
主要使用dump文件
和jprofiler
dump文件
- 作用:dump文件是一个进程或者系统在某一个给定的时间的快照,dump文件中包含了程序运行的模块信息、线程信息、堆栈调用信息、异常信息等数据,在服务器运行我们的Java程序时,是无法跟踪代码的,所以当发生线上事故时,dump文件就成了一个很关键的分析点。
- 生成:其实在很多时候我们是不知道何时会发生OOM,所以需要在发生OOM时自动生成dump文件。其实很简单,只需要在启动时加上如下参数即可。HeapDumpPath表示生成dump文件保存的目录。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\tmp
。那么在服务器运行一个Java程序,总的命令就是nohup java -jar -Xms32M -Xmx32M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local user-0.0.1-SNAPSHOT.jar > log.file 2>&1 &
- 寻找:查看保存dump的目录,果然生成了对应的dump文件。
Jprofiler工具
- 作用:这里我介绍使用Jprofiler,有可视化界面,功能也比较完善,能够打开JVM工具(通过-XX:+HeapDumpOnOutOfMemoryError JVM参数触发)创建的hporf文件,即dump文件
- 打开dump文件
4. OOM的解决办法
如果因为内存大小不够而导致的OOM,那么可以在IDEA中设置对应区域的大小
# 栈
-Xss128k <==> -XX:ThreadStackSize=128k 设置每个线程栈的大小为128K
# 堆
-Xms2048m <==> -XX:InitialHeapSize=2048m 设置 JVM 初始堆内存为2048M
-Xmx2048m <==> -XX:MaxHeapSize=2048m 设置 JVM 最大堆内存为2048M
-Xmn2g <==> -XX:NewSize=2g -XX:MaxNewSize=2g 设置年轻代大小为2G,官方推荐配置为整堆的3/8
-XX:NewSize=2g 设置年轻代的初始值为 2g
-XX:MaxNewSize=2g 设置年轻代的最大值为 2g
-XX:SurvivorRatio=8 设置 Eden 区与 Survivor 区的比值,默认为 8
-XX:NewRatio=2 设置老年代与年轻代的比例,默认为 2
-XX:+UseAdaptiveSizePolicy 设置大小比例自适应,默认开启
-XX:PretenureSizeThreadshold=1024 设置让大于此阈值的对象直接分配在老年代,只对 Serial、ParNew 收集器有效
-XX:MaxTenuringThreshold=15 设置新生代晋升老年代的年龄限制,默认为 15
-XX:PrintTenuringDistribution 让 JVM 在每次 MinroGC 结束后打印出当前使用的 Survivor 中对象的年龄分布
-XX:TargetSurvivorRatio 设置 MinorGC 结束后 Survivor 区占用空间的期望比例
# 方法区
-XX:MetaspaceSize / -XX:PermSize=256m 设置元空间/永久代初始值为256M
-XX:MaxMetaspaceSize / -XX:MaxPermSize=256m 设置元空间/永久代最大值为256M
-XX:+UseCompressedOops 使用压缩对象指针
-XX:+UseCompressedClassPointers 使用压缩类指针
-XX:CompressedClassSpaceSize 设置 Klass Metaspace 的大小,默认1G
# 直接内存
-XX:MaxDirectMemorySize 指定 DirectMemory 容量,默认等于 Java 堆最大值
JVM调优
1. 性能调优
性能调优包含多个层次:架构调优、代码调优、JVM调优、数据库调优、操作系统调优。
大多数的Java应用不需要进行JVM优化,并且JVM优化是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。
2. JVM调优
何时进行JVM调优
- 老年代达到最大值,出现OOM内存异常
- Major GC次数频繁,导致GC停顿时间过长
- 系统吞吐量不高
调优的基本原则
JVM调优是一个手段,但并不一定所有问题都可以通过JVM进行调优解决。并且在所有优化手段中,JVM优化是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。
在大多数的情况下优先选择架构优化和代码优化。
JVM调优目标
其中,任何一个属性性能的提高,几乎都是以牺牲其他属性性能的损为代价的,不可兼得。
- 延迟:缩短或完全消除GC带来的停顿
- 低内存占用:令运行在虚拟机上的应用能够使用更少的内存
- 高吞吐量:令应用程序使用最小的硬件消耗来承载更大的吞吐
JVM调优的步骤
- 分析GC日志或者dump文件,判断是否需要优化
- 根据业务的性能需求,确定JVM(内存、延迟、吞吐量)的调优参数
- 依次堆内存、延迟、吞吐量进行调优
- 对比调优前后的差异,并不断调整参数
- 找到最合适的参数,将这些参数应用到所有服务器,并持续跟踪
JVM参数解析
# 栈
-Xss128k <==> -XX:ThreadStackSize=128k 设置每个线程栈的大小为128K
# 堆
-Xms2048m <==> -XX:InitialHeapSize=2048m 设置 JVM 初始堆内存为2048M
-Xmx2048m <==> -XX:MaxHeapSize=2048m 设置 JVM 最大堆内存为2048M
-Xmn2g <==> -XX:NewSize=2g -XX:MaxNewSize=2g 设置年轻代大小为2G,官方推荐配置为整堆的3/8
-XX:NewSize=2g 设置年轻代的初始值为 2g
-XX:MaxNewSize=2g 设置年轻代的最大值为 2g
-XX:SurvivorRatio=8 设置 Eden 区与 Survivor 区的比值,默认为 8
-XX:NewRatio=2 设置老年代与年轻代的比例,默认为 2
-XX:+UseAdaptiveSizePolicy 设置大小比例自适应,默认开启
-XX:PretenureSizeThreadshold=1024 设置让大于此阈值的对象直接分配在老年代,只对 Serial、ParNew 收集器有效
-XX:MaxTenuringThreshold=15 设置新生代晋升老年代的年龄限制,默认为 15
-XX:PrintTenuringDistribution 让 JVM 在每次 MinroGC 结束后打印出当前使用的 Survivor 中对象的年龄分布
-XX:TargetSurvivorRatio 设置 MinorGC 结束后 Survivor 区占用空间的期望比例
# 方法区
-XX:MetaspaceSize / -XX:PermSize=256m 设置元空间/永久代初始值为256M
-XX:MaxMetaspaceSize / -XX:MaxPermSize=256m 设置元空间/永久代最大值为256M
-XX:+UseCompressedOops 使用压缩对象指针
-XX:+UseCompressedClassPointers 使用压缩类指针
-XX:CompressedClassSpaceSize 设置 Klass Metaspace 的大小,默认1G
# 直接内存
-XX:MaxDirectMemorySize 指定 DirectMemory 容量,默认等于 Java 堆最大值
3. JVM调优示例
内存调优
1. 在GC日志中,找到发生Major GC的位置,找到老年代的空间占用。为了更加精确需要多次收集,计算出老年代的空间占用。
2. 设置参数-Xms和-Xmx,建议扩大至3-4倍Major GC后的老年代空间占用
- 新生代:1.2-1.5倍FullGc后的永久带空间占用。
- 老年代:2-3倍FullGC后的老年代空间占用。
- 永久代:1.2-1.5倍FullGc后的永久带空间占用。
延迟优化
1. 新生代空间越大,Minor GC的GC时间越长,频率越低。
2. 新生代空间越小,Minor GC的GC时间越短,频率越高。
吞吐量调优
通过对内存优化和延迟优化达到吞吐量优化的效果