原创

初探jvm

JVM体系结构图

img

img

**方法区(Method Area):**方法区是各个线程共享的内存区域;方法区用于存储已被虚拟机加载的类的模板信息、常量、静态变量、运行时常量池;虽然Java虚拟机规范把方法区描述为堆的一部分,但是他还有个别名叫做Non-heap(非堆),目的应该是与Java堆区分开来;根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常;相对而言,垃圾收集在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样永久存在了。这区域的内存回收目标重要是针对常量池的回收和类型的卸载。 方法区只是一个规范:

  • 在HotSpot虚拟机上开发、部署程序我们把方法区称为“永久代”(Permanent Generation);
  • 虚拟机(如 BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。
  • HotSpot虚拟机在JKD.8中已经没有方法区的概念了,他使用元空间代替该区域

**2、PC寄存器(程序计数器):**每个线程都有一个程序计数器,是线程私有的;就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,既将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记;它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。如果执行的是一个Native方法,那这个计数器是空的;用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出OOM错误

3、虚拟机栈(Vm Stack)   栈也叫栈内存,主管 Java 程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就仔放,对于栈来说不存在垃圾回收问题,只要线程结束该栈就释放,生命周期和线程一致,是线程私有的。8种基木类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。

**栈的运行原理:**栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法( Method )和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 Fl ,并被压入到栈中, A方法又调用了B方法,于是产生栈帧 F2 也被压入栈,B方法又调用了C方法,于是产生栈帧 F3 也被压入栈,执行完毕后,先弹出 F3 栈帧,再弹出 F2 栈帧,再弹出 Fl 栈帧 以此类推, 遵循“先进后出” / “后进先出”原则。每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在 256K~1024K 之间, 1M 左右。

JVM栈的特点:

  • 局部变量表所需的内存空间在编译期间完成内存分配。当进入一个方法时,这个方法需要在帧中分配多大的内存空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  • 在Java虚拟机规范中,对这个区域规定了两种异常状态:如果线程请求的栈的深度大于虚拟机允许的深度,将抛出StackOverFlowError异常(栈溢出);如果虚拟机栈可以动态扩展(现在大部分Java虚拟机都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的java虚拟机栈),如果扩展时无法申请到足够的内存空间,就会抛出OutOfMemoryError异常(没有足够的内存)。

**4、本地方法栈(Native Method Stacks):**与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地Native方法服务‘;在虚拟机规范中对本地方法栈中的使用方法、语言、数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(例如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。本地方法栈也会抛出StackOverFlowError和OutOfmMemoryError异常。与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。(栈的空间大小远远小于堆)

  • 本地方法栈是 JVM 运行时数据区的一部分,用于管理本地方法调用。
  • 线程私有
  • 并非所有 JVM 中都有实现, 在 Hotspot VM 中直接将本地方法栈与虚拟机栈合二为一

5、Java堆(Java Heap):是Java虚拟机管理内存中的最大一块;Java堆是所有线程共享的一块内存管理区域。此内存区域唯一目的就是存放对象的实例,几乎所有对象实例都在堆中分配内存。这一点在Java虚拟机规范中的描述是:所有对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也不是变的那么“绝对”了

类加载器

类装载器(ClassLoader)负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

JVM中提供了三层的ClassLoader:

Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。

ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。

AppClassLoader:主要负责加载应用程序的主函数类

双亲委派机制

img

当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。那么有人就有下面这种疑问了?

为什么要设计这种机制? 这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

沙箱机制

什么是沙箱?

  Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

  所有的Java程序运行都可以指定沙箱,可以定制安全策略。

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程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。

  • 类装载器
    

    (class loader):其中类装载器在3个方面对Java沙箱起作用

    • 它防止恶意代码去干涉善意的代码;
    • 它守护了被信任的类库边界;
    • 它将代码归入保护域,确定了代码可以进行哪些操作。

  虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。

  类装载器采用的机制是双亲委派模式。

  1. 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
  2. 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
  • 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

  • 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。

  • 安全软件包
    

    (security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:

    • 安全提供者
    • 消息摘要
    • 数字签名
    • 加密
    • 鉴别
JVM
  • 作者:yangfeng(联系作者)
  • 发表时间:2025-03-18 22:44
  • 版权声明:非商用-保持署名-转载请注明来源
  • 留言评论

    留言