Java虚拟机
2021-03-29 16:23:06 50 举报
AI智能生成
登录查看完整内容
Java虚拟机(JVM)是Java技术的核心组成部分,它是一个虚拟的计算机,负责执行Java字节码指令。JVM将Java源代码编译成平台无关的字节码,然后在各种操作系统和硬件平台上运行这些字节码。这使得Java程序具有跨平台性,即一次编写,到处运行。JVM还提供了一个运行时环境,用于管理内存、垃圾回收和多线程等资源。此外,JVM还包含了一系列工具和库,如Java标准类库(JCL)和Java安全机制,以支持开发人员构建高质量的Java应用程序。总之,Java虚拟机是实现Java“一次编写,到处运行”的关键组件,它为Java程序提供了稳定、高效的运行环境。
作者其他创作
大纲/内容
当Eden区分配没有足够的空间进行分配时,虚拟机将会发起一次Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
1.对象优先在 Eden 区分配
2.大对象直接进入老年代
3.长期存活对象将进入老年代
内存分配策略
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,准备、解析和初始化,最终形成可以被虚拟机直接使用的java类型。
new一个对象时,隐式调用类装在器,加载类到JVM中
隐式装载
通过Class.forname显示装在类到JVM中
显示装载
为了节省内存开销,Java类加载是动态的,不会一次性把所有类加载之后再运行,而是保证程序运行的基础类完全加载到JVM中,其他类需要的时候再加载
类装载方式
加载java核心类库,无法被java程序直接引用;用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
启动类加载器
加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录;java扩展库,加载\\lib\\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
扩展类加载器
根据java应用classpath来加载,java的应用类都是由它加载完成
应用类加载器
通过继承 java.lang.ClassLoader类的方式实现。
其他加载器
类加载器
当一个类加载器加载一个类时,该类所依赖的类也都全部由他加载
全盘负责
先让父类加载,只有当父类无法加载时才从自己的类路径中加载该类
父类委托
保证所有加载过的类都被缓存;当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
缓存机制
类加载原则与机制
1.通过一个类的全限定名来获取其定义的二进制字节流;2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;3.在java堆中生成代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口
根据查找路径找到相应的 class 文件然后导入;
1.装载
验证字节流是否符合Class文件格式的规范
文件格式验证
对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了Java.lang.Object之外。
元数据验证
通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
字节码验证
确保解析动作能正确执行
符号引用验证
检查加载的 class 文件的正确性;
2.检验
如果字段同时被final和static修饰,那么在准备阶段变量value就会被初始化为其制定的值
给类中的静态变量分配内存空间,并将其初始化为默认值(初始化为具体的值在初始化阶段)
3.准备
虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标识,而在直接引用直接指向内存中的地址;
4.解析
对静态变量和静态代码块执行初始化工作
5.初始化
执行过程
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。
类加载
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
为了防止内存中出现多个相同的字节码;因为如果没有双亲委派的话,用户就可以自己定义一个java.lang.String类,那么就无法保证类的唯一性;
自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法;
打破双亲委派模型
防止内存中出现多份一样的代码
保证java程序安全运行
意义
双亲委派模型
虚拟机加载机制
用于对 JVM 中的内存、线程和类等进行监控;
jconsole
JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
jvisualvm
子主题
调优工具
-Xms2g:初始化堆大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
常用的 JVM 调优的参数
CPU load过高,导致系统不可用或tps急剧降低
Young GC 次数过多
Oracle preparestatment内存预分配占用过大
Local Cache 内容过大
Full GC次数过多
Full GC 时间长
Perm space回收频繁或者OOM
jvm问题导致tps降低
重复查询泛滥
锁竞争或死锁
连接池连接占用
不合理的使用集合
常见性能问题分析
让可以并行的方法并行
避免重复查询
减少远程交互次数
批量操作
local cache
设置合理的Young Gen大小
减少Full GC 时间
设置合理线程数
减少模板的大小
内部实现层面上的优化
集中式数据Cache
Page Cache
片段 Cache
架构层面优化
找到性能瓶颈(工具)
性能问题分析
JVM调优
垃圾回收时调用
finalize()
析构方法,只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。
finalization()
finalize()/finalization()
元空间
类加载子系统
存储被虚拟机加载的类信息,常量,静态变量及及时编译的代码等数据
方法区
存储局部变量表,操作数栈,动态链接,方法出口等
虚拟机栈
与虚拟机栈类似,不过虚拟机栈服务java方法,本地方法栈服务native方法服务
本地方法栈
被所有线程共享,几乎所有对象实例都在这里分配内存
堆
当前线程执行字节码的行号指示器字节码解析器的工作就是通过改变程序计数器的值,来获取下一条要执行的命令
程序计数器
运行时数据区(JVM内存)
执行引擎
本地库接口
内存区域
new(调用构造方法)
Class类的newInstance(调用构造方法)
反射:Constructor的newInstance(调用构造方法)
使用clone
反序列化
对象的创建方式
1.类加载:检查常量池是否已加载类,如果没有则先加载类
堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离
指针碰撞:内存绝对规整使用“指针碰撞”分配内存
堆的内存不是规整的,则需要由虚拟机维护一个列表来记录哪些内存是可用的,分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。
空闲列表:内存不规整,从空闲列表分配
两种分配方式
堆的规整与否主要取决于垃圾回收是否带有压缩回收功能决定
2.分配内存
CAS失败重试
每个线程在java堆中预先分配一小块内存
本地线程分配缓存
3.划分内存时的并发问题
4.内存空间初始化操作,初始化内存空间为零,做一些必要的初始化操作(元信息,哈希码)
5.执行初始化方法
对象创建流程
Java 程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现
句柄指向对象的指针,对象的指针指向对象的地址
堆中划分一块作为句柄池
句柄中存放对象类型和对象地址
相对慢
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改
句柄
指向对象在内存中的直接地址
java堆中对象内部需维护对象数据类型
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
直接指针
两种方式
对象的访问定位
hotsop虚拟机
长生命周期对象持短生命周期对象
内存溢出
虚拟机空间或内存不足时触发
1.System.gc()是建议垃圾回收,并不一定马上就回收。且触发的是major垃圾回收
2.当永久代满了或到达临界值会触发完全垃圾回收,永久代也会被回收
元数据区
3.java8移除了永久代,增加了一个元数据区的navtive内存区
4.程序员不能实时的对某个对象或所有对象调用垃圾回收器进行回收
垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收。
5.垃圾回收不会发生在永生代(Java虚拟机规范中不要求对方法区进行垃圾收集)
正确的永生代大小,对避免完全垃圾回收很重要
6.如果永生代满了或者超过临界值,则会触发完全垃圾回收,此时永生代也会被回收(仅限HotPot虚拟机等实现了永生带垃圾回收的虚拟机)
要点
采用有向图的方式记录和管理堆中的对象,通过这种方式分析对象的可达性,当确定一些对象不可达时,GC就有职责回收这些垃圾
原理
不会被回收
Java引用类型之——强引用
强引用
在发生内存泄漏前回收
Java引用类型之——软引用
软引用SoftReference
在下一个垃圾回收时回收
Java引用类型之——弱引用
弱引用
无法通过虚引用获取对象,虚引用的用途在回收时返回一个通知
Java引用类型之——虚引用
虚引用
对象引用类型
循环引用无法解决
引用计数器
虚拟机栈中引用的对象
方法区静态属性引用的变量
JNI 句柄,包括 JNI Local Handles 和 JNI Global Handles。
本地方法栈JNI引用的对象
在类中定义常量与静态变量,在方法中定义局部变量,这些都是堆中对象的起点
当前活跃线程的栈桢里指向堆中对象的引用,即当前所有正在被调用方法的引用类型参数、局部变量等
类的引用类型静态变量,这里指的是引用类型,像 int 等基本数据类型的静态变量肯定不能作为 GC Roots
当前所有已被加载的Java类和类加载器
在方法区中常量引用的对象,譬如字符串常量池 ( String Table ) 里的引用
所有被同步锁 ( synchronized关键字 ) 持有的对象引用。
可以作为GC Roots的对象
1.标记所有不可达对象,并进行筛选,筛选的标准是该对象覆盖了finalize()方法且finalize()方法没有被虚拟机调用过,选出的对象将被放置在一个“即将被回收”的队列中。稍后虚拟机会创建一个低优先级的Finalizer线程去遍历队列中的所有对象并执行finalize()方法
2.对队列中的对象进行第二次标记,如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联,那么这个对象将被移除队列,而还留在队列中的对象,就会被回收了
满足上述条件的时候,不会马上被回收,它还可以自救,一个对象真正的死亡至少要进行两次标记
GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
在类中定义常量与静态变量,在方法中定义局部变量,这些都是堆中对象的起点。那么在垃圾回收时,这些引用自然而然就成为了 Tracing GC 的起点
所有无须跟踪引用就可以得到的对象
可达性分析(主流)
判断回收方法
标记无用对象,然后进行清除回收
1.标记可回收
2.清除可回收
两个阶段
实现简单,不需要对象移动
优点
标记清楚效率低,产生大量碎片内存,增加垃圾回收频率
缺点
是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS(Concurrent Mark Sweep)收集器
收集器
标记回收法
按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉
按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
Serial收集器
ParNew收集器
Parallel Scavenge收集器
G1(Garbage First)收集器(整堆回收器)
复制清除法
标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存
解决了标记-清理算法存在的内存碎片问题
仍需要进行局部对象移动,一定程度上降低了效率
Serial Old收集器
Parallel Old收集器
标记整理法
根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法
Eden(8/10)
From Survivor(1/10)
To Survivor(1/10)
基本采用复制算法
新生代(1/3)
采用标记整理算法,标记清除法
老年代(2/3)
回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。加载该类的ClassLoader已经被回收。该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading查看类的加载和卸载信息。在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类
永生代(方法区)
分代
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,
把Eden、From Survivor存活的对象发去To Survivor分区清空Eden、From Survivor分区From Survivor分区与To Survivor分区交换
1.新生代复制算法
在1.5.0_05之前最大值可以设置为31 ,1.5.0_06以后最大值可以设置为15,超过15会被认为无限大。
2.每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
3.老年代空间到达某个值的时候就会触发全局垃圾回收
执行流程
具体描述
如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。
对象年龄计数规则
分代算法
垃圾回收算法
新生代单线程收集器,标记和清理都是单线程,优点是简单高效
新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩
-XX:+UseSerialGC
Serial(复制算法)
Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现
新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
-XX:+UseParNewGC ParNew收集器-XX:ParallelGCThreads 限制线程数量
PraNew(复制算法)
追求高吞吐量,高效利用 CPU
吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务
适合后台应用等对交互响应要求不高的场景
和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量
使用Parallel收集器+ 老年代串行;新生代复制算法、老年代标记-压缩
-XX:+UseParallelGC
Parallel Scavenge(复制算法)
新生代收集器
单线程收集器,Serial收集器的老年代版本
Serial Old(标记-整理算法)
并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本
使用Parallel收集器+ 老年代并行
-XX:+UseParallelOldGC
Parallel Old(标记-整理算法)
并行收集器,以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器,追求最短GC回收停顿时间。
对于要求服务器响应速度的应用上,这种垃圾回收器非常适合
收集结束会产生大量空间碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低
新生代使用ParNew,老年代CMS
-XX:+UseConcMarkSweepGC 使用CMS收集器-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
标记GC roots能直接关联到的对象
初始标记(STW)
并发标记
重新标记(STW)
并发清除
步骤
CMS(Concurrent Mark-Sweep标记-清除算法)
老年代收集器
Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,不会产生内存碎片。
不会产生空间碎片,可以精确地控制停顿;
回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率;
G1收集器基于“标记-整理”算法实现,不会产生内存碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
特点
使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
回收整个java堆(标记-整理算法)
G1收集器
分布式垃圾回收(DGC)
Jvm 系列(三):GC 算法 垃圾收集器
垃圾收集器
可以搭配使用的垃圾回收器
垃圾收集器组合策略
垃圾回收
发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所以 Minor GC 非常频繁,一般回收速度也非常快
Minor Gc
是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上
Major GC(Full GC)
Minor GCMajor GC(Full GC)
物理地址分配对象不连续,性能较慢
主要存放对象实例和数组,关注数据结构
整个应用程序共享可见
运行期确认大小
使用数据结构中的栈,先近后出,物理地址分配连续,性能较快
主要存放局部变量,操作数栈,返回结果,关注程序执行
线程私有,当前线程可见,生命周期同线程
编译期确认大小
栈
堆栈区别
1.IDEA开发java源文件
2.编译器将Java源文件编译为字节码文件
3.java虚拟机类加载器,加载字节码文件到运行时数据区的方法区类,并在堆上创建一个java.lang.Class对象用来封装类在方法区的数据结构
4.执行引擎把字节码文件翻译成系统命令,再交由cpu执行这个过程会调用其他语言的本地接口
java程序运行机制
软引用和弱引用
参考资料资料
JVM系列
Java虚拟机
0 条评论
回复 删除
下一页