JVM初识
JVM初识
- 请你谈谈对JVM的理解?java8虚拟机和之前的变化更新?
- 什么是OOM,什么是栈溢出 StackOverFlowError?怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?
- 谈谈JVM中,类加载器你的认识?
1、JVM的位置
2、JVM的体系结构
3、类加载器
- 作用:加载class文件 ---》new Student();
1、虚拟机自带的加载器
2、启动类(根)加载器
3、扩展类加载器
4、应用程序(系统类)加载器
示例:
package lesson04;
public class Car {
public int age;
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class c1 = car1.getClass();
Class c2 = car2.getClass();
Class c3 = car3.getClass();
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
System.out.println(c3.hashCode());
ClassLoader classLoader = c1.getClassLoader();
System.out.println(classLoader);//AppClassLoader
ClassLoader parent = classLoader.getParent();
System.out.println(parent);//ExtClassLoader jre/lib/ext
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);//null rt.jar
}
}
结果:
460141958
1163157884
1956725890
685325104
685325104
685325104
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
null
4、双亲委派机制
示例:
package java.lang;
public class String {
/**
*双亲委派机制:安全
* 1、APP ---->EXT---->BOOT(最终执行)
*/
public String toString(){
return "hello";
}
public static void main(String[] args) {
String s = new String();
System.out.println(s.getClass().getClassLoader());
s.toString();
}
/**
* 1、类加载器收到类加载的请求
* 2、将这个请求向上委托给父类加载器完成,一直向上委托,直到跟加载器BOOT
* 3、启动类加载器是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
* 4、重复步骤 3
*
* null :Java调用不到——————c\c++
* Java = C++; 去掉指针和内存管理 --》C++--
*/
}
结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
Process finished with exit code 1
5、沙箱安全机制
组成沙箱的基本组件:
-
字节码校验器(bytecode verifier):确保java类文件遵循java语言规范,这样可以帮助java程序实现内存保护,但并不是所有的类文件都会经过字节码校验,比如核心类。
-
类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码区干涉善意的代码;//双亲委派机制
- 它守护了被信任的类库边界
- 它将代码归入保护域,确定了代码可以进行那些操作
类装载器采用的机制是双亲委派机制
1、从最内层的JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
2、由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获取权限访问到内层类,破坏代码就自然无法生效;
- 存取控制器(access controller ):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
- 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
- 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性 包括:
- 安全提供者
- 消息摘要
- 数字签名 keytools
- 加密
- 鉴别
6、Native
package test;
public class Demo {
public static void main(String[] args) {
new Thread(()->{},"my thread name").start();
}
/**
* native:凡是带了native 关键字的,说明Java的作用范围达不到了,会去掉底层C语言的库
* 会进入本地方法栈
* 调用本地方法本地接口 JNI java native interface
* JNI作用:扩展java的使用,融合不同的编程语言为JAVA所用! 最初:c c++
* Java诞生的时候 C C++ 横行,想要立足 就必须要有调用 C C++ 的程序
* 它在内存区域中专门开辟了一块标记区域:Native Method Stack【本地方法栈】 登记了 native 方法
* 在最终执行的时候,加载本地方法库中的方法通过JNI
*
* java程序驱动打印机,管理系统,掌握即可,在企业级应用中较为少见!
*/
public native void hello();
/**
* 调用其他接口 : Socket; WebService; http;
*/
}
7、PC寄存器
程序计数器:program counter register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的命令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
8、方法区
Method Area 方法区
方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数、接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区域
[static]静态变量、[final]常量、[Class]类信息(构造方法、接口定义)、运行时的常量池存放方法去中,但是 实例变量存在堆内存中,和方法无关
9、栈
-
栈是数据结构
程序 = 数据结构 + 算法
-
栈===》先进后出 后进先出
队列==》先进先出 后进后出(FIFO : first input first output)
-
栈:栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存也就释放了,对于栈来说不存在垃圾回收,一旦线程结束,栈就Over了
-
栈存储:八大基本类型 + 对象的引用(地址) + 实例方法
-
栈运行原理:栈帧
- 栈满了:StackOverflowError 【error虚拟机就停止了】
- 栈 + 堆 + 方法区 :交互关系 【一个对象在内存中实例化的过程】
10、三种JVM
-
Sun公司
Java -version可以查看
HotSpot Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)
-
BEA公司 JRockit
-
IBM公司 J9VM
11、堆[重点]
-
Heap:一个JVM只有一个堆内存,堆内存的大小是可以调节的。
-
类加载器读取了类文件后,一般会把什么东西放到堆中呢? 类、方法、常量、变量--保存我们所有引用的真实对象
-
堆内存中还要细分为三个区域:
- 新生区 (伊甸园)Young/New
- 养老去old
- 永久区
12、新生区、老年区、永久区
新生区
- 一个类诞生、成长的地方,甚至死亡
- 伊甸园区:所有的对象都是在伊甸园区new出来的
- 幸存者区 0区 1区
真理:经过研究,99%的对象都是临时对象!
永久区
这个区域常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或类信息--------这个区域不存在垃圾回收!关闭虚拟机就会释放永久区的内存。
假设:一个启动类,加载了大量的第三方jar包。tomcat部署了太多的应用。大量动态生成的反射类。 不断的被加载,知道内存满了,就会出现OOM。
- JDK1.6之前:永久代,常量池在方法区
- JDK1.7:永久代,但是慢慢的退化了【去永久代,常量池在堆中】
- JDK1.8之后:无永久代,常量池在元空间
元空间:逻辑上存在,物理上不存在 新生区 + 老年代 = JVM maxMemory
示例:vm options
vm options: -Xms1024m -Xmx1024m -XX:+PrintGCDetails
package test;
public class Demo02 {
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long maxMemory = Runtime.getRuntime().maxMemory();//字节 1024 * 1024
//返回JVM的初始化总内存
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("maxMemory:"+maxMemory+"字节\t"+maxMemory/(double)1024/1024+"MB");
System.out.println("totalMemory:"+totalMemory+"字节\t"+totalMemory/(double)1024/1024+"MB");
/**
* 默认情况下:分配的总内存 是电脑内存的1/4,而初始化的内存 是1/64
*/
/**
* 遇到OOM:
* 解决方式:
* 1、尝试将堆内存扩大 看结果
* 2、分析内存,看一下那个地方出现问题(专业工具)
*
* 初识内存和总内存都设置为1024M 并且打印GC消息
* -Xms1024m -Xmx1024m -XX:+PrintGCDetails
*/
/**
* 305664K + 699392K = 1005056K (除1024)---》 981.5M
*/
}
}
结果:
maxMemory:1029177344字节 981.5MB
totalMemory:1029177344字节 981.5MB
Heap
PSYoungGen total 305664K, used 20971K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7afb8,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3235K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
13、堆内存调优
问:
在一个项目中,突然出现了OOM故障,那么该如何排除?研究为什么出错?
- 能够看到代码第几行出错:内存快照分析工具 MAT Jprofiler
- Debug:一行行分析代码
MAT Jprofiler作用
- 分析Dump内存文件,快速定位内存泄露;
- 获得堆中的数据
- 获得大的对象
安装插件
-
File ----》settings ----》 Plugins ----》 搜Jprofiler ---》search in market ----> install
-
安装完之后重启IDEA
-
下载安装Jprofiler 自定义安装路径要没有中文和空格 建议下载9.2.1的可用下面注册码
-
弹出的license information
选enter license key ---》name company 随便选 ----》 注册码如下选一个
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
-
重新打开IDEA File ---> settings --->Tools ---> JProfiler ---> JProfiler executable
找到下载路径的bin目录下的.exe 之后点击Apply OK
示例:jprofiler用法
可以根据不同的错 dump出文件
- -Xms :设置初始化内存大小 1/64
- -Xmx :设置最大分配内存 1/4
- -XX:+PrintGCDetails :打印GC垃圾回收信息
- -XX:+HeapDumpOnOutOfMemoryError :OOM dump
VM options: -Xms1m -Xmx4m -XX:+HeapDumpOnOutOfMemoryError
package test;
import java.util.ArrayList;
//Dump
public class Demo03 {
byte[] array = new byte[1024*1024];
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
int count = 0;
//OOM
try {
while (true){
list.add(new Demo03());
count++;
}
} catch (Exception e) {
System.out.println("count+"+count);
}
/**
* Throwable
* Exception
* Error
*/
}
}
结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid9696.hprof ...
Heap dump file created [3514346 bytes in 0.045 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at test.Demo03.<init>(Demo03.java:8)
at test.Demo03.main(Demo03.java:16)
Process finished with exit code 1
先看大对象-----》看线程 能定位到第几行!!!
14、GC:垃圾回收(自动进行)
GC的作用区只有堆--绿色部分
JVM在进行GC时:并不是对这三个区域统一回收,大部分的时候,回收的都是新生代~
- 新生代
- 幸存区 (from to两个区 会交换的区域~~ from变to to变from)
- 老年区
GC两种类型:
- 轻GC [普通的GC]: 新生代和幸存区(偶尔幸存区满的话)
- 重GC [全局GC]:全部清
题目:
- JVM的内存模型和分区!详细到每个区放什么?
- 堆里面的分区有哪些? Eden from to 老年区,说说他们的特点?
- GC的算法有那些?标记清除法、标记整理(压缩)法、复制算法、引用计数法
- 轻GC 和 重GC 分别在什么时候发生?
引用计数法:
复制算法:
- 好处:没有内存碎片!
- 坏处:浪费了内存空间(多了一半空间永远是空的to,假设对象100%存活(极端情况:from区中好多对象都复制到to中))
复制算法最佳使用场景:对象存活度较低的时候:新生区
标记清除法:
- 优点:不需要额外的空间!
- 缺点:两次扫描严重浪费时间,会产生内存碎片。
标记清除压缩:
对标记清除的优化~~~
再次优化
- 先标记清除几次-------》再进行压缩
总结:
-
内存效率:复制算法 > 标记清除算法 > 标记压缩算法 (时间复杂度)
-
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
-
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
-
思考?没有最优的JVM算法吗? ----木有~~ 没有最好的算法~·只有最合适的(看场景)
所以---------GC : 分代收集算法
- 年轻代:【存活率低】 复制算法
- 老年代:【区域大、存活率高】标记清除+标记压缩 混合实现(JVM调优,清多少次在压等等)
15、JMM Java内存模型
Java Memory Model
-
JMM是干嘛的? 作用:缓存一致性,用于定义数据读写的规则(遵守这个规则)
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
解决共享对象可见性这个问题:volilate [当线程更改了共享变量 会马上刷新到主内存,保证其他线程取时候是正确的]
-
如何学习? 学volilate
-
JMM制定了一些规则:
- 不允许read和load、store和write操作之一单独出现。即 使用了read必须load,使用了store必须write
- 不允许线程丢弃它最近的assign操作,即 工作变量的数据改变了之后,必须告知主内存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程对其进行lock操作。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新assgin或load操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作,也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock之前,必须把此变量同步回主内存
JMM对这八种操作规则和对volilate的一些特殊规则就能确定哪些操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以我们一般也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。
补充:
多看博客,多百度~~·加油!
思维导图网站:https://www.processon.com/
读深入理解JAVA虚拟机
运行时是数据区域
程序计数器
- 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
- 在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有“的内存。
- 如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器值则为空(undefined)。
- 此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈
-
与程序计数器一样,java虚拟机栈也是线程私有的,它的生命周期与线程相同。
-
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应这个一个栈帧在虚拟机栈中入栈到出栈的过程。
-
局部变量表存放了编译器可知的各种基本数据类型(八大基础类型)、对象引用(reference类型,它不等同于对象本身,可能是一个指向起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
-
其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间时完全确定的,在方法运行期间不会改变局部变量表的大小。
-
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以扩展(当前大部分的java虚拟机都可以动态扩展,只不过java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈
- 本地方法栈与虚拟机所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
- 在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
- 有的虚拟机直接把本地方法栈和虚拟机栈合二为一(e.g.Sun HotSpot虚拟机)。
- 与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆
- 对于大多数应用来说,java堆是java虚拟机所管理的内存中最大的一块。
- java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- java堆是垃圾收集器管理的主要区域,因此很多时候也被称为”GC堆“。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例。
- java堆可以处于物理不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。
- 在实现时,既可以实现成固定大小的,也可以时可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
- 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区
- 方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多人都更愿意把方法区称为”永久代“,本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理java堆一样管理这部分内存,能够省专门为方法区编写内存管理代码的工作。对于其他虚拟机来说是不存在永久代的概念的。
- java虚拟机规范对方法区的限制非常宽松,除了和java堆一样不需要连续的内存和可以选择固定大小或者可拓展外,还可以选择不实现垃圾收集。
- 根据java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
- 运行时常量池时方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息时常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- java虚拟机对class文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,java虚拟机规范没有做任何细节要求,不同的提供商实现的虚拟机可以按照自己的需求来实现这个内存区域。不过,一般来说,除了保存class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
- 运行时常量池相对于class文件常量池的另外一个重要特征时具备动态性,java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
- 运行时常量池是方法区的一部分,自然也受到方法区内存的限制,当常量池无法再申请到内存是会抛出OutOfMemoryError异常。
直接内存
- 直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
- 在jdk1.4新加入了NIO(New Input/Output)类,引入了一种基于通道(channel)和缓冲区(buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在java堆和native堆中来回复制数据。
- 显然,本机直接内存的分配不会受到java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存大小以及处理器寻址的限制。
- 服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。