JVM内容整合

爱⌒轻易说出口 提交于 2020-04-08 00:48:55

前言:JVM全称Java Virtual Machine是虚构的计算机,也是因此Java才可在各个系统平台运行,本文内容篇幅较长主要分为JVM整体流程,内存划分及组成以及JVM机制等方面进行介绍


一、JVM整体流程

一个java文件执行的大致步骤流程如下:

一张复杂的JVM架构图:

JVM加工类过程

  1. 加载

    • 将class字节码文件加载进虚拟机,存储至元空间的方法区内
  2. 验证

    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证
  3. 准备

    • 为类变量(static修饰)分配内存并设置初始值(指JVM默认值,例如:引用类型为null,int类型为0,不是类中指定的值)
    • 如果类变量被final修饰,则直接赋类中定义的默认值(特殊情况:该final静态变量如果需要计算,会导致初始化)
    • 成员变量(实例变量、属性、属于对象)不分配内存
  4. 解析

    • 将常量池的符号引用转为直接引用(由于java为值传递,去掉了指针)
  5. 初始化

    • 给类变量赋值(类中定义的默认值)
    • 初始化静态语句块

    相关题目测试

    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初始化类的时机

  1. 创建对象(new Obj())
  2. 访问类的静态变量,静态方法
  3. 反射加载某类(Class.forName("obj"))
  4. 初始化子类会导致父类的的初始化
  5. 被标明为启动类的类(Main方法的类)
  6. 此时类才会被赋予真正的值(类中定义的初始默认值),也会执行静态代码块

二、JVM运行时数据区

经过类加载后,class字节码文件被读进内存,放至元空间,并且创建一个Class对象(反射对象)放入堆中

元空间(非堆)存储信息

在Java1.8后,常量池在方法区中,方法区在在元空间内

  1. 这个类型的完整有效名
  2. 该类型的父类的完整有效名
  3. 该类型的修饰符(public、final、abstract等)
  4. 该类型的直接接口列表
  5. 方法区:所有线程共享的区域
    • 静态变量(static)
    • 常量(final)
    • 类信息(构造方法,接口定义)

堆存储的信息:Class对象(反射对象)

  1. 类的成员变量(Field[]),构造方法(Constructor[]),成员方法(Method[])
  2. 类的返回值
  3. 类的访问权限
  4. 实例变量

内存划分代码示例

分析:

  1. 加载:new Person对象,即将对Person类进行初始化
  2. 准备阶段:String name,int age,show()方法进入方法区,static String country = null;
  3. 执行main方法,main栈帧压栈
  4. 初始化:new Person(”邓丽君“,16,”中国“),String name = null -》”邓丽君“,int age =0 -》16,static String country = null-》”中国“
  5. p1.show():show方法压栈,方法结束后弹栈
  6. p2.show():show方法压栈,方法结束后弹栈
  7. main方法结束,弹栈,栈清空

元空间(非堆)与堆结构示意图(详细看四、扩展下的堆)

堆真正意义上为:伊旬园区+幸存0,1区(to,from)+老年代

栈帧存储的信息

栈以栈帧为单位,一个栈帧对应一个方法,栈帧分为顶帧(前栈帧)和底帧,JVM只对顶帧(当前方法)起作用,生命周期与线程同步,一个main线程的结束意味着栈内存的释放,不存在垃圾回收问题

  1. 方法的局部变量表(存放方法参数和方法内的局部变量,八大基本类型),索引从0开始最大slot-1
  2. 操作数栈
  3. 动态连接(对象的引用)
  4. 方法返回地址
  5. 附加信息

本地方法区

作用:通过JNI(Java Native Interface)结合其他语言对Java进行功能的扩展

流程:在内存中开辟了一块本地方法栈(Native Method Stack ),用于登记标记native修饰的方法,再通过调用JNI执行本地库的方法

注意:现在一般通过套接字(Socket),Restful等方式扩展调用

三、JVM机制

双亲委派机制

  1. 作用:保证程序安全,稳定
  2. 执行流程:执行mian方法时会依次在三个加载器()中不断查找检查有没该同名方法,有则执行最后匹配的加载器里的方法,没有则执行最初加载器的方法(方法执行加载流程如下)

委派流程

获取加载器:Obj.getClass.getClassLoader()

沙箱安全机制

让java代码只能在指定的环境下运行

组件

  1. 字节码校验器(核心类不用校验)

  2. 类装载器

    • 防止恶意代码干涉正常代码(双亲委派机制)
    • 将代码归入保护域,指定代码能执行哪些操作(沙箱安全)
  3. 存取控制器:控制核心API对操作系统的存取权限

  4. 安全管理器:核心API操作系统之间的主要接口,实现权限控制,优先级高于存取控制器

  5. 软件安全包

    • 安全提供者
    • 消息摘要
    • 数字签名: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目录

三种虚拟机

  1. Sun的HotSpot(最常用)
  2. BEA的JRockit
  3. IBM的J9 VM

堆(Heap)

一个JVM只有一个堆内存,堆内存的大小是可以调节的

堆的划分

  1. 新生区

    • 伊甸园(Eden Space):所有对象都是在该区域new出来的

    • 幸存者区:伊甸园区满了之后会执行一次轻GC幸存的对象进入该区域

      • 幸存者0区:1,0区满后会执行一次重CG
      • 幸存者1区
  2. 老年区:幸存区幸存的对象会进入老年区

  3. 永久存储区(元空间jdk1.8+)

    • jdk1.6之前:永久代,常量池在方法区中
    • jdk1.7:永久代,去永久代,常量池在堆中
    • jdk1.8:无永久代,常量池在元空间的方法区

永久区(元空间):该区域常驻内存,存放jdk自身携带的Class对象,Interface元数据,存储java运行时的一些环境或是类信息,该区域不存在垃圾回收,关闭VM虚拟机,会释放该区域内存

永久区引发OOM(OutOfMemoryError)的原因:

  • 加载了大量第三方jar包
  • Tomcat部署太多应用
  • 大量动态生成的反射类,不断被加载

解决OOM问题:

尝试扩大堆内存空间,如果还有OOM问题则排查是否是代码问题

  1. 扩大堆内存

输出原占用内存

可以看到jvm最大可以从操作系统里挖走1797.5MB的内存,目前已经占了123MB

通过edit config对虚拟机参数进行调整

设置参数后内存都变为981.5MB

科普上图各个参数含义:

  1. -Xmx:没添加该参数,则默认能从操作系统挖到的内存大小(默认为操作系统的1/4),加了则以规定的内存大小挖(max)
  2. -Xms:没添加该参数,则是慢慢挖取默认规定的操作系统内存(默认初始化为操作系统的1/64),加了则直接挖去定义的内存(total)
  3. -XX:+PrintGCDetails:输出堆的详细信息
  4. PSYoungGen:新生区
  5. eden space:伊甸园区
  6. from/to:幸存者0/1区(时常互换)
  7. ParOldGen:老年区
  8. Metaspace:元空间

小扩展:

为什么元空间逻辑上存在,物理上不存在?

由上面的GCDetails可以看出

305664(新生区)+699392(老年区)= 1005056K

1005056/1024 = 981.5MB

所以元空间也可称为非堆

freeMemory是什么?

JVM挖内存会偷偷多挖一些内存,这些偷挖过来又没用上的即freeMemory,如果设置了-Xms,偷挖的内存大部分不会用到这时freeMemory这个值就大一些

内存快照工具分析代码问题(MAT:eclipse,Jprofile:IDEA)

  • 分析Dump内存文件,快速定位内存泄露
    • 获取dump:-XX:+HeapDumpOnOutOfMemoryError
  • 获取堆中数据
  • 获取大的对象

参考地址

深入 Java 类加载全流程,值得你收藏

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!