vlambda博客
学习文章列表

JAVA虚拟机解析--基于JDK1.8

        java虚拟机(java virtual machine,JVM),一种能够运行java字节码的虚拟机。作为一种编程语言的虚拟机,实际上不只是专用于java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。



JVM的基本结构

   JVM由三个主要的子系统构成:

  • 类加载子系统

  • 运行时数据区(内存结构)

  • 执行引擎





------------------------------------------------------------------------------


类加载机制

类的生命周期

JAVA虚拟机解析--基于JDK1.8


解析:

动态链接:程序运行期间,由符号引用转化为直接引用



类加载器的种类

  • 启动类加载器(Bootstrap ClassLoader)

        负责加载JRE核心类库,如JRE目标下的rt.jar, charset.jar等

  • 扩展类加载器(Extension ClassLoader)

        负责加载JRE扩展目录ext中jar类包

  • 系统类加载器(Application ClassLoader)

        负责加载ClassPath路径下的类包

  • 用户自定义加载器(User ClassLoader)

        负责用户自定义路径下的类包


JAVA虚拟机解析--基于JDK1.8



-----------------------------------------------------------------------------------------


类加载机制:

  • 全盘负责委托机制

    当一个classlader加载一个类的时候,除非显示的使用另一个classloader,该类所依赖和引用的类也由这个classloader载入。

  • 双亲委派机制:

    指定先委托父类加载器寻找目标类,在找不到的情况下,在自己的路径中查找并载入目标类

  • 双亲委派模式的优势:

  • 沙箱安全机制:比如自己写的string类不会被加载,这样可以防止核心库被随意篡改

  • 避免类的重复加载:当父classloader已经加载了该类的时候,就不需要子classloader再加载一次。


举个例子:

