文章目录
一、JMV的位置
二、JVM的体系结构(简单)
堆区: java 堆(java heap)是在虚拟机启动时建立的,是虚拟机所管理的内存中最大的一块,且不需要联系的存储空间,但是要求逻辑上时连续的。java 堆是被 java 所有线程所共享的内存区域。java 堆的目的是为了存放 java 的对象实例,几乎所有的 java 对象实例都会在 java 堆中分配内存。java 堆是垃圾搜集器管理的主要的区域,因此也被成为 GC 堆。
方法区: 方法区域堆区一样,也是 java 中所有线程所共享的,方法区中主要用户存储已经被虚拟机加载的类信息、常量、静态变量以及一些编辑器编译后的代码。
Java 虚拟机规范对这个区域的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但不是说数据进入了方法区就“永久”存在了。这个区域的内存回收目标主要是针对 常量池的回收 和对 类型的卸载
。根据 Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError
异常。
程序计数器: 我们知道 CPU 的程序计数器(PC)是一个物理设备,而jvm的程序计数器是一块比较小的内存空间,每一个线程有独立的一个程序计数器。它是当前线程字节码执行指令指示器,在java的概念模型中,字节码解释器就是通过改变这个程序计数器中的值来获取下一条执行的字节码指令。字节码解释器的程序控制流的指示器,分支,线程恢复等功能都依赖于这个计数器。
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
栈区: 与程序计数器一样,栈区也是线程私有的区域,且其生命周期与线程相同。我们知道,当执行一段程序时,会从上往下进行压栈处理,执行之后一次出栈处理。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度
,将抛出StackOverflowError
异常(比如:错误的递归方法);如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存
时会抛出OutOfMemoryError
异常。
本地方法栈: 本地方法栈(Native Method Stacks)与 虚拟机栈 所发挥的作用是非常相似的,其区别不过是 虚拟机栈 执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。与 虚拟机栈 一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
三、类加载器
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象.
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
1、加载过程:
加载——> 连接(验证->准备->解析)——> 初始化
① 加载
示意图(加载):
这个过程主要是通过类的全限定名,例如 java.lang.String 这样带上包路径的类名,获取到字节码文件;然后将这个字节码文件代表的静态存储结构(可简单理解为对象创建的模板)存在方法区,并在堆中生成一个代表此类的 Class 类型的对象,作为访问方法区中“模板”的入口,往后创建对象的时候就按照这个模板创建。
上述提到的“模板”,指的是无论你有多少个初始化对象,其class只有一个,或者说,它们的Class都是相同的,例如:
② 连接
验证:验证过程主要确保被加载的类的正确性。
其主要包括四种验证:
文件格式验证:主要是验证字节流是否符合Class文件格式规范,以至于可以被当前的虚拟机加载处理。
元数据验证:对class文件的字节码进行语义分析,判断其是否符合java的语法规范
字节码验证:可以说是最重要的验证,分析数据流和控制确定语义是否是符合的、符合逻辑的,主要是基于元数据的验证后对方法方法体的验证。保证运行时不会出现危害
符合引用验证:主要是针对符合引用转换为直接引用的时候,确保引用一定会被访问到,避免类出现无法访问的情况。
准备:
这个阶段主要是给类变量(静态变量)分配方法区的内存并初始化。实例变量不是在这个阶段分配内存,实例变量是随着对象一起分配在堆中。
给静态变量设置初始值为0或null,例如 public static int num = 2
这里并不是马上给 num 分配 2,而是先分配 0,对于引用数据类型设置为 null。值得注意的是,对于 final 类型,必须在程序中给其赋值,系统不会对 final 的数据进行初始化 。
③ 初始化
此时是给静态变量附初始值,例如 public static int num = 2
此时就是给 num 赋值为 2
2、类加载器
示意图:
① 启动类(根)加载器:
启动类加载器是由C/C++写的,主要负责加载 jre\lib 目录下的类
② 扩展类加载器:
扩展类加载器主要负责加载 jre\lib\ext 目录下的类
③ 应用程序类加载器:
应用程序类加载器主要负责加载我们自己编写的类
对于上述的三个加载器,我们可以在java代码中进行演示
④ 自定义类加载器
我们自己写的继承了ClassLoader
的自定义类加载器
四、双亲委派机制
保证 JVM 不被加载的代码破坏
双亲委派机制是一种安全机制,,即当要加载一个类时,先由父类加载器加载,当父类加载器没办法加载时,才由下面的加载器加载。
基本过程如下:当加载一个类的时候,由 应用程序类加载器
开始,判断 是否加载过
,加载过则结束,未加载者上浮到 扩展类下载器
,判断如上。会到 启动类加载器
加载时,当 启动类加载器
中没有对应的类时,会下沉到 扩展类加载器
,一次往下… 。
示意图(双亲委派机制):
五、沙箱安全机制
保证机器资源不被 JVM 里的程序破坏(类似于一种查杀病毒机制)
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。
沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问
,通过这样的措施来保证 对代码的有效隔离,防止对本地系统造成破坏
。
六、Native
简介
一个 Native Method 就是一个 java 调用非 java 代码的接口,Native Method 是由非 java 语言实现(一般是由 C/C++ 实现的)。
当java中的 native 方法被调用时,jvm 中有一个 “本地方法栈” 的结构,本地方法栈的会调用 “本地方法接口”,本地方法结构会调用 “本地方法库”。
为什么要有native?
Java与Java外的环境交互
这是本地方法存在的主要原因;例如,Java与一些底层系统如操作系统或某些硬件交换信息;
native方法提供了一个非常简洁的接口,而无需了解Java应用之外的细节;
与操作系统交互
JVM 支持 Java 语言本身和运行时库。JVM 也依赖于一些底层的支持;通过使用本地方法让Java实现了jre与地层系统的交互,使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。例如 java 有 Robot 类,其中就会用到很多 native 关键字。
七、 对象实例化过程
八、堆
堆结构(简图):
在 java( jdk1.8 + )中,堆分为元空间、新生代(伊甸园代、幸存区0、幸存区1)和老年代。实例化的对象是存储在新生代(伊甸园代、幸存区-from、幸存区-to)和老年代中的。
其中元空间又时会被成为 非堆,之所以元空间被称之为“非堆”,是因为元空间有逻辑上存在,物理上不存在 的特性。证明如下:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
:设置初始化分配内存为1024MB ;最大的使用内存1024MB; 打印GC细节。
我们可以发现通过java程序获取的程序运行时最大的使用内存为 981.5MB,总的内存也是 981.5MB(虽然我们设置的1024MB,但是java不会计算的那么准,有误差很正常)。
但是通过输出的内容,我们可以发现,我们在没有加 元空间(Matespace)的情况下,只是 新生代(PSYoungGen)和 养老代(ParOldGen)就已经达到的最大使用空间和总的使用空间。所以上面会说 元空间是逻辑上存在,物理上不存在
。这下应该清楚了吧。
九、GC算法
1、复制算法
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC)
2、标记清除算法
将堆中的 “死对象” 进行标记,然后进行清除,在这个过程中不会动任何的 “活对象”,这样的话就会导致很多的 “活对象” 是零散分布的。
3、标记清除压缩算法
标记清除压缩算法 是在 标记清除算法 的基础上进行优化的算法,主要是解决了在清除 “死对象” 之后导致 “活对象零散分布的问题”。
4、分代收集算法(新生代的GC+老年代的GC)
当前商业虚拟机的GC都是采用的 “分代收集算法”,这并不是什么新的思想,只是根据对象的存活周期的不同将内存划分为几块儿。一般是把 Java 堆分为 新生代 和 老年代:短命对象归为新生代,长命对象归为老年代。
少量对象存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。
大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用 “标记-清理” / “标记-整理” 算法进行GC。