
JVM体系结构
JVM完整架构图
JVM简图

Java程序执行过程:java文件编编译 —> class字节码文件 —> 类加载器 —> 运行时数据区;
其中:
- Java栈、本地方法栈和程序计数器不能进行垃圾回收;
- JVM调优一般指的是调优方法区和堆
类加载器
分类
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
- 用户自定义类加载器
类加载器加载的文件
Bootstrap ClassLoader (启动类加载器) :主要负责加载Java核心类库,目录:/lib下的rt.jar、resources.jar、charsets.jar和class等;
Extention ClassLoader(扩展类加载器):主要负责加载目录/jre/lib/ext目录下的jar包文件和class文件;
Appliation ClassLoader(应用程序类加载器):主要负责加载当前应用的classpath下的所有类;
User ClassLoader(用户自定义类加载器):科技在指定路径的class文件。
自定义一个类,查看其加载器
1 | public class Student { |
1 | sun.misc.Launcher$AppClassLoader@18b4aac2 |
自定义类的类加载器为AppClassLoader
应用程序类加载器。
通过getParent()
,探索类加载器
1 | public class ClassLoader学习 { |
双亲委派机制
介绍
当一个类加载器收到了类加载的请求,它不会直接去加载指定的类, 而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。如果都不能加载,就会触发findclass
,抛出异常classNotFoundException
。
- 类加载器收到类加载请求;
- 将请求委托给父类加载器去完成;
- 启动加载器检查是否能够加载当前这个类,能加载则加载,使用当前的加载器;若不能,则通知子加载器去加载这个类;
- 若都不能加载,则抛出异常
classNotFoundException
。
存在的意义
- 通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某个类时,子加载器就不会再重新加载这个类;
- 通过双亲委派的方式,保证了安全性。
Bootstrap ClassLoader
在加载的时候,只会加载JAVA_HOME中jar包里面的类,如java.lang.Integer
,那么这个类是不会被随意替换的,除非有人故意破坏JDK。就可以避免有自人定义一个有破坏功能的java.lang.Integer
类被加载。这样可以有效方式核心Java API被篡改。
尝试定义一个String类,并运行使用。
1 | public class String { |
1 | 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: |
Application
-> Extention
-> Bootstrap
执行过程中,依据上面的顺序依次寻找String的类,优先执行Bootstrap,然后Extention
,最后Application。
由于String在BOOT中找到String的类,所以运行BOOT中的String类;
而BOOT中的String类没有main方法,所以出现错误。
沙箱安全机制
Java安全模型的核心是Java沙箱(sandbox)。
介绍
沙箱是一个限制程序运行的环境,沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样的错误来保证代码的有效隔离,防止对本地系统造成破坏。
Java沙箱主要限制系统资源访问。
历史
在Java中,执行程序分为本地代码和远程代码两种。
本地代码默认视为可信任的,而远程代码被看做是不受信任的。
- 对于受信任的本地代码,可以访问一切本地资源;
- 对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(SandBox机制)。
Java1.0 安全模型
Java 1.1 安全模型
Java1.6安全模型 (目前最新的安全模型)
在当前最新的安全机制实现,引入了域(Domain)的概念,虚拟机会把所有代码加载到不同的系统域和应用域;
系统域部分专门负责与关键资源进行交互,各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。
虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限。
组成沙箱的基本组件
- 字节码校验器(bytecode verifier):确保java类文件遵循java语言规范。但并不是所有的类文件都会经过字节码校验,比如核心类。
- 类装载器(class loader):
- 防止恶意代码去干涉正常的代码 //双亲委派机制
- 守护了被信任的类库边界;//双亲委派机制
- 代码归入保护域,确定了代码可以进行哪些操作。 //沙箱安全机制
类加载器采用双亲委派模式
通过包区分了访问域,外层恶意的类通过内置代码无法获得权限访问到内置类,破坏代码因此无法生效。
存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
**安全管理器(security manager)**:是核心API和操作系统之间的主要接口,实现权限控制,比存取控制器优先级高。
安全软件包(security package):java.scurity下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
Native、方法区
Thread.start()简述
1 | public synchronized void start() { |
Native
Java里用native修饰的方法,不在java作用范围内,调用的是底层c语言的库。
使用native修饰的方法,会进入本地方法栈,调用JNI(本地方法接口),JNI调用本地方法库。
JNI本地方法接口:,扩展Java的使用,融合不同的编程语言为Java所用。(最初是C、C++)
Native Method Stack本地方法栈:登记Native方法。
PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也是将要执行的指令代码)。
当执行引擎读取下一条指令,PC计数器会+1。
PC寄存器是一个非常小的内存空间。(小到可以忽略不计)
方法区
Method Area
方法区被所有线程共享。
所有定义的方法的信息都保存在该区域。
属于共享区间。
内容
静态变量,常量,类信息(构造方法、接口定义),运行时常量池都在方法区中;
- static , final , Class ,常量池。
实例变量存在于堆内存中,与方法区无关。
JVM中内存分析
1 | public class JVM内存分配部分 { |
栈
持续学习:程序 = 数据结构 + 算法 饭碗:程序 = 框架 + 业务逻辑
栈:先进后出,后进先出。
队列:先进先出。(FIFO:First Input First Output)
通过main方法理解栈
1 | public class UnderstandStackByMainMethod { |
1 | main start |
程序运行:main方法开始,然后其他方法,最后main方法结束。
在栈中,main先进入栈,然后是hello()入栈,然后hello()出栈,最后main出栈。
JVM栈
栈:主管程序的运行,生命周期和线程同步(线程结束 —> 栈内存释放)。因此,对于栈来说,不存在垃圾回收问题。
介绍
JVM栈描述的是每个线程Java方法执行的内存模型:每个方法被执行的时候,JVM会同步创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
栈是运行时单位,堆是存储的单位,即:
栈解决的是运行问题,即程序如何执行,或者如何处理数据。
堆解决的事数据存储问题,即数据怎么放,放哪儿。
特点
- 访问速度快,仅次于程序计数器
- 线程私有
- 存在OOM,不存在GC
存放类型
8种数据类型、对象的引用,实例的方法。
其他
Java虚拟机栈是线程私有的,生命周期与线程相同。(随线程而生,随线程而灭。)
如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverflowError异常;
若虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)
Java虚拟机栈描述的是Java方法执行的内存模型。
堆
Heap,一个JVM只有一个堆内存,堆内存的大小可以调节。
堆里面放什么
类,方法,常量,变量,保存引用类型的指向类型(真实对象)。
分三个区域
Java 7及之前堆内存逻辑上分为3个部分:新生区 + 养老区 + 永久区
Java 8及之后堆内存逻辑上分为3个部分:新生区 + 养老区 + 元空间
- 新生区 Young Generation Space Young/New
- Eden区 伊甸区
- Survivor区 幸存区
- Tenure generation Space 养老区 Old/Tenure
- Permanent Space 永久区 Perm
- Meta Space 元空间 Meta
GC垃圾回收主要在伊甸园区和老年区。
在JDK8之后,永久存储区改为元空间。
若内存满了,会触发OOM:java.lang.OutOfMemoryError: java heap space。
新生区的内存比例: Eden : from : to = 8 : 1 : 1
新生区
Eden
新创建的对象会放在Eden区,每经历一次GC,位于Eden区存活的对象会被移到幸存区的from区。
幸存区
- 幸存区分为from和to两部分,两部分会互相交换;
- 分辨from分区和to分区:一般情况下,空的为to分区。
- 每次GC,都会把Eden中没有被清理的对象移到幸存区中。
- 一旦Eden区被GC后,一部分被清理,没有被清理的移到幸存区。
计算过程
每个对象都被定义有寿命;
当寿命达到指定值,就会被移入老年代;
永久区
这个区域常驻内存,用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境。
这个区域不存在垃圾回收关闭VM虚拟机会释放这个区域的内存。
永久区满的情况
- 一个启动类,加载了大量的第三方jar包;
- Tomcat部署了太多的应用,或者大量动态生成的反射类。
不断地加载,若内存满,就会出现OOM。
版本历史
Jdk1.6之前:永久代,常量池在方法区;
Jdk1.7:永久代,但是慢慢退化了,去永久代
,常量池在堆中;
jdk1.8之后:无永久代,常量池在元空间。
用代码表现内存溢出
VM参数:-Xms8M -Xmx8M -XX:+PrintGCDetails
1 | public class GetJVMMaxMemoery { |
1 | [GC (Allocation Failure) [PSYoungGen: 1536K->488K(2048K)] 1536K->716K(7680K), 0.0007954 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
JProfiler工具
- 分许Dump内存文件,快速定位内存泄露。
- …
其他
Java程序运行的部分参数
-Xms 初始化分配内存大小 -Xms1G、-Xms512M
-Xmx 设置最大分配内存 -Xmx1024M、-Xmx2G
-XX:PrintGCDetails 输出内存分配
-XX:HeapDumpOnOutOfMemoryError 输出栈溢出信息
-XX:MaxTenuringThreshold = 15 通过这个参数可以设定对象经过GC多少次仍然存活后晋升到老年代的最大阈值。默认:15。
Java代码校验过程
java代码首先会经过编译器校验,然后在解释器解释前会被字节码校验器校验。
对于字节码校验器,运行的代码可能来自本地计算机,也有可能来自远程计算机,本地计算机的代码经过编译器校验,但远程计算机的代码不一定被编译器校验,所以解释前必须经过字节码校验器再次校验。
来源
https://www.oracle.com/java/technologies/security-in-java.html
What about the concept of a “hostile compiler”? Although the Java compiler ensures that Java source code doesn’t violate the safety rules, when an application such as the HotJava Browser imports a code fragment from anywhere, it doesn’t actually know if code fragments follow Java language rules for safety: the code may not have been produced by a known-to-be trustworthy Java compiler. In such a case, how is the Java run-time system on your machine to trust the incoming bytecode stream? The answer is simple: the Java run-time system doesn’t trust the incoming code, but subjects it to bytecode verification.
The tests range from simple verification that the format of a code fragment is correct, to passing each code fragment through a simple theorem prover to establish that it plays by the rules:
- it doesn’t forge pointers,
- it doesn’t violate access restrictions,
- it accesses objects as what they are (for example,
InputStream
objects are always used asInputStream
s and never as anything else).
A language that is safe, plus run-time verification of generated code, establishes a base set of guarantees that interfaces cannot be violated.
不准确的翻译
“恶意编译器”是什么?即使Java编译器确保Java源代码不会避开安全规则,但当代码片段导入来自像HotJava这样的浏览器,不能保证代码片段在安全上是否遵循Java语言规则:代码可能被不被信任的Java编译器编译。因此,电脑上的Java运行时系统如何相信输入的字节流?答案很简单:Java运行时系统不信任输入的代码,而是把它交给字节码校验器。
首先进行简单的代码格式正确性验证,然后每段代码都要通过一个简易的规则检测器,确保符合以下规则:
- 不会伪造指针
- 不违反访问显示
- 按原样访问对象(例如InputStream对象仅仅被访问作InputStream而不是其他的东西)
个人理解
交给Java解释器的代码,可以来自可信的Java编译器,也有可能来自恶意的Java编译器。为保证安全,在Java解释器之前,字节码校验器会对代码再次(在此之前是Java编译器)进行校验,确保不会有不合适的代码被运行。
- 没有伪造指针
- 不会违反访问限制
- 访问对象正确(例如InputStream对象仅仅被访问作InputStream而不是其他的东西)
Java的Robot类:java.awt.Robot
Java中Robot类位于java.awt.Robot,该类用于为测试自动化,自运行演示程序和其他需要控制鼠标和键盘的应用程序生成本机系统输入事件,Robot类的主要目的是便于Java平台实现自动测试。
JNI 本地方法接口
Java Native Interface
三种JVM
- Sun公司 HotSpot
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
- BEA JRockit
- IBM J9VM
学习用主要是HotSpot
一些问题
请谈谈你对
core
- 请你谈谈你对JVM的理解?Java8虚拟机和之前的变化更新?
- 什么是OOM,什么是栈溢出
StackOverFlowError
?怎么分析? - JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?
- 谈谈JVM中,类加载器的你的认识?
JVM
- JVM的内存模型和分区 ,每个区放什么?
- 堆里面的分区有哪些?Eden、from、to,老年区,说说他们的特点。
- GC的算法有哪些?标记清除法,标记整理,复制算法,引用计数器,如何使用?
- 轻GC 和 重GC 分别在什么时候发生?
知识点
JVM位置
JVM体系结构
类加载器
双亲委派机制
沙箱安全机制
Native
PC寄存器
方法区
三种JVM
堆
新生区老年区
堆内存调优
GC
常用算法
JMM
总结