比如自定义一个 demo类(双亲委派机制

编译完后

  1. 由application classcloader加载器

  2. app进行判断它头上是否有加载器(它的父类加载器是ext(同理往上找到boot))(这些jvm来完成判断)

  3. boot发现自定义的demo类并不是自己加载范畴,于是往下提交给ext,ext也加载不了,于是往下交给app;

  4. 此时app才可以进行加载demo


双亲委派机制的好处:

  1. 避免重复;2.安全性更好(隔离)

比如自定义一个java.lang.String类,此时无法正常加载,因为jvm真正使用的是boot加载器

JAVA虚拟机解析--基于JDK1.8

JAVA虚拟机解析--基于JDK1.8


全盘委派机制

JAVA虚拟机解析--基于JDK1.8


提问:为什么要打破双亲委派机制?(不按照双亲机制去加载)自定义类加载器去执行

答:比如jdbc,tomcat,一些热部署技术都是打破双亲委派机制,一个tomcat容器跑了一个jvm,同个tomcat可以部署多个war包,多个war包里面共用了一个jar,为了做隔离,tomcat就通过自定义类加载器来实现类隔离,避免产生冲突(因为jar包只有一个,全限定名也一致)







-----------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------

运行时数据区(内存结构)


JAVA虚拟机解析--基于JDK1.8


程序代码解析

public class Demo {
public int math() { int a = 1; int b = 2; int c = (a + b) * 10; return c; } public static void main(String[] args) { Demo d = new Demo(); d.math(); }
}

对上面的代码进行解析(main线程栈帧解析)

JAVA虚拟机解析--基于JDK1.8

编辑java文件,获得到一段16进制文件$ javac Demo.java

JAVA虚拟机解析--基于JDK1.8

进行反汇编:

javap -c demo.class

用法: javap <options> <classes>其中, 可能的选项包括: -help --help -? 输出此用法消息 -version 版本信息 -v -verbose 输出附加信息 -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示所有类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的 系统信息 (路径, 大小, 日期, MD5 散列) -constants 显示最终常量 -classpath <path> 指定查找用户类文件的位置 -cp <path> 指定查找用户类文件的位置 -bootclasspath <path> 覆盖引导类文件的位置

javap -c Demo.class > demo.txt

打开demo.txt



https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html


demo.txt

Compiled from "Demo.java"public class com.test.Demo { public com.test.Demo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return
public int math(); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn
public static void main(java.lang.String[]); Code: 0: new #2 // class com/test/Demo 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method math:()I 12: pop 13: return}

看math方法:(分析两个指令,其他的类似)

iconst_1

JAVA虚拟机解析--基于JDK1.8

将第一个变量放到程序计数器中;


istore_1

JAVA虚拟机解析--基于JDK1.8

从程序计数器中取出数据(栈:先进后出机制),然后放到局部变量表

图示过程:(长度根据变量类型来定义)

JAVA虚拟机解析--基于JDK1.8

JAVA虚拟机解析--基于JDK1.8


JAVA虚拟机解析--基于JDK1.8

JAVA虚拟机解析--基于JDK1.8

(上右图局部变量表不存在顺序(非栈结构))

JAVA虚拟机解析--基于JDK1.8

JAVA虚拟机解析--基于JDK1.8

。。。。(省略)
。。。。(省略)

常量池:-128-127(10就是从方法区中的Integer常量池中获取的,每个线程都可以获取(线程共享))


Code:就是程序计数器记录的位置(确保多线程按照顺序执行)


-----------------------------------------------------------------------------

javap -v demo.class >demodynamic.txt (获取动态链接的信息,内容更多)

红色为自己标记:

Classfile /E:/workspace/demo01/src/main/java/com/test/demo.class Last modified 2020-3-3; size 374 bytes MD5 checksum 642b4793cbb9e43369b45632d50d2f3d Compiled from "Demo.java"public class com.test.Demo minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool:                  # 编译期间已经确定,在方法区中叫运行时常量池 #1 = Methodref #5.#16 // java/lang/Object."<init>":()V #2 = Class #17 // com/test/Demo #3 = Methodref #2.#16 // com/test/Demo."<init>":()V #4 = Methodref #2.#18 // com/test/Demo.math:()I #5 = Class #19 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 math #11 = Utf8 ()I #12 = Utf8 main #13 = Utf8 ([Ljava/lang/String;)V #14 = Utf8 SourceFile #15 = Utf8 Demo.java #16 = NameAndType #6:#7 // "<init>":()V  #17 = Utf8               com/test/Demo   #全限定名 #18 = NameAndType #10:#11 // math:()I #19 = Utf8 java/lang/Object{ public com.test.Demo(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0
public int math(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn LineNumberTable: line 10: 0 line 11: 2 line 12: 4 line 13: 11
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #2 // class com/test/Demo 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method math:()I 12: pop 13: return LineNumberTable: line 16: 0 line 17: 8 line 18: 13}SourceFile: "Demo.java"


程序运行时,新建一个类的程序运行状态(动态链接:符号引用(字符串)》直接引用):






  • 方法区(Method Area)(jdk1.8以前:永久代/持久代,之后:元空间)

类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在这里定义。简单来说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是为了和java的堆区分开(jdk1.8以前hotspot虚拟机叫做永久代,持久代,jdk1.8叫元空间)

  • 堆(Heap)

虚拟机启动时,自动分配创建,用于存放对象的实例,几乎所有对象都在对上分配内存,当对象无法在该空间申请到内存是将抛出OutOfMemoryError异常,同时也是垃圾回收(GC)管理的主要区域

  1. 新生代(Young Generation)

gc: 

youngGC/MinorGC

CMS OldGC(针对老年代的回收)

majorGC/FullGC(所有的回收,包含元空间)

G1 MixedGC (混合回收)

        类出生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。

新生代分为两部分:eden和survivor space(from+to)。所有的类都在eden区被new出来,幸存区分为from+to。当eden区的空间用完后,程序又需要创建对象,jvm的gc将eden区进行垃圾回收(minor gc),将eden区中的不再被其它对象应用的对象进行销毁。然后将eden区中剩余的对象移到from区,若from也满了,再对该区进行gc,然后移到to区(这块有多种gc算法,不扩展,后续展开)


from才会接收来自eden的对象,from满了会触发gc转移到to,同时年龄+1;

然后from-to逻辑进行转换(复制算法),多次转换后当年龄==15,就转移到老年代;


年龄:(64位)对象头中针对年龄是一个4字节,0000~FFFF (0~15)

age<=15,通过jvm参数设置



分配担保机制(如果对象过大(和新生代的百分比进行比较),则直接初始化到老年代中)

引用如下进行解释分配担保机制:

https://blog.csdn.net/kavito/article/details/82292035



        b. 老年代(Old Generation)

        新生代经过多次GC仍然存货的对象移动到老年区。若老年代也满了,这时候将发生Major GC(也可以叫Full GC),进行老年区的内存清理。若老年区执行了Full GC之后发现依然无法进行对象的保存,就会抛出OOM(OutOfMemoryError)异常


  • 元空间(Meta Space)

        在JDK1.8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,区别在于元数据区不在虚拟机当中,而是用

的本地内存,永久代在虚拟机当中,永久代逻辑结构上也属于堆,但是物理上不属于。

为什么移除了永久代?

参考官方解释http://openjdk.java.net/jeps/122

大概意思是移除永久代是为融合HotSpot与 JRockit而做出的努力,因为JRockit没有永久代,不需要配置永久代。

  • 栈(Stack)

        Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致

  • 本地方法栈(Native Method Stack)

        和栈作用很相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行native方法服务。登记native方法,在Execution Engine执行时加载本地方法库

  • 程序计数器(Program Counter Register)


jvm还有很多内容,后期有时间继续研究,有不足的大家留言提出谢谢!