Java体系图
![]()
JVM详解
虚拟机与Java虚拟机
所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
- Visaual Box,VMware就属于系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台
- 程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令
JVM是什么
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
特点
- 一次编译,到处运次(跨平台)
- 自动内存管理
- 自动垃圾回收功能
字节码
我们平时所说的java字节码,指的是用java语言编写的字节码,准确的说任何能在jvm平台上执行的字节码格式都是一样的,所以应该统称为jvm字节码。
不同的编译器可以编译出相同的字节码文件,字节码文件也可以在不同的jvm上运行。
Java虚拟机与Java语言没有必然的联系,它只与特定的二进制文件格式——Class文件格式关联,Class文件中包含了Java虚拟机指令集(或者称为字节码、Bytecodes)和符号集,还有一些其他辅助信息。
Java代码执行过程
![]()
JVM的作用
![]()
JVM位置
JVM位于操作系统之上,相当于一个软件。
![]()
JVM结构
![]()
![]()
Class Loader 类加载器
类加载器的作用是加载类文件到内存,比如编写一个HelloWord.java 程序,然后通过javac 编译成class 文件,那怎么才能加载到内存中被执行呢?Class Loader 承担的就是这个责任,那不可能随便建立一个.class 文件就能被加载的,Class Loader 加载的class 文件是有格式要求。
Class Loader 只管加载,只要符合文件结构就加载,至于说能不能运行,则不是它负责的,那是由Execution Engine 负责的。
![]()
有哪些类加载器
![]()
双亲委派机制
![]()
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。那么有人就有下面这种疑问了?
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
![]()
沙箱安全机制
什么是沙箱安全机制
java安全模型的核心就是java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。
沙箱机制就是将java代码限定在虚拟机的特定运行范围中,并且严格限定代码对本地系统资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏,沙箱主要限制系统资源访问,系统资源包括
CPU、内存、文件系统、网络。不同级别的沙箱对这些资源的访问限制也不一样。
所以的java程序都可以指定沙箱,可以定制安全策略。
安全模型
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示
![]()
1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示
![]()
JDK1.1安全模型
在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示
![]()
JDK1.2安全模型
当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示
![]()
沙箱的基本组件
字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
类装载器:其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效
存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
Execution Engine 执行引擎
执行引擎也叫做解释器(Interpreter) ,负责解释命令,提交操作系统执行。
Native Interface 本地接口
本地接口的作用是融合不同的编程语言为Java 所用,它的初衷是融合C/C++ 程序,Java 诞生的时候是C/C++ 横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++ 程序,于是就在内存中专门开辟了一块区域处理标记为native 的代码,它的具体做法是Native Method Stack 中登记native 方法,在Execution Engine 执行时加载native libraies 。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java 程序驱动打印机,或者Java 系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket 通信,也可以使用Web Service 等等,不多做介绍。
![]()
PC寄存器
程序计数器:Program Counter Register
每个程序都有一个程序计数器,都是线程私有的,就是一个指针,指向方法区中的字节码(用来存储指向对象一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令时,是一个非常小的内存空间,几乎可以忽略不计。
方法区
Method Area 方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间。 静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
介绍方法区不得不说的就是运行时常量池,它是方法区的一部分。Class文件中除了有类、字段、方法、接口等描述信息外,还有一个信息是常量池,常量池主要用来存放编译期生成的各种字面量和符号引用,这部分的内容将在类加载后存放到运行时常量池中。
运行时常量池相比于Class文件常量池的一个重要特性就是具备动态性,Java并不要求常量一定只能在编译其产生,也可以在运行期间将新的常量放入常量池中,这种特性最明显的使用方法就是String.intern方法。
许多熟悉GC的程序员们更喜欢把方法区称之为永久代,但是本质上它俩其实是不等价的,仅仅是因为HotSpot虚拟机的设计团队将GC分代收集扩展到了方法区。相对来说,垃圾收集行为在这个区域是比较少出现的,这个区域的垃圾收集目标主要是针对常量池的回收和类型的卸载。
Stack 栈
栈是什么?
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
栈帧中主要保存3 类数据:
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
- 栈操作(Operand Stack):记录出栈、入栈的操作;
- 栈帧数据(Frame Data):包括类文件、方法等等。
栈运行原理:
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
遵循“先进后出”/“后进先出”原则。
Heap 堆
三种最主要的JVM:
- Sun公司的HotSpot 是目前使用范围最广的Java虚拟机。
- BEA公司的JRockit(原来的 Bea JRockit)电脑软件,系列产品是一个全面的Java运行时解决方案组合。
- IBM公司的J9 VM 是一个高性能的企业级 Java 虚拟机。
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存在逻辑上分为三部分:
![]()
注意:伊甸区:0区:1区 = 8 :1 :1(比例大小)
新生区:
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再次垃圾回收,满足条件后再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
假设0区就是 from space;1区就是 to space:
** Minor GC**:将from区和伊甸园区中不再被其他对象所引用的对象进行销毁,把伊甸园区和from区中的幸存者从都移动到to 区,此时伊甸园区和from区变为空,然后把form区和to区相互转换,所以to区总为空。
每移动一次,幸存者的年龄就加一,当年龄大于15的时候,再将幸存者移动到养老区。
养老区:
满足条件后再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。
原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
永久区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
Jdk1.6及之前: 有永久代, 常量池1.6在方法区
Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后: 无永久代,常量池1.8在元空间 >
JDK 1.8之后将最初的永久代取消了,由元空间取代。
![]()
1.7演示
![]()
1.8演示
![]()
使用JProfiler工具分析OOM原因
下载 Jprofiler 安装
在idea中加入 Jprofiler 插件
加入配置jvm参数
# 设置堆内存为1m 最大堆内存2m 如果堆发生OOM 就生成一个dump文件 放在f盘下
-Xms1m -Xmx2m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=F://heapdump.hprof
![]()
用Jprofiler 打开heapdump.hprof
一般看Biggest Object
Gc算法
待续------