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调用不到——————cc++
* 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+"字节 "+maxMemory/(double)1024/1024+"MB");
System.out.println("totalMemory:"+totalMemory+"字节 "+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/