一、什么是JVM
jvm全称是JVM(Java Virtual Machine,Java虚拟机),JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。 JVM的作用:为java程序提供一个可以运行的环境;Java程序的跨平台特性主要就是因为JVM实现的。在编译java程序时会将写好的源程序通过编译器编译生成.class文件(又称为字节码文件),之后就是通过JVM内部的解释器将字节码文件解释成为具体平台上的机器指令执行,所以就可以实现java程序的跨平台特性。
二、JVM的内部体系结构
JVM内部体系结构大致分为三部分:类装载器(ClassLoader)子系统,运行时数据区和执行引擎。 如下图所示: 1、类加载器 作用:加载 .class文件;类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。 JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器: (1)根类加载器(bootstrap class loader) 它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由**C++**实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。 (2)扩展类加载器(extensions class loader) 它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包中的类。由Java语言实现,父类加载器为null。 (3)系统类加载器(system class loader) 被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
2、双亲委派机制 双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。 (1)双亲委派机制作用:
- 保证安全,java核心api中定义类型不会被随意替换;
- 通过层级关系避免类的重复加载
(2)执行顺序为:应用程序加载器=>扩展类加载器=>根加载器(最终执行);根加载器检查是否能够加载当前类,如果可以加载,就结束,不能就会抛出异常,通知子加载器进行加载。如果到了应用程序加载器还没找到,就会报错(Class Not Found);
3、沙箱安全机制 (1)什么是沙箱? Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。 (2)java中的安全模型 在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示 JDK1.0安全模型: 但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示 JDK1.1安全模型: 在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示 JDK1.2安全模型: 当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示 最新的安全模型(jdk 1.6) (3)组成沙箱的基本组件
- 字节码校验器
确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。 - 类装载器
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。 类装载器在三个方面对沙箱起作用: - 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
4、native关键字
- 凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!
- 如图所示,会进入本地方法栈,然后调用本地方法本地接口JNI
- JNI的作用:扩展java的使用,融合不同的编程语言为java所用!最初想融合:c,c++
- Java在内存中专门开辟了一块标记区域:本地方法栈(Native Method Stack)登记native方法
- 在最终执行过程中加载本地方法库中的方法通过JNI
5、程序计数器 内存空间小,线程私有。指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令;字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。 注意:如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
6、方法区 属于所有线程所共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。简单来说,所有定义的方法的信息都保存在该区域。比如构造函数,接口代码; 注意:运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
7、Java 虚拟机栈
- 线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
- 线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题
- 一旦线程结束,栈也就结束了
- 栈帧:八大类型((boolean、byte、char、short、int、float、long、double))+对象引用+实例的方法
注意: StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。 OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
8、堆 Heap:一个JVM只有一个堆内存,堆内存的大小是可以调节的;线程共享,主要是存放对象实例和数组。类加载器读取了类文件后,一般会把类的常量、方法、变量放到堆中,保存我们所有引用类型的真实对象。 堆内存中还要细分为以下三个区域:
- 新生区:对象诞生和成长的地方,甚至死亡;
新生区可细分为: 1)伊甸园区.所有对象都是在伊甸园区new出来的; 2)幸存者区(0,1) - 老年区:老年区:新生区进行轻gc(垃圾回收机制)活下来的对象
- 永久区:这个区域是常驻内存的,存放JDK自身携带的Class对象.interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域的内存。
注意: 1)jdk 1.6 之前:永久代,常量池是在方法区 2)jdk 1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中 3)jdk 1.8之后:无永久代,常量池在元空间(逻辑上存在,物理上不存在,因为元空间直接在内存,不受堆空间的限制) 通过上图可知为什么元空间逻辑上存在,物理上不存在。 注意: - GC垃圾回收,主要是工作在养老区和伊甸区。
- 内存满了就会出现OOM,堆内存不够!
- 在JDK8以后,永久存储区改了个名字(元空间).
9、简单类对象的实例化过程 10、垃圾回收机制 程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。 注意:
- JVM在运行GC时并不是对新生代(伊甸区),幸存区,老年区统一回收,大部分时候,回收的都是新生代;
- GC两个种类:轻GC(普通GC;当伊甸园区满了发生),重GC(全局GC;当老年区满了发生);
- 每次GC,都会将伊甸园的对象转移到幸存区:一旦伊甸区被GC,那么伊甸园区就会是空的;
- 幸存区from和幸存区to的区别,记住一句话 :谁空谁是to;
- 当一个对象经历了15次GC还没有死的情况下,就好进入老年区,因为对象头只留了四位设置GC最大次数(-XX:MaxTenuringThreshold),所以只能最大值设为15。
注意:-XX:MaxTenuringThreshold(垃圾最大年龄):如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率,该参数只有在串行GC时才有效.
11、堆内存调优 OutOfMemoryError(OOM)解决措施:
- 尝试扩大堆内存看结果
- 分析内存,使用专业工具(JPofiler)看一下是否有垃圾代码或者无限循环的代码占用空间;
一直设置的指令含义:
-Xms 设置初始化内存分配大小 1/64
-Xmx 设置最大分配内存,默认1/4
-XX:+PrintGCDetails 打印GC垃圾回收信息
需要配置命令:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
查找OOM错误可以在VM中设置上述最后一条指令,并且通过JPofiler工具来分析OOM发生的原因。
12、垃圾回收算法
-
标记清除算法 从根集合出发,将能访问到的对象打上标记,然后遍历堆,将没有标记的对象清除。 优点:不会挪动对象在堆中的位置 缺点:容易产生内存碎片 -
复制算法 把堆划分为两部分,每次分配对象的时候只使用其中一半.当标记过程结束后,把存活的对象全都挪到堆的另一半中. 优点:不会产生内存碎片 缺点:浪费了内存空间 -
标记整理算法 优点:不会产生内存碎片 缺点:比标记清除算法多了一次扫描时间。多了一个移动成本。 -
分代回收算法 根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。 新生代:每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。 老年代:老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用标记 —— 清除或者标记 —— 整理算法回收。
|