JVM学习笔记
JVM的位置
JVM的体系结构
双亲委派机制
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
1 | public class Car { |
双亲委派机制: 安全
APP –> EXC –> BOOT (最终执行)
- 类加载器收到类加载的请求
- 将这个请求向上委托给父类加载器去完成,一直向上委托, 直到启动类加载器
- 启动类加载器检查是否能够加载当前这个类,能加载就结束使用当前的加载器,否则抛出异常通知子类(子加载器)进行加载
- 重复步骤3
1 | 首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。 |
沙箱安全机制
java中的安全模型:
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示 JDK1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略
,允许用户指定代码对本地资源的访问权限。如下图所示 JDK1.1安全模型
在 Java1.2 版本中,再次改进了安全机制,增加了代码签名
。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示 JDK1.2安全模型
当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示 最新的安全模型(jdk 1.6)
以上提到的都是基本的Java 安全模型概念
,在应用开发中还有一些关于安全的复杂用法
,其中最常用到的 API 就是 doPrivileged。doPrivileged 方法能够使一段受信任代码获得更大的权限,甚至比调用它的应用程序还要多,可做到临时访问更多的资源
。有时候这是非常必要的,可以应付一些特殊的应用场景。例如,应用程序可能无法直接访问某些系统资源,但这样的应用程序必须得到这些资源才能够完成功能。
组成沙箱的基本组件:
字节码校验器
(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。```
类装载器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
1. 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
2. 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
- `存取控制器`(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
- `安全管理器`(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
- ```
安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
沙箱包含的要素:
1. 权限
权限是指允许代码执行的操作。包含三部分:权限类型、权限名和允许的操作。权限类型是实现了权限的Java类名,是必需的。权限名一般就是对哪类资源进行操作的资源定位(比如一个文件名或者通配符、网络主机等),一般基于权限类型来设置,有的比如java.security.AllPermission不需要权限名。允许的操作也和权限类型对应,指定了对目标可以执行的操作行为,比如读、写等。如下面的例子:
1 | permission java.security.AllPermission; //权限类型 |
标准权限:
说明 | 类型 | 权限名 | 操作 | 例子 |
---|---|---|---|---|
文件权限 | java.io.FilePermission | 文件名(平台依赖) | 读、写、删除、执行 | 允许所有问价的读写删除执行:permission java.io.FilePermission “<< ALL FILES>>”, “read,write,delete,execute”;。允许对用户主目录的读:permission java.io.FilePermission “${user.home}/-”, “read”; |
套接字权限 | java.net.SocketPermission | 主机名:端口 | 接收、监听、连接、解析 | 允许实现所有套接字操作:permission java.net.SocketPermission “:1-”, “accept,listen,connect,resolve”;。允许建立到特定网站的连接:permission java.net.SocketPermission “.abc.com:1-”, “connect,resolve”; |
属性权限 | java.util.PropertyPermission | 需要访问的jvm属性名 | 读、写 | 读标准Java属性:permission java.util.PropertyPermission “java.”, “read”;。在sdo包中创建属性:permission java.util.PropertyPermission “sdo.”, “read,write”; |
运行时权限 | java.lang.RuntimePermission | 多种权限名[见附录A] | 无 | 允许代码初始化打印任务:permission java.lang.RuntimePermission “queuePrintJob” |
AWT权限 | java.awt.AWTPermission | 6种权限名[见附录B] | 无 | 允许代码充分使用robot类:permission java.awt.AWTPermission “createRobot”; permission java.awt.AWTPermission “readDisplayPixels”; |
网络权限 | java.net.NetPermission | 3种权限名[见附录C] | 无 | 允许安装流处理器:permission java.net.NetPermission “specifyStreamHandler”;。 |
安全权限 | java.security.SecurityPermission | 多种权限名[见附录D] | 无 | |
序列化权限 | java.io.SerializablePermission | 2种权限名[见附录E] | 无 | |
反射权限 | java.lang.reflect.ReflectPermission | suppressAccessChecks(允许利用反射检查任意类的私有变量) | 无 | |
完全权限 | java.security.AllPermission | 无(拥有执行任何操作的权限) | 无 |
2. 代码源
代码源是类所在的位置,表示为以URL地址。
3. 保护域
保护域用来组合代码源和权限,这是沙箱的基本概念。保护域就在于声明了比如由代码A可以做权限B这样的事情。
4. 策略文件
策略文件是控制沙箱的管理要素,一个策略文件包含一个或多个保护域的项。策略文件完成了代码权限的指定任务,策略文件包括全局和用户专属两种。
native
凡是带了native关键字的,说明java的作用范围达不到了,回去调用低层c语言的库,会进入本地方法栈,调用本地方法本地接口 jmi JMI的作用:拓展java的使用,融合不同的语言为java使用,如c , c++,他在内存中单独开辟了一块标记区域,本地方法栈(native methods stack),登记native方法。在最终执行的时候,加载本地方法库中的方法通过JMI
PC寄存器
程序计数器(program counter register)
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,即将要执行指令的代码),在指令引擎下读取下一条数据,是一个非常小的内存空间,几乎可以忽略不计。
方法区
method area 方法区
方法区是被所有县城管共享的,所有字段的方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间。
静态变量,常量,类信息(构造方法、接口定义)、运行时的常量池在方法区中,但是实例变量存在堆内存中,和方法区无关。
栈
栈的结构
栈:栈内存 主管程序的运行, 生命周期和线程同步;
线程结束, 栈内存释放,对栈来说,不存在垃圾回收。
栈:8大基本数据类型 + 对象引用 + 实例方法
栈满了: stackOverFlowError
栈运行单位: 栈帧
局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放,但这种描述与明确指出“每个Slot占用32位长度的内存空间”是有一些差别的,它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。只要保证即使在64位虚拟机中使用了64位的物理内存空间去实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。
一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference(Java虚拟机规范中没有明确规定reference类型的长度,它的长度与实际使用32还是64位虚拟机有关,如果是64位虚拟机,还与是否开启某些对象指针压缩的优化有关)和returnAddress 8种类型。第7种reference类型表示对一个对象实例的引用,虚拟机规范既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但一般来说,虚拟机实现至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现Java语言规范中定义的语法约束。并不是所有语言的对象引用都能满足这两点,例如C++语言,默认情况下(不开启RTTI支持的情况),就只能满足第一点,而不满足第二点。这也是为何C++中提供Java语言里很常见的反射的根本原因。第8种即returnAddress类型目前已经很少见了,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址,很古老的Java虚拟机曾经使用这几条指令来实现异常处理,现在已经由异常表代替。
对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。Java语言中明确的(reference类型则可能是32位也可能是64位)64位的数据类型只有long和double两种。值得一提的是,这里把long和double数据类型分割存储的做法与“long和double的非原子性协定”中把一次long和double数据类型读写分割为两次32位读写的做法有些类似。不过,由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot数量。如果访问的是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot。对于两个相邻的共同存放一个64位数据的两个Slot,不允许采用任何方式单独访问其中的某一个,Java虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非static的方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为
1 | public static void main(String[]args)(){ |
placeholder能否被回收的根本原因是:局部变量表中的Slot是否还存有关于placeholder数组对象的引用。代码虽然已经离开了placeholder的作用域,但在此之后,没有任何对局部变量表的读写操作(即没有int a=0这段代码),placeholder原本所占用的Slot还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。这种关联没有被及时打断,在绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存、实际上已经不会再使用的变量,手动将其设置为null值(用来代替那句int a=0,把变量对应的局部变量表Slot清空)便不见得是一个绝对无意义的操作,这种操作可以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到JIT的编译条件)下的“奇技”来使用。
关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那样存在“准备阶段”。我们已经知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为Java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为false等这样的默认值。这段代码其实并不能运行,还好编译器能在编译期间就检查到并提示这一点,即便编译能通过或者手动生成字节码的方式制造出下面代码的效果,字节码校验的时候也会被虚拟机发现而导致类加载失败。
1 | public static void main(String[]args){ |
栈 + 堆 + 方法区 :交互关系
class文件在方法区里,但是在堆里面对方法区的class信息进行了一个封装创建了一个对象
堆
heap, 一个jvm只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆内存中? 类, 方法, 常量, 变量~ 保存我们引用类型的真实对象。
堆内存中还要分为三个区域:
- 新生区(伊甸园区)edson区8,from区1,to区1
- 养老区
- 永久区
JVM类加载过程
java类加载过程:加载–>验证–>准备–>解析–>初始化,之后类就可以被使用了。绝大部分情况下是按这
样的顺序来完成类的加载全过程的。但是是有例外的地方,解析也是可以在初始化之后进行的,这是为了支持
java的运行时绑定,并且在一个阶段进行过程中也可能会激活后一个阶段,而不是等待一个阶段结束再进行后一个阶段。
1. 加载
加载时jvm做了这三件事:
1)通过一个类的全限定名来获取该类的二进制字节流
2)将这个字节流的静态存储结构转化为方法区运行时数据结构
3)在内存堆中生成一个代表该类的java.lang.Class对象,作为该类数据的访问入口
2. 验证
验证、准备、解析这三步可以看做是一个连接的过程,将类的字节码连接到JVM的运行状态之中
验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会威胁到jvm的安全
验证主要包括以下几个方面的验证:
1)文件格式的验证,验证字节流是否符合Class文件的规范,是否能被当前版本的虚拟机处理
2)元数据验证,对字节码描述的信息进行语义分析,确保符合java语言规范
3)字节码验证 通过数据流和控制流分析,确定语义是合法的,符合逻辑的
4)符号引用验证 这个校验在解析阶段发生
3. 准备
为类的静态变量分配内存,初始化为系统的初始值。对于final static修饰的变量,
直接赋值为用户的定义值。如下面的例子:这里在准备阶段过后的初始值为0,而不是7
1 | public static int a=7 |
4. 解析
解析是将常量池内的符号引用转为直接引用(如物理内存地址指针)
5. 初始化
到了初始化阶段,jvm才真正开始执行类中定义的java代码
1)初始化阶段是执行类构造器方法的过程。类构造器()方法是由编译器自动收集
类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
2)当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先触发其父类的初始化。
3)虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为例,这个指令用于整型数加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。另外,在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递,Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。