深入理解java虚拟机-jvm基础知识总结
jvm的学习对于java程序员来说还是很重要的, 我在学习了jvm相关知识后, 对java虚拟机有所深入理解,整理了笔记并分享内容有 : java虚拟机种类, Java内存区域, 垃圾回收, 内存分代管理, 内存分配策略, 虚拟机工具, Class文件结构, 类加载java虚拟机Sun Classic VM : SUN公司第一款java虚拟机(落后,
·
jvm的学习对于java程序员来说还是很重要的,作为程序员,应该学习了解底层的东西,而不是停留在代码层面。我在简单学习了一下jvm相关的知识后,整理以下笔记,便于以后复习回顾。
- 内容有 : java虚拟机种类, Java内存区域, 垃圾回收, 内存分代管理, 内存分配策略, 虚拟机工具, Class文件结构, 类加载
java虚拟机
- Sun Classic VM : SUN公司第一款java虚拟机(落后, 淘汰)
- 世界上第一款商用的java虚拟机
- 只能使用纯解释器的方式来执行java代码
- Exact VM : Exact Memory Management
- 编译期和解释器混合工作以及两级即时编译期
- 只在Solaris平台发布
- 很快就被取代
- HotSpot VM
- 继承以上两款vm的优势
- 目前jdk官方jvm
- KVM : Kilobyte
- 简单, 轻量 ,高度可移植
- 在手机平台运行
- JRockit : BEA公司(被oracle收购)
- 世界上最快的Java虚拟机
- 专注服务器端应用
- 优势
- 垃圾收集器
- MissionControl服务套件
- J9 : IBM
- Dalvik
- Dalvik是Google公司设计用于Android平台的虚拟机(android5后换成ART)
- .dex 格式
- Dalvik 基于寄存器,而 JVM 基于栈。
- Microsoft JVM (被sun公司控告后, 停止使用)
- Azul VM; Liquid VM
- 高性能的java虚拟机
- Taobao VM
- 通过修改大量的HotSpot源代码深度定制的
Java内存区域
▶ 程序计数器
- 内存中比较小的一块内存空间, 它可以看做是当前线程所执行的字节码的行号指示器
- 程序计数器处于线程独占区
- 如果线程执行的Java方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址, 如果正在执行的是native方法, 这个计数器的值为undefined
- 此区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
▶ 虚拟机栈
- 虚拟机栈描述的是Java方法执行的动态内存模型
- 栈帧
- 每个方法执行, 都会创建一个栈帧, 伴随着方法从创建到执行完成
- 用于存储局部变量表, 操作数栈, 动态链接, 方法出口等
- 局部变量表
- 存放编译期可知的各种基本数据类型, 引用类型, returnAddress类型
- 局部变量表的内存空间在编译期完成分配, 当进入一个方法时, 这个方法需要在帧分配多少内存是固定的, 在方法运行期间是不会改变局部变量表的大小
- 大小
- StackOverflowError : 当虚拟机栈放不下栈帧时
- OutOfMemory
▶ 本地方法栈
- 基本结构和虚拟机栈相同
- 为虚拟机执行native方法服务
- native方法 : 以关键字"native "声明, 简而言就是Java中声明的可调用的使用C/C++实现的方法。
▶ 堆内存
- 存储对象实例
- 垃圾收集器管理的主要区域
- 新生代, 老年代, Eden空间
- 大小
- -Xmx , -Xms
- OutOfMemory
▶ 方法区
- 存储虚拟机加载的类信息, 常量, 静态常量, 即时编译期编译后的代码等数据
- 类信息 : 类的版本, 字段, 方法, 接口
- 方法区和永久代
- HotSpot JVM中, 方法区即一个与堆内存相邻的数据区,即永久代
- jdk1.8后, 永久代PermGen 替换为 元空间Metaspace(本地内存中)
- 垃圾回收在方法区中出现的比较少, 效率比较低
▶ 运行时常量池
- 在方法区中
- 用于存放编译期生成的各种字面量和符号引用
- 字符串存储示例
▶ 对象在内存中的布局
- 对象创建流程
- 给对象分配内存
- 方法1 : 指针碰撞
- 假设Java堆中内存是绝对规整的,所有用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离
- 方法2 : 空闲列表
- 如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
- 解决并发情况下的线程安全问题
- 对分配内存空间的动作进行同步(执行效率低)
- 本地线程缓冲分配(TLAB)
- 每个线程在Java堆中预先分配一小块内存
- 方法1 : 指针碰撞
- 对象结构
- Header(对象头)
- 自身运行时数据(Mark Word)
- 哈希值, GC分代年龄, 锁状态标志, 线程持有的锁, 偏向线程ID, 偏向时间戳
- 类型指针
- 自身运行时数据(Mark Word)
- InstanceData
- 存储继承自父类和对象本身的数据
- HotSpot默认存储策略 : 相同宽度的字段分配到一起
- Padding(填充,无意义)
- HotSpot内存管理要求对象起始地址必须是8字节的整数倍
- Header(对象头)
- 对象的访问定位
- 句柄
- 栈中的对象引用指向堆中的一块内存区域-句柄池, 句柄池再保存实例对象的地址
- 栈中的引用地址可以固定, 只变动句柄池存储的地址
- 直接指针(HotSpot采用)
- 栈中的对象引用指向堆中对象真正的内存区域
- 性能更好
- 句柄
垃圾回收
▶ 配置运行参数显示垃圾回收信息
- -verbose:gc
- -xx:+PrintGCDetail
▶ 如何判定对象为垃圾对象
- 引用计数法
- 在对象中添加一个引用计数器, 当有地方引用这个对象的时候, 引用计数器的值就+1, 当引用失效时, 计数器的值就-1, 为0时标记为垃圾对象
- 缺陷 : 当堆中的对象之间相互引用时, 即使栈中的引用失效, 此对象的引用计数器还是大于0, 而不会被回收
- 可达性分析法(HotSpot)
- 通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
- 可作为GCRoots的对象
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 方法区中的类属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中引用的对象
▶ 回收策略
- 标记-清除算法
- 1.标记垃圾对象→ 2.清除
- 两个问题
- 效率问题
- 空间问题
- 复制算法
- 将堆上的内存分为两个大小相等的区域,一个是空闲区域,一个是活动区域
- 1.找出活动空间中所有存活的对象→ 2.将存活的对象复制到空闲区域→ 3.将之前的活动空间清空
- 优点 : 内存回收完毕后,有一大片连续的可用空间
- 缺点 : 有50%的空间被浪费掉
- 标记-整理算法
- 让所有可用的对象都向一端移动, 然后直接清理掉边界外的内存
- 分代收集算法
- 综合以上三种算法, 根据内存分代管理, 选择不同算法
▶ 垃圾回收器
-
Serial
- 单线程垃圾收集器, 在其进行垃圾收集的时候需要暂停其他的线程
- 简单而高效
- Client模式下的默认的新生代垃圾收集器
-
Parnew
- 除去性能因素,很重要的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作
-
Cms
- 执行过程分为4个步骤
- 初始标记
- 并发标记
- 重新标记
- 并发清除
- 优点 : 并发收集、低停顿
- 缺点
- 占用大量CPU资源
- 无法处理浮动垃圾
- 大量的空间碎片的产生
- 执行过程分为4个步骤
-
G1收集器(最牛的垃圾收集器)
- G1(Garbage-First)收集器是现今收集器技术的最新成果之一,之前一直处于实验阶段,直到jdk7u4之后,才正式作为商用的收集器。
- 执行过程
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
- 优势
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
-
垃圾收集器实现
- 总结
- 1、两个串行收集器、三个并行收集器、一个并发收集器。
- 2、ParNew收集器是Serial的多线程版本。
- 3、Serial Old收集器是Serial收集器 的旧生代版本。
- 4、Parallel Scavenge收集器以吞吐量为目标,适合在后台运算而不需要太多交互的任务。
- 5、Parallel Old收集器是Parallel Scavenge的旧生代版本。
- 6、Parallel Scavenge收集器和Parallel Old收集器是名副其实的“吞吐量优先”组合。
- 7、除CMS外,其他收集器工作时都需要暂停其他所有线程,CMS是第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收 集器线程与 用户线程同时工作,是一款以最短停顿时间为目标的收集器,适合交互性较多的场景,这也是与Parallel Scavenge/Parallel Old吞吐量优先组合的区别。
- 8、新生代因为回收留下的对象少,所以采用标记-复制法。
- 9、旧生代因为回收留下的对象多,所以采用标记-清除/标记-整理算法。
- 总结
内存分代管理
▶ 为什么要分代
- 如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率
▶ 内存分代划分
- 新生代
- Eden 伊甸园
- Survivor 存活区
- 分为from和to两块区域
- 老年代
- 永久代
▶ 新生代(Young)
- HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1
- 划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。
- 当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC
- 流程
- GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)
- GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向
- 年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区
- 接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区
- 接着,From Survivor区和To Survivor区会交换它们的角色
- 也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区, 保证To Survivor区在一轮GC后是空的
- GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中
▶ 老年代(Old)
- 在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢
▶ 永久代(Permanent)
- 永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收
▶ Minor GC 和 Full GC的区别
- 新生代GC(Minor GC):Minor GC指发生在新生代的GC,因为新生代的Java对象大多都是朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发Minor GC
- 老年代GC(Full GC/Major GC):Full GC指发生在老年代的GC,出现了Full GC一般会伴随着至少一次的Minor GC(老年代的对象大部分是Minor GC过程中从新生代进入老年代),比如:分配担保失败。Full GC的速度一般会比Minor GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC
内存分配策略
- 优先分配到eden
- 大对象直接分配到老年代
- 长期存活的对象分配到老年代
- 空间分配担保
- 动态对象年龄判断
虚拟机工具
- jps:jps位于jdk的bin目录下,其作用是显示当前系统的java进程情况,及其id号
- jstat:Jstat是JDK自带的一个轻量级工具,主要用JVM内建的指令对java应用程序的资源和性能进行实时的监控
- jinfo:可以用来查看正在运行的Java应用程序的扩展参数,甚至支持在运行时,修改部分参数
- jmap:查看Java进程对象使用情况
- jhat:用来分析java的堆情况的命令
- jstatck:查看当前java进程的堆栈状态
- Jconsole : jdk自带,是一个基于JMX的GUI工具,用于连接正在运行的JVM
- VisualVM : 第三方可视化
Class文件结构
- 魔数
- 作用:确定该文件是否是虚拟机可接受的class文件。java的魔数统一为 0xCAFEBABE (来源于一款咖啡)。
- 区域:文件第0~3字节
- 版本号
- 作用:表示class文件的版本(jdk版本),由minorversion和majorversion组成。
- 区域:文件第4~7字节。
- 常量池
- 前端的两个字节占有的位置叫做常量池计数器(constant_pool_count),它记录着常量池的组成元素 常量池项(cp_info)的个数
- 每个常量池项(cp_info) 都会对应记录着class文件中的某种类型的字面量
- 访问标志
- 表示类或者接口方面的访问信息,比如Class表示的是类还是接口,是否为public、static、final等
- 类索引、父类索引与接口索引集合
- Class文件中由这3项数据来确定类的继承关系
- 字段表集合
- 描述接口或者类中声明的类变量以及实例变量,不包括方法中的局部变量
- 方法表集合
- 描述该类中的方法
- 属性表集合
- 该属性里主要存放由javac编译器处理后得到的字节码指令
类加载
▶ 类加载流程
-
- 加载和连接的时机并不是完全的串行执行
- 加载和连接的时机并不是完全的串行执行
▶ 加载
- 规范
- 通过一个类的全限定名来我获取定义此类的二进制流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象, 作为这个类的各种数据的访问入口
- 加载源(二进制流)
- 文件
- Class文件
- Jar文件
- 网络
- 计算生成一个二进制流
- $Proxy
- 由其它文件生成
- Jsp
- 数据库
- 文件
▶ 连接
- 验证
- 确保Class文件的字节流中包含的信息符合当前虚拟机的要求, 并且不会危害虚拟机自身的安全
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
- 确保Class文件的字节流中包含的信息符合当前虚拟机的要求, 并且不会危害虚拟机自身的安全
- 准备
- 准备阶段正式为类变量分配内存并设置变量的初始值, 这些变量使用的内存都将在方法去中进行分配
- 这里的初始值并非我们制定的值, 而是其数据类型对应的默认值
- 如果被final修饰, 那么在这个过程中, 常量值会被一同指定
- 准备阶段正式为类变量分配内存并设置变量的初始值, 这些变量使用的内存都将在方法去中进行分配
- 解析
- 解析阶段是将虚拟机将常量池中的符号引用替换为直接引用的过程
- 类或者接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
- 解析阶段是将虚拟机将常量池中的符号引用替换为直接引用的过程
▶ 初始化
-
初始化是类加载的最后一步
-
初始化是执行 clinit( ) 方法的过程
- clinit( ) 方法是由编译器自动收集类中的所有 类变量始化语句 和 静态代码块中的语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序决定的
- 静态代码块中只能访问定义在它之前的变量, 定义在它之后的变量, 可以赋值, 但不能访问。
- clinit( ) 方法是线程安全的 : 如果多个线程同时初始化一个类, 只有一个线程会执行这个类的 clinit( ) 方法, 其他线程等待执行完毕, 如果方法执行时间过长, 那么就会造成多个线程阻塞
-
初始化时机规范
- 遇到new, getstatic, putstatic或invokestatic这4条字节码指令时, 如果类没有进行过初始化, 则需要先出发其初始化。生成这4条指令的最常见的java代码场景是:
- 使用new 关键字实例化对象时
- 读取或设置一个类的静态字段(被final修饰和已在编译器把结果放入常量池的静态字段除外) 时
- 调用一个类的静态方法时
- 使用Java.lang.reflect包的方法对类进行反射调用的时候, 如果类没有进行过初始化, 则需要先触发其初始化
- 当初始化一个类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化,
- 当虚拟机启动时, 用户需要制定一个要执行的主类(包含main()方法的类), 虚拟机会先初始化这个主类
-
不被初始化的例子
1. 通过子类引用父类的静态字段, 子类不会被初始化 2. 通过数组定义来引用类 3. 调用类的常量(static final修饰的字段)
▶ 类加载器
- 功能 : 通过一个类的全限定名来获取描述此类的二进制字节流
- 只有被同一个类加载器加载的类才可能会相等, 相同的字节码被不同额度类加载器加载的类不相等
- 分类
- 启动类加载器
- 由C++实现, 是虚拟机的一部分, 用于加载JAVAHOME下的lib目录下的类
- 扩展类加载器
- 加载JAVAHOME下lib/ext目录中的类
- 应用程序类加载器(使用最多)
- 加载用户类路径上的所指定的类库
- 自定义类加载器
- 实现
- 定义一个类, 继承ClassLoader
- 重写loadClass方法
- 实例化Class对象
- 优势
- 高度的灵活性
- 通过自定义类加载器可以实现热部署
- 代码加密
- 实现
- 启动类加载器
▶ 类加载器如何协同工作
- 双亲委派模型(jdk1.2之后)
- 流程
1. 如果一个类加载器收到了类加载请求, 它首先不会去自己尝试加载这个类, 而是把加载请求委派给父类加载器去完成
2. 每一层的类加载器都把类加载请求为派给父类加载器, 知道所有的类加载请求都传递给顶层的启动类加载器
3. 如果顶层的启动类加载器无法完成加载请求, 子类加载器尝试去加载, 知道最后, 如果连最初发起类加载请求的类加载器都无法完成加载请求时, 就会抛出ClassNotFoundException, 而不再调用其子类加载器 - 优点
- java类和它的类加载器一起具备了一种带优先级的层次关系, 越是基础的类, 越是被上层的类加载器进行加载, 保证了Java程序的稳定运行
END
更多推荐
已为社区贡献1条内容
所有评论(0)