JVM浅探
参考《深入理解Java虚拟机》
1、前言
面试题:
1、请你谈谈你对JVM的理解?Java8的虚拟机有什么更新?
2、什么是OOM?什么是StackOverFlowError?有哪些方法分析?
3、JVM的常用参数调优你知道哪些?
4、内存快照抓取和MAT分析DUMP文件知道吗?
5、堆里面的分区:Eden,Survival from to,老年代,各自的特点?
6、GC的三种收集方法:标记清除,标记整理,复制算法的原理与特点,分别用在什么地方?
2、JVM概述
了解三种JVM:
Sun公司的 HotSpot
BEA公司的 JRockit
IBM公司的 J9VM
2.1、JVM的位置
2.2、JVM体系结构图
3、类加载器ClassLoader
3.1、一个类加载到 JVM 的一个基本结构
3.2、类的加载、连接与初始化
从代码理解类的加载,连接与初始化
class Test{
public static int a = 1;
}
//我们程序中给定的是 public static int a = 1;
//但是在加载过程中的步骤如下:
1. 加载阶段
编译文件为 .class文件,然后通过类加载,加载到JVM
2. 连接阶段
第一步(验证):确保Class类文件没问题
第二步(准备):先初始化为 a=0。(因为你int类型的初始值为0)
第三步(解析):将引用转换为直接引用
3. 初始化阶段:
通过此解析阶段,把1赋值为变量a
3.3、类的加载
1、Java虚拟机自带的加载器
根类加载器(BootStrap)(BootClassLoader) sun.boot.class.path (加载系统的包,包含jdk核
心库里的类)
扩展类加载器(Extension)(ExtClassLoader) java.ext.dirs(加载扩展jar包中的类)
系统(应用)类加载器(System)(AppClassLoader) java.class.path(加载你编写的类,编译后
的类)
2、用户自定义的类加载器
Java.long.ClassLoader的子类(继承),用户可以定制类的加载方式
3.4、双亲委派机制
双亲委派机制的工作原理:一层一层的 让父类去加载,最顶层父类不能加载往下数,依次类推。
- 类加载器收到类加载的请求;
- 把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;
- 启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通
知子加载器进行加载 - 重复步骤三;
3.5、Java历史,沙箱安全机制
参考文档:https://blog.csdn.net/qq_30336433/article/details/83268945
什么是沙箱?
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虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
-
存取控制器
(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。 -
安全管理器
(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。 -
安全软件包
(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
4、native、方法区
4.1、native关键字
凡是带了native关键字的,说明 java的作用范围达不到,去调用底层C语言的库!
JNI:Java Native Interface (Java本地方法接口)
凡是带了native关键字的方法就会进入本地方法栈;
Native Method Stack 本地方法栈
4.2、程序计数器PC计数器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的。
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚
拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指
令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是一个非常小
的内存空间,几乎可以忽略不计
4.3、方法区
==静态变量、常量、类信息(构造方法,接口定义)、运行时的常量池存在方法去中,但是,实例变量存在堆内存中,和方法区无关。==
static final Class 常量池
Method Area 方法区 是 Java虚拟机规范 中定义的运行时数据区域之一,它与堆(heap)一样在线程之间
共享。
JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据迁移至 Java Heap 或 Native
Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)。
元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常
量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实
现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
5、栈(Stack)
栈:先进后出/后进先出
队列:先进先出(FIFO)
栈:栈内存,主管程序的运行,生命周期和线程同步。
线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题。
一旦线程结束,栈就Over
栈:8大基本类型+对象引用+实例的方法
说明:
1、栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程
结束栈内存也就释放。
2、对于栈来说不存在垃圾回收问题,只要线程一旦结束,该栈就Over,生命周期和线程一致,是线程
私有的。
3、方法自己调自己就会导致栈溢出(递归死循环测试)
Java栈的组成元素—栈帧
栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。他是独立于线程的,一个线程有自己
的一个栈帧。封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
6、堆(Heap)
heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的
-
新生区 Young Generation Space Young/New
-
养老区 Tenure generation space Old/Tenure
-
永久区 Permanent Space Perm
堆内存逻辑上分为三部分:新生,养老,永久(元空间 : JDK8 以后名称)
GC垃圾回收主要是在 新生区和养老区,又分为 轻GC 和 重GC,如果内存不够,或者存在死循环,就会导致 java.lang.OutOfMemoryError: Java heap space
新生区、养老区
新生区是类诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分:伊甸区(Eden Space)和幸存者区(Survivor Space),所有的类都是在伊甸区
被new出来的,幸存区有两个:0区 和 1区。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾
回收器将对伊甸园区进行垃圾回收(Minor GC)。将伊甸园中的剩余对象移动到幸存0区,若幸存0区也
满了,再对该区进行垃圾回收,然后移动到1区,那如果1区也满了呢?(这里幸存0区和1区是一个互相
交替的过程)再移动到养老区,若养老区也满了,那么这个时候将产生MajorGC(Full GC),进行养老
区的内存清理,若养老区执行了Full GC后发现依然无法进行对象的保存,就会产生OOM异常
“OutOfMemoryError ”。
老年代满了才会重GC,否则都是轻GC
分代管理,不同的区域使用不同的算法:
真相:经过研究,不同对象的生命周期不同,在Java中98%的对象都是临时对象
永久区(Perm)
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,存储java运行时的一些环境,这个区域不存在垃圾回收
一个启动类,加载了大量的第三方jar包,Tomcat部署了太多的应用,大量动态生成反射类。直到内存满,就会出现OOM
- Jdk1.6之前: 有永久代,常量池1.6在方法区
- Jdk1.7: 有永久代,但是已经逐步 “去永久代”,常量池1.7在堆
- Jdk1.8及之后:无永久代,常量池1.8在元空间
堆内存调优
查看默认内存
package com.badwei.demo;
public class demo1 {
public static void main(String[] args) {
//返回Java虚拟机试图使用的最大内存量
long maxMemory = Runtime.getRuntime().maxMemory();
//返回Java虚拟机中的内存总量
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("MAX_MEMORY="+maxMemory+"(字节)、"
+(maxMemory/(double)1024/1024)+"MB");
System.out.println("TOTAL_MEMORY="+totalMemory+"(字节)、"
+(totalMemory/(double)1024/1024)+"MB");
}
}
发现,默认的情况下分配的内存是总内存的 1/4,而初始化的内存为 1/64 !
堆内存调优
-Xms :设置初始分配大小,默认为物理内存的 “1/64”
-Xmx :最大分配内存,默认为物理内存的 “1/4”
-XX:+PrintGCDetails :输出详细的GC处理日志
在vm option中输入
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
VM参数调优:把初始内存,和总内存都调为 1024M,运行,查看结果!
发现:新生代,老年代默认比例为1:2
发现:PSYoungGen+ParOldGen=305664k+699392k=981.5MB=TOTLE_MEMORY
即:新生代+老年代=JVM总内存,那元空间在哪呢?答:元空间在本地内存,不在JVM,与堆共享内存
Dump内存快照(解决OOM)
Dump文件是进程的内存镜像。
-
有 Eclipse Memory Analyzer tool(MAT)
-
在idea中也有这么一个插件,就是JProfiler,一款性能瓶颈分析工具!而且这个软件跨平台
jprofiler作用:
分析Dump文件,快速定位内存泄漏;
获得堆中对象的统计数据
获得对象相互引用的关系
采用树形展现对象间相互引用的情况
具体的jprofiler使用
https://www.cnblogs.com/jpfss/p/8488111.html
接下来我们演示基本安装使用:
1.idea安装jprofiler插件,速度慢直接去官网下载插件
https://plugins.jetbrains.com/plugin/253-jprofiler/versions
然后idea通过本地安装插件
2.下载jprofiler工具
https://www.ej-technologies.com/download/jprofiler/version_92
注册码
// 注册码仅供大家参考
L-Larry_Lau@163.com#23874-hrwpdp1sh1wrn#0620
L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038
L-Larry_Lau@163.com#99016-hli5ay1ylizjj#27215
L-Larry_Lau@163.com#40775-3wle0g1uin5c1#0674
3.配置idea,关联jprofiler
测试:
修改JVM内存【测试就把JVM内存调小一点,否则Dump文件非常大】【加入+HeapDumpOnOutOfMemoryError,表示OOM就Dump】
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
测试代码
package com.badwei.demo;
import java.util.ArrayList;
public class Demo1 {
byte[] byteArray = new byte[1*1024*1024]; //1M = 1024K
public static void main(String[] args) {
ArrayList<Demo1> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new Demo1());//问题所在
count = count + 1;
}
}catch (Error e){
System.out.println("count:"+count);
e.printStackTrace();
}
}
}
运行
然后src目录下就会有Dump的文件
我们用jprofiler打开
我们一般看Biggest Object,发现ArrayList占比89%,说明是ArrayList出错
然后我们看进程,直接发现问题所在代码行
7、GC
记住GC口诀: 分代收集算法
JVM 在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代
因此GC按照回收的区域又分了两种类型,一种是普通的GC(minor GC),一种是全局GC (major GC
or Full GC)
普通GC:只针对新生代区域的GC
全局GC:针对老年代的GC,偶尔伴随对新生代的GC以及对永久代的GC
GC面试题
1、JVM内存模型以及分区,需要详细到每个区放什么
2、堆里面的分区:Eden,Survival from to,老年代,各自的特点。
3、GC的三种收集方法:标记清除,标记整理,复制算法的原理与特点,分别用在什么地方?
4、Minor GC 与 Full GC 分别在什么时候发生?
很多的问题其实很简单,只是大家没有去研究而已,下面我们来聊聊几种垃圾回收方法!
8、GC四大算法
引用计数法,复制算法(Copying),标记清除(Mark-Sweep),标记压缩(Mark-Compact)
1.引用计数法
2.复制算法(Copying)
年轻代中使用的是Minor GC,采用的就是复制算法(Copying)
复制算法需要多一个空的幸存区。适合存活度较低的新生区
from 复制到 to
15次GC,就进入养老区
Minor GC 会把Eden中的所有活的对象都移到幸存区Survivor区域中,如果Survivor区中放不下,那么剩下的
活的对象就被移动到Old generation中,也就是说,一旦收集后,Eden就是变成空的了
当对象在Eden(包括一个Survivor区域,这里假设是From区域)出生后,在经过一次Minor GC后,如
果对象还存活,并且能够被另外一块Survivor区域所容纳 (上面已经假设为from区域,这里应为to区
域,即to区域有足够的内存空间来存储Eden 和 From 区域中存活的对象),则使用复制算法将这些仍然
还活着的对象复制到另外一块Survivor区域(即 to 区域)中,然后清理所使用过的Eden 以及Survivor
区域(即form区域),并且将这些对象的年龄设置为1,以后对象在Survivor区,每熬过一次MinorGC,就将这个对象的年龄 + 1,当这个对象的年龄达到某一个值的时候(默认是15岁,通过XX:MaxTenuringThreshold 设定参数)这些对象就会成为老年代。
在GC开始的时候,对象只会在Eden区和名为 “From” 的Survivor区,Survivor区“TO” 是空的,紧接着进
行GC,Eden区中所有存活的对象都会被复制到 “To” , 而在 “From” 区中,仍存活的对象会更具他们的
年龄值来决定去向。年龄达到一定值 的对象会被移动到老年代中,没有达到阈值的对象会被复制到 “To
区域”,经过这次GC后,Eden区和From区已经被清空,这个时候, “From” 和 “To” 会交换他们的角色,
也就是新的 “To“ 就是GC前的”From“ , 新的 ”From“ 就是上次GC前的 ”To“。不管怎样,都会保证名为
To 的Survicor区域是空的。 Minor GC会一直重复这样的过程。直到 To 区 被填满 , ”To “ 区被填满之
后,会将所有的对象移动到老年代中。
优缺点
好处:没有内存碎片,坏处:浪费内存空间
劣势:
复制算法它的缺点也是相当明显的。
1、他浪费了一半的内存,这太要命了
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视,所以从以上描述不难看出。复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%的内存浪费
3.标记清除(Mark-Sweep)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
什么是标记清除?
回收时,对需要存活的对象进行标记;
回收不是绿色的对象
优缺点
优点:不需要额外的空间
缺点:
总结:两次扫描,严重浪费时间,会产生内存碎片。
- 首先、它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用
程序,这会导致用户体验非常差劲 - 其次、主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象
都是随机的出现在内存的各个角落,现在把他们清除之后,内存的布局自然乱七八糟,而为了应付
这一点,JVM就不得不维持一个内存空间的空闲列表,这又是一种开销。而且在分配数组对象的时
候,寻找连续的内存空间会不太好找。
标记压缩(Mark-Compact)
什么是标记压缩?
在整理压缩阶段,不再对标记的对象作回收,而是通过所有存活对象都像一端移动,然后直接清除边界
以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被
清理掉,如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比
维护一个空闲列表显然少了许多开销。
标记、整理算法不仅可以弥补 标记、清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内
存减半的高额代价;
4.标记清除压缩(Mark-Sweep-Compact)
标记整理
总结
内存效率:复制算法 > 标记清除算法 > 标记整理算法 (时间复杂度)
内存整齐度:复制算法 = 标记整理算法 > 标记清除算法
内存利用率:标记整理算法 = 标记清除算法 > 复制算法
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所
提到的三个指标,标记整理算法相对来说更平滑一些 , 但是效率上依然不尽如人意,它比复制算法多了
一个标记的阶段,又比标记清除多了一个整理内存的过程。
难道就没有一种最优算法吗?猜猜看,下面还有
答案 : 无,没有最好的算法,只有最合适的算法 。 -----------------> 分代收集算法
年轻代:(Young Gen)
- 存活率低
- 复制算法
老年代:(Tenure Gen)
- 区域大:存活率高
- 标记清除、标记清除和标记清除压缩混合使用
9、常见面试题
1.JMM
可以看JUC高并发的JMM笔记
JMM:JAVA内存模型,不存在的东西,是一个概念,也是一个约定!
Java Memory Model(Java内存模型), 围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
解决共享对象可见性这个问题:volilate
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存;
2、线程加锁前,必须读取主存中的最新值到工作内存中;
3、加锁和解锁是同一把锁;
线程中分为 工作内存、主内存
评论区