jvm专题 - 体系结构

落花踏尽游何处,笑入胡姬酒肆中。这篇文章主要讲述jvm专题 - 体系结构相关的知识,希望能为你提供帮助。

jvm是老生常谈的一个话题了,虽然大家一直在用在研究,但遇到一些线上问题时有时还是无从下手,笔者刚开始接触时可以说是一看就会,一用就废(可能水平有限哈,本质上还是不理解),后续又系统性的回顾了几次,又处理了各种由于JVM配置不合理引发的线上问题,随着理解的深入发现对复杂程序的编写以及性能的调优还是有很大用处的。
基于以上,笔者最近整理了一系列文章,供大家参考。希望大家能少走笔者的弯路。全系统大概分4部分:jvm基础、class文件、字节码编程、gc。持续时间可能会比较长,笔者尽量多花时间整理完整。
一、基本概念jvm 可以想像成一个是可运行 java 代码的软件APP ,这个APP要安装在操作系统,与硬件没有直接的交互。但由于不同操作系统的底层实现可能不一样,所以每一种平台的解释器是不同的,但是实现的jvm是相同的,这也就是 Java 为什么能够跨平台的原因 ,当一个程序从开始运行,jvm就开始实例化了,多个程序启动就会存在多个jvm实例。jvm实例会随着程序退出或者关闭而消亡,多个虚拟机实例之间数据不能共享。
java执行过程:1、Java 源文件--> 编译器--> 字节码文件;2、字节码文件--> JVM--> 机器码
体系结构
jvm的结构如下图所示,共包含4块内容,这几块内容是相互独立的:

上图涉及的几个术语解释如下:
  • java程序设计语言:也就是我们的JAVA程序
  • class文件格式:java程序设计语言编辑后的由虚拟机可以理解的字节码,这种字节码就是java VM的“机器语言”,它通过jvm和操作系统的动态库进行通信
  • java应用程序接口(API):java类库的实现,class文件负责调用API,API负责处理平台无关性,它是通过调用本地动态库的方式实现的。
  • java虚拟机:它和API共同组成了java平台,java虚拟机可以由不同的技术软件或硬件遵循java虚拟机规范来实现。
结构补充说明执行引擎的实现有几种方式:最简单的是一次性解释字节码,损失的是速度换得的是内存;第二种是即时编译,类似于延迟加载的概念;第三种是自适应优先器,虚拟机开始的时候会解释字节码,但会一直监视并记录下使用最频繁的代码段并把他们解释成本地机器代码,运行时再解释,这样VM可以在90%左右的时间内运行频繁代码;最后一种就是硬件VM。一般的实现是延迟加载实现。
      java中有两种方法:java方法和本地方法,java方法保存在class中,本地方法是由其它语言编写的保存在动态连接库中,它是联系java程序和OS的连接方法。java程序也可以直接访问OS,一般不要这样做因为这样JAVA程序就会变成平台相关的了,这种一般用于嵌入式系统居多。
java应用程序有两种class loader:启动类加载器和用户自定义加载器。前者是系统默认的,它一般只负责加载核心也就是JAVA API中的类,自定义的加载器一般是在程序运行时由启动类加载器加载的。class loader会监视加载过的类,如果此类调用了其它类,其它类只能加载在同一个加载器中。这样这些类就动态联系起来了。通过这种方法,java的体系结构允许在一个java应用程序中建立多个命名空间,每一个class loader都有自己的命名空间。默认情况下各个class loader是相互屏蔽的,除非application显示地允许这样做。这种相互隔离的方式可以避免代码间的相互影响,特别是能够阻止恶意代码,类加载器要知道如何下载class文件。
java代码安全模型:在1.2中称为访问控制器,它是一个类。在1.2以前称为安全管理器。java程序是动态连接的(不通过C语言这样的指针而通过命名的方式连接的)也是动态扩展的(需要时才下载)。
二、沙箱机制java的安全性主要目的是保护不知名的程序对本地环境的破坏,java在安全方面经历了一个过程,1.0版本很暴力就是不允许;1.1版本加了签名技术,但这种技术也是允许和不允许,只不过给了用户一个选择权;1.2版本加入了一个sandbox策略,由自定义不同安全策略来达到安全的目的。sandbox主要由四部分组成:1、类装载器;2、class文件检验器;3、内置于java VM的安全特性;4、安全管理器及java API。
java的沙箱模型最主要的优点是类装载器和安全管理器是可以用户定制的。允许用户在一个和程序分离的ASCII策略文件中说明安全策略,这个安全管理器在运行时获得一个类(访问控制器)的帮助。
1、类装载器体系结构
在java沙箱中,class loader是第一道防线,它有三方面的保护作用:
  1. 防止恶意代码去干涉善意的代码:通过为不同的class loader提供不同的命名空间来实现的,命名空间是由java VM为每个class loader维护的;
  2. 将代码分类,该类别确定了代码可以执行哪些操作,敏感和非敏感操作,敏感操作每次都要底部;
  3. 守护了被信任的类库的边界:通过分别使用不同的class loader装载可靠的包和不可靠的包来实现的,即下图的4层加载;

针对上图的补充说明:
越底层的信任度越高(启动类装载器为最底层),启动类加载器可以抢在标准扩展类加载器前去装载类。所以类加载的顺序是以最底层的为准,启动类加载器是类加载链最后一个,如果它有能力装载一个类就会直接返回此类型,否则用户自定义的class loader会试图自己来装载此类。也就是由于这种机制才限制了不能用自定义的java.lang.String替换掉java API中的此类,因为启动类加载类会先从java API加载。
类加载器的安全机制中对于同路径、同加载器加载的类之间才会通信,不同加载器间需要代码控制。所以如果定义了一个java.lang.Virus类。则此类会被网络加载器加载(如果存在),形式上看它是一个java API中的核心类,可以做一些坏事。事实并非如此,如果想让Virus能调用java API中的核心函数,它必须同时满足几个条件:1、包名一样;2、类加载器一样。这里明显第二条不满足。
启动类加载器启动类加载器它只负责装载那些核心的java API的class文件,即%JAVA_HOME%\\lib 目录中的,或通过-Xbootclasspath 参数指定路径且被虚拟机认可(按文件名识别,如 rt.jar)的类。JVM开始运行时,可以创建一个或多个用户自定义的class loader。这里的用户自义其实也是由java VM实现提供的,不是开发人员自己写的;
扩展类加载器扩展类加载器(Extension ClassLoader): 负责加载 JAVA_HOME\\lib\\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库;
应用类加载器应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库;
自定义类加载器?正常来讲很少用到这种技术,除非一些字节码编程或底层的架构才会用到。比如不想让自己的类加载器比如(网络)加载类能装载来自absolutepower包中的任何类。这时你就需要写自己的类加载器了。让它做的第一件事就是确认被请求的类型不是absolutepower包中的一员,如果是就直接抛出异常,而不是把这个类的名字传给它的双亲去加载。这种方式可能就称为“双亲委派破坏”;
默认的双亲委派有个问题就是底层的类不能访问上层的类,这也会限制java的一些扩展功能,所以有了一个上下文加载器,可以参考 javax.xml.parsers.DocumentBuilderFacotry.newInstance()实现,它主要是用Thread.currentThread()中的两个静态方法实现的JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。
1.1、双亲委派
用户自定义的classloader被创建时都会默认分配一个双亲classloader,如果不显示指定则默认为系统classloader。自定义的classloader可以显式地传递一个双亲类装载器给它的构造方法,如果不传则默认为系统类加载器,如果传null则默认为启动类装载器。
双亲委派破坏这种技术很好理解,其基本原理就是自定义类加载器,然后定义加载路径,本质上就是重写getClassFile方法,指定为开发人员自定义的路径,下面是一个简单的例子:
public class OrderClassLoader extends ClassLoader
private String fileName;

public OrderClassLoader(String fileName)
this.fileName = fileName;


//其它代码省去了,注意这里的fileName路径;
private String getClassFile(String name)
StringBuffer sb = new StringBuffer(fileName);
name = name.replace(., File.separatorChar) + ".class";
sb.append(File.separator + name);
return sb.toString();



public class ClassLoaderTest
public static void main(String[] args) throws ClassNotFoundException
geym.zbase.ch10.brkparent.OrderClassLoader myLoader=new geym.zbase.ch10.brkparent.OrderClassLoader("D:/tmp/clz/");
Class clz=myLoader.loadClass("geym.zbase.ch10.brkparent.DemoA");
System.out.println(clz.getClassLoader());

System.out.println("==== Class Loader Tree ====");
ClassLoader cl=clz.getClassLoader();
while(cl!=null)
System.out.println(cl);
cl=cl.getParent();



I cant load the class:geym.zbase.ch10.brkparent.DemoA need help from parent
sun.misc.Launcher$AppClassLoader@18b4aac2
==== Class Loader Tree ====
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@66d3c617
java.io.FileNotFoundException: D:/tmp/clz/geym/zbase/ch10/brkparent/DemoA.class (No such file or directory)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.< init> (FileInputStream.java:138)
at java.io.FileInputStream.< init> (FileInputStream.java:93)
at geym.zbase.ch10.brkparent.OrderClassLoader.findClass(OrderClassLoader.java:41)
at geym.zbase.ch10.brkparent.OrderClassLoader.loadClass(OrderClassLoader.java:28)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at geym.zbase.ch10.brkparent.ClassLoaderTest.main(ClassLoaderTest.java:11)

双亲委派应用-热加载原理就是必须用自定义的加载器加载需要被替换的类。这样手工替换了.class文件后就会自动加载新类了。这时字节码ASM技术就有用处了。这是一个很大的课题,后续会详细介绍,本节只是讲述下概念,热加载技术在不停机线上问题排查、以及租户系统中用到的比较多,甚至扩展后还可以用于线程隔离。
2、class文件检验器
和class loader一样,class文件检验器保证装载的class文件内容有正确的内部结构,并且这些class文件相互间协调一致,如果class文件中出现问题就会抛出异常。它实现的安全目标之一就是程序的健壮性,检验的过程由四次独立的扫描来完成。
第一次:class的结构检查      类加载时发生,同时也会分配内存空间。第一次扫描主要是从整体上确认class文件的合理性,比如每个class必须以四个同样的字节开始(0xCAFEBABE);class的文件长度是否正确,因为程序中每个标识都有一个默认的长度,检验器可以通过计算得到正确的长度,如果人为在class文件后加入一些非法程序就会造成长度不相符。
第二次:类型数据的语义检查      连接过程中检查(类加载过程中的一步,后续讲到连接模型时会详细说明),主要是确认每个方法描述符都是符合特定方法的、格式正确的字符串。class必须遵循java的class文件的固定格式才能被编译成在方法区中的内部数据结构。
第三次:字节码验证      连接过程中检查,结构检查称为“字节码验证”,第三次扫描是对字节码进行数据流的分析,这此字节码代码的是java类的方法,它是由操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数,它是java VM执行操作码指令所需的数据。每一个方法调用都获得一个自己的堆栈帧--内存片断,用于存储局部变量和中间结果。字节码验证不会验证所有的安全问题,因为这样的程序会遇到一个“停机”问题,程序实现不了。java VM只会检验一部分字节码。
        通过前三次扫描可以保证导入的class文件的构成合理,否则会抛出一个错误。前面三步读者只要了解即可,并不能人为的干预,第四次检查是一些底层框架扩展的关键,比如热加载等。
第四次:符号引用的验证      动态连接时检查,动态边接就是把符号引用解析为指针引用的过程,这个指标会被缓存起来,以防止二次加载。这也是基于方法的,异常时会抛出NoSushMethodError。java VM一般都是延迟加载类的,只在调用时才加载。这里也涉及到了一个编译后的class类再修改的二进制兼容的问题。如果类之间相互引用,在修改一个类时重新编译时就会存在这种问题。java规范中也列出了哪些修改操作会导致兼容问题。
3、java VM中内置的安全特性
这些安全特性是在运行期发生作用的,比如类型安全的引用转换、结构化的内存访问,无指针算法,防止了内存的恶意修改、自动垃圾收集、数组边界的检查、空引用检查等、异常(抛出一个异常总是会导致当前线程死亡,用一种结构化的错误处理方式中止线程,防止系统崩溃)。
如果直接调用了本地方法就会破坏沙箱的保护功能的(java本身对本地方法的调用是不做检查的),因此为每个类加载器设计了一个安全管理器(默认时程序是不安装安全管理器的)。它有一个check方法用来确定一个程序是否允许装载新的动态连接库,这个方法是调用动态连接库必须的。
4、安全管理器和Java API
前三种安全是为了保证JVM内部的完整性,不被外部程序破坏。但是安全管理器是一个相反的过程,防止内部的API破坏外部环境,但这需要人为来开发。安全管理器java.lang.SecurityManager是一个单独的对象。访问控制器是可以定制的。它定义了sandbox的外部边界。在java api在执行敏感操作时都会调用SecurityManager中相应的方法来询问是否安全。java默认的安全策略配置文件  $JAVA_HOME/jre/lib/security/java.policy,启动方式来两种:命令和编码
System.setSecurityManager(new SecurityManager());
-Djava.security.manager -Djava.security.policy="E:/java.policy"

Application在启动时,它还没有访问控制器。但可以通过把java.lang.SecurityManager的子类传给System.setSecurityManager()来进行安装。当java API请求不安全的操作时,会向访问控制器请求许可,如果一个动作被禁止,会抛出一个安全异常。JVM中的默认实现一般可以满足大部分的需要,称为访问控制器AccessController。使用访问控制器时一般会涉及到代码签名。
三、网络移动性JINI(即揷即用)通过对象序列化和远程方法调用(RMI),可以打破java VM之间的界限。分布式对象模型使得一个VM中的对象可以引用另一个VM中的对象,调用那些远程对象的方法,在虚拟机之间把对象当作参数、返回值或方法调用抛出的意外来交换。这种由java底层基于网络的体系结构所带来的能力,可以简化设计分布式系统的任务,因为它们有效地把OOP带入了网络。
JINI是一系列协议和API的集合,可以支持分布式系统的编写和部署,JINI是以“查找服务”为中心的,其他的服务在“查找服务”中注册,这是通过在对象间传递一种特殊的对象来完成的。JINI的思维是以“查找服务”为中心的。JINI提供了一个运行时基础结构,这种基础结构采用一种称为“探索”的网络级协议,以及两种对象级协议--“加入”和“查找”。
四、编译模式目前java有两种编译方式:1、javac使用时编译,编译执行模式;2、jit运行时编译,解释执行模式;可以用java -version查看JVM的模式。javac的类路径是com.sun.tools.Javac.Main,这个类负责生成.class文件
MacBook:~ liudong$ java -version
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode) --这行内容指定了编译模式

混合模式是指混用jit和javac。这是根据函数是否热点来区分的。可以用参数来控制运行模式:-Xint解释执行模式;-Xcomp编译执行模式,后者效率更高;-Xmixed混模式;热点阀值默认为10000,可用-XX:CompileThreshold来设置;编译一共有7个阶段(八股文内容,但最好理解下,因为ASM内容就是在下面的第3步实施并人为修改第1步的内容来实现的):
  1. parse:读取.java源文件,生成AST语法树,然后做词法和语法分析
  2. enter:生成符号表,由标识符、类型、作用域等信息
  3. process:处理注解
  4. attr:检查语义合法性、常量折叠,处理方法返回值等信息
  5. flow:数据流分析,编译期的校验
  6. desuger:去除语法糖,比如泛型、基础类型的包装以及类似的try-resources实现等的转换;
  7. generate:生成字节码
C1和C2编译器
-client用的是C1,-server用的是C2。C1编译快,C2主要是针对代码质量优化,运行速度更快。JVM为了权衡设计了一个叫多级编译器的策略,只在server模式下生效;可用XX:+TieredComplilation参数打开:但这个过程一般由JVM来自动处理,不需要人为设置。
  • 0级:解析执行,不采集性能数据;
  • 1级:简单C1,简单的快速编译,根据需要采集性能数据;
  • 2级:有限C1,更多的优化编译代码;
  • 3级:完全C1,更多的编译优化;
  • 4级:C2,完全优化;
五、代码缓存字节码被编译为机器码后,会保存在-XX:ReservedCodeCacheSize指定的内存空间内 。这个区被占满后不会抛出异常,只会停止使用JIT编译,一旦停止后就不会再开启了,其空间受GC管辖。
本章小结【jvm专题 - 体系结构】

    推荐阅读