前言:JVM全称Java Virtual Machine是虚构的计算机,也是因此Java才可在各个系统平台运行,本文内容篇幅较长主要分为JVM整体流程,内存划分及组成以及JVM机制等方面进行介绍
一、JVM整体流程
一个java文件执行的大致步骤流程如下:
一张复杂的JVM架构图:
JVM加工类过程
-
加载
- 将class字节码文件加载进虚拟机,存储至元空间的方法区内
-
验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
-
准备
- 为类变量(static修饰)分配内存并设置初始值(指JVM默认值,例如:引用类型为null,int类型为0,不是类中指定的值)
- 如果类变量被final修饰,则直接赋类中定义的默认值(特殊情况:该final静态变量如果需要计算,会导致初始化)
- 成员变量(实例变量、属性、属于对象)不分配内存
-
解析
- 将常量池的符号引用转为直接引用(由于java为值传递,去掉了指针)
-
初始化
- 给类变量赋值(类中定义的默认值)
- 初始化静态语句块
相关题目测试
class GrandParent3{ static { System.out.println("GrandParent3静态代码块"); } } class Parent3 extends GrandParent3{ public final static String parent3="hello parent3"; static{ System.out.println("Parent3 静态代码块");} } class Children3 extends Parent3{ public static String children3 ="hello children3"; static{ System.out.println("Children3 静态代码块");} } // 测试----------------------------------------------------------- public class ClassLoaderTest { public static void main(String[] args) { System.out.println(Children3.parent3); } } // 输出结果 hello parent3
解释:由于Children3.parent3调用的是父类final修饰的类变量,由上面类的加工流程可以看出final修饰的类变量在准备阶段就已经赋予了初始值,所以根本就没初始化
主动使用与被动使用
子类引用调用父类非final静态变量会导致父类初始化,自身不会初始化(被动使用)
子类调用自身静态变量,父类,子类都会初始化(主动使用)
导致JVM初始化类的时机
- 创建对象(new Obj())
- 访问类的静态变量,静态方法
- 反射加载某类(Class.forName("obj"))
- 初始化子类会导致父类的的初始化
- 被标明为启动类的类(Main方法的类)
- 此时类才会被赋予真正的值(类中定义的初始默认值),也会执行静态代码块
二、JVM运行时数据区
经过类加载后,class字节码文件被读进内存,放至元空间,并且创建一个Class对象(反射对象)放入堆中
元空间(非堆)存储信息
在Java1.8后,常量池在方法区中,方法区在在元空间内
- 这个类型的完整有效名
- 该类型的父类的完整有效名
- 该类型的修饰符(public、final、abstract等)
- 该类型的直接接口列表
- 方法区:所有线程共享的区域
- 静态变量(static)
- 常量(final)
- 类信息(构造方法,接口定义)
堆存储的信息:Class对象(反射对象)
- 类的成员变量(Field[]),构造方法(Constructor[]),成员方法(Method[])
- 类的返回值
- 类的访问权限
- 实例变量
内存划分代码示例
分析:
- 加载:new Person对象,即将对Person类进行初始化
- 准备阶段:String name,int age,show()方法进入方法区,static String country = null;
- 执行main方法,main栈帧压栈
- 初始化:new Person(”邓丽君“,16,”中国“),String name = null -》”邓丽君“,int age =0 -》16,static String country = null-》”中国“
- p1.show():show方法压栈,方法结束后弹栈
- p2.show():show方法压栈,方法结束后弹栈
- main方法结束,弹栈,栈清空
元空间(非堆)与堆结构示意图(详细看四、扩展下的堆)
堆真正意义上为:伊旬园区+幸存0,1区(to,from)+老年代
栈帧存储的信息
栈以栈帧为单位,一个栈帧对应一个方法,栈帧分为顶帧(前栈帧)和底帧,JVM只对顶帧(当前方法)起作用,生命周期与线程同步,一个main线程的结束意味着栈内存的释放,不存在垃圾回收问题
- 方法的局部变量表(存放方法参数和方法内的局部变量,八大基本类型),索引从0开始最大slot-1
- 操作数栈
- 动态连接(对象的引用)
- 方法返回地址
- 附加信息
本地方法区
作用:通过JNI(Java Native Interface)结合其他语言对Java进行功能的扩展
流程:在内存中开辟了一块本地方法栈(Native Method Stack ),用于登记标记native修饰的方法,再通过调用JNI执行本地库的方法
注意:现在一般通过套接字(Socket),Restful等方式扩展调用
三、JVM机制
双亲委派机制
- 作用:保证程序安全,稳定
- 执行流程:执行mian方法时会依次在三个加载器()中不断查找检查有没该同名方法,有则执行最后匹配的加载器里的方法,没有则执行最初加载器的方法(方法执行加载流程如下)
委派流程
获取加载器:Obj.getClass.getClassLoader()
沙箱安全机制
让java代码只能在指定的环境下运行
组件
-
字节码校验器(核心类不用校验)
-
类装载器
- 防止恶意代码干涉正常代码(双亲委派机制)
- 将代码归入保护域,指定代码能执行哪些操作(沙箱安全)
-
存取控制器:控制核心API对操作系统的存取权限
-
安全管理器:核心API操作系统之间的主要接口,实现权限控制,优先级高于存取控制器
-
软件安全包
- 安全提供者
- 消息摘要
- 数字签名:keytools(生成网站信用证书;)
- 加密
- 鉴别
垃圾回收机制
复制算法
GC分轻GC和重GC,轻GC一般针对伊甸园区,该区经过GC后,少部分幸存对象转入幸存0,1区剩余空间大的区,之后清空伊甸园区。但是,当幸存0,1区对象数相等时,则采用复制算法,将幸存区的对象复制到另一个幸存区(关于from,to也就是幸存0,1区是动态互换的,谁空谁就是to),保证to区是空的
所以一次轻GC后,伊甸园区和to区都为空,幸存区反复清理15(默认值)次后,幸存区幸存的对象就进入老年区
好处:没有碎片化空间
坏处:浪费了一片幸存区空间(to区),极端情况下如果对象存活率达100%,会导致复制大量对象
总结:复制算法适用于对象存活率低的新生区
标记清除算法
清除时标记有使用的对象,回收时清除没有标记的对象
好处:不需要额外空间
坏处:两次扫描严重浪费时间,会产生内存碎片
标记压缩算法
对标记清除法的优化,减少内存碎片
再次扫描内存,将存活的对象都移到一端
总结:老年代用标记清除+标记压缩混合实现(GC调优处)
四、扩展
类加载器
- 根类加载器(Boot Strap class Loader)位于jre/lib/rt.jar
- 扩展类加载器(Extension class Loader)位于jre/lib/ext包下
- 应用类加载器(Application class Loader)当前java工程的bin目录
三种虚拟机
- Sun的HotSpot(最常用)
- BEA的JRockit
- IBM的J9 VM
堆(Heap)
一个JVM只有一个堆内存,堆内存的大小是可以调节的
堆的划分
-
新生区
-
伊甸园(Eden Space):所有对象都是在该区域new出来的
-
幸存者区:伊甸园区满了之后会执行一次轻GC幸存的对象进入该区域
- 幸存者0区:1,0区满后会执行一次重CG
- 幸存者1区
-
-
老年区:幸存区幸存的对象会进入老年区
-
永久存储区(元空间jdk1.8+)
- jdk1.6之前:永久代,常量池在方法区中
- jdk1.7:永久代,去永久代,常量池在堆中
- jdk1.8:无永久代,常量池在元空间的方法区
永久区(元空间):该区域常驻内存,存放jdk自身携带的Class对象,Interface元数据,存储java运行时的一些环境或是类信息,该区域不存在垃圾回收,关闭VM虚拟机,会释放该区域内存
永久区引发OOM(OutOfMemoryError)的原因:
- 加载了大量第三方jar包
- Tomcat部署太多应用
- 大量动态生成的反射类,不断被加载
解决OOM问题:
尝试扩大堆内存空间,如果还有OOM问题则排查是否是代码问题
- 扩大堆内存
输出原占用内存
可以看到jvm最大可以从操作系统里挖走1797.5MB的内存,目前已经占了123MB
通过edit config对虚拟机参数进行调整
设置参数后内存都变为981.5MB
科普上图各个参数含义:
- -Xmx:没添加该参数,则默认能从操作系统挖到的内存大小(默认为操作系统的1/4),加了则以规定的内存大小挖(max)
- -Xms:没添加该参数,则是慢慢挖取默认规定的操作系统内存(默认初始化为操作系统的1/64),加了则直接挖去定义的内存(total)
- -XX:+PrintGCDetails:输出堆的详细信息
- PSYoungGen:新生区
- eden space:伊甸园区
- from/to:幸存者0/1区(时常互换)
- ParOldGen:老年区
- Metaspace:元空间
小扩展:
为什么元空间逻辑上存在,物理上不存在?
由上面的GCDetails可以看出
305664(新生区)+699392(老年区)= 1005056K
1005056/1024 = 981.5MB
所以元空间也可称为非堆
freeMemory是什么?
JVM挖内存会偷偷多挖一些内存,这些偷挖过来又没用上的即freeMemory,如果设置了-Xms,偷挖的内存大部分不会用到这时freeMemory这个值就大一些
内存快照工具分析代码问题(MAT:eclipse,Jprofile:IDEA)
- 分析Dump内存文件,快速定位内存泄露
- 获取dump:-XX:+HeapDumpOnOutOfMemoryError
- 获取堆中数据
- 获取大的对象
参考地址
来源:https://www.cnblogs.com/guaosky/p/12657352.html