JVM性能调优
组成:类加载子系统;运行时数据区;执行引擎
方法区
永久代 堆 jdk7
元空间 直接内存native memory
jdk8 最小20. 75M 最大 无限
方法区与永久代、元空间之间的关系
方法区是一种规范,永久代与元空间是它的一种具体的实现,jdk8以前使用的是永久代,它存在堆上;jdk8以后使用元空间取代永久代,运行在内存上
为甚要这样做 以前的机器是32位的能运行的最大内存是2^32,4G(应用层2G,内核层2G,如果java执行运行在内存上的化当死循环或其他什么情况会将内存占满,所以放在了堆内存上)
现在的机器一般都是64位了(实际使用了48位,还有16位作为保留),所以运行的最大内存位2^48即256T
方法区中存放的是class对象(java文件被编译成class文件,class文件被类加载器加载成class content,然后被解析成class对象)
Jvm:从软件的层面屏蔽不同操作系统在底层硬件指令上的区别
栈(filo):局部变量,为线程分配内存空间
栈帧:一个方法对应一块栈帧内存区域
局部变量不像前面介绍的类变量那样存在“准备阶段”。类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的值。因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。
java对象的组成
- mark word 在64位机器(永远)占8个字节
- 类型指针(KClass Pointer) 占 4bytes(如果是32G内存以下的,默认开启对象指针压缩,4个字节)
jdk中所有的数据都是8字节对齐(永远保证是8的整数倍)
线性地址(即类型指针的大小)
- 开启指针压缩 8bytes
- 不开指针压缩 16bytes
开启指针压缩
-XX:+/-UseCompressed0ops
空对象(没有任何普通属性的对象【不包括静态属性,静态属性是跟类绑定在一起的,存在方法区中】)占多少字节?
- 开启指针压缩 16bytes (8【mark word】+4【类型指针】+0【数组长度】+0【实例数据】+4【对齐填充】)
- 关闭指针压缩 16bytes (8【mark word】+8【类型指针】+0【数组长度】+0【实例数据】+0【对齐填充】)
普通对象占多少字节?
int id;//占4个字节
byte b1;//占1个字节
byte b2;//占1个字节
String name;//占4个字节
- 开启指针压缩 24 bytes (8【mark word】+4【类型指针】+4【id】+1【b1】+1【b2】+4【name】+2【对齐填充】)
- 关闭指针压缩 32 bytes (8【mark word】+8【类型指针】+4【id】+1【b1】+1【b2】+4【name】+6【对齐填充】)
数组对象占多少字节?
数组对象结构是
可以发现此时对象头中有对齐填充该中情况只会在【数组对象、未开启指针压缩】对象头中才有填充
对象头(_mark), 8个字节
Oop指针,如果是32G内存以下的,默认开启对象指针压缩,4个字节
数组长度,4个字节
数据区
Padding(内存对齐),按照8的倍数对齐
- 开启指针压缩
- 关闭指针压缩
对象指针【oop】(ordinary object pointer)[四个字节,支持的最大堆空间位4G]
从0X0000
test1=16字节
test2=32字节
test3=24字节
内存地址
test1=0x00000
test2=0x10000
test3=0x30000
开启指针压缩后,会将后面的三个0截掉(jdk中所有的对象都是以8字节对齐,1000,所以后面永远有三个0;所以在存储的时候,会将内存地址的后三位补0)
test1=0x00
test2=0x10
test3=0x30
这样的话,在使用的时候在加上000
一个oop能支持的最大空间是多少(oop在开启指针压缩后,最多存储35位(4字节*8+3个0))2^35
开启指针压缩如何扩容oop表示的最大的空间:左移—补零(8字节对齐,扩大对齐字节 补的0就会增加 ,表示的最大空间也就随之增大;但是会浪费空间;因此使用8字节对齐是合理的)
OOM
OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”,内存溢出
导致栈溢出的原因有哪些?
- 调用链过长(栈调用太深)
- 死循环
- 无限递归
- a方法调用b方法,b方法调用a方法
jdk默认栈大小为:1M
java -XX:+PrintFlagsFinal -version | grep ThreadStack
设置栈大小(最小为160k)
-Xss160k
public class Test(){
privite int deep =0;
public void aa(){
deep++;
aa();
}
public static void main(String[] args){
Test test = new Test();
try{
test.aa();
}catch(Throwable t){
System.out.println(test.deep);//此时输出的是栈深度
}
}
}
栈深度 772
栈大小为 160k
如何计算栈帧的大小:160*1024⁄772
如何避免虚拟机栈溢出
避免死循环,递归给出口
如何调优:设置参数
-Xss160k
class文件解析
一个类可以声明的成员属性上限是多少?
255:class文件中用于存储属性数量占两个字节,因此最大值为[0F0F]
一个类 可以声明的方法上限是多少?
255:class文件中用于存储方法数量占两个字节,因此最大值为[0F0F]
一个方法的形参上限是多少?怎么算出来的?算上this呢?
255:class文件中用于存储方法数量占两个字节,因此最大值为[0F0F]
- java static方法的参数最多只能有255个,非static方法最多只能有254个
- 在计算args_size时,有判断方法是否为static方法,如果不是static方法,则会在方法原有参数数量上再加一,这是因为非static方法会添加一个默认参数到参数列表首位:方法的真正执行者,即方法所属类的实例对象this
code的attribute_length占四个字节,最大值为【0F0F0F0F】Java方法体的字节数不能超过65535字节
假设现在在方法内部,你想获得对当前对象的引用。但是,对象引用是被秘密地传达给编译器——并不在参数列表中。方便的是,有一个关键字: this 。this 关键字只能在非静态方法内部使用。当你调用一个对象的方法时,this 生成了一个对象引用。你可以像对待其他引用一样对待这个引用。如果你在一个类的方法里调用其他该类中的方法,不要使用 this,直接调用即可,this 自动地应用于其他方法上了。
一个方法最多支持多少个局部变量?如果是long、double类 型呢?
- 255 其实255规定的是参数的最大单位数量,而非参数数量。long或double类型作为参数会占据两个两个单位的长度。
每个方法的局部变量表的size是如何计算出来的?
Java中的异常是如何实现的?
字节—-位 1字节= 8位8个二进制位 0000 1111
0 – 2的8次方 无符号 0-255 有符号 -128 — 127 255.255.255.255 JVM最多只会255个字节码指令
255.255.255.255
JVM最多只会255个字节码指令
javap -c xx
javap -verbose
字节码文件.class
深入理解Java虚拟机笔记—class类文件魔数,版本,常量池
JVM NEW的过程
JVM this生成时机
this其实就是通过方法体的第一个参数传递过来的,它指代调用该方法的对象。通过这个例子应该能够更加直观具体的体会this的含义了。
java 在执行构造函数的时候会将,this作为参数列表的第一个参数传给构造函数,也就是说this在这个时候产生的
JVM探究
- 请你谈谈你对VM的理解? java8虚拟机和之 前的变化更新?
- 什么是00M,什么是栈溢出StackOverFlowError? 怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?知道吗?
- 谈谈JVM中, 类加载器你的认识?
JVM的位置
运行在操作系统上(jre包含了jvm)
JVM的体系结构
栈,本地方法栈,程序计数器中没有垃圾
所谓了jvm调优就是在堆中调优(最重要)和人方法区中调优
本地方法接口(JNI)
类加载器
类是一个模板是抽象的
Class<? extends Car> aClass1 = car1. getClass();
ClassLoader classLoader = aClass1. getClassLoader();
System. out . print1n(classLoader);//AppClassLoader //
System. out . println(classLoader . getParent()); //ExtClassLoader \jre\lib\ext
System. out . print1n(classLoader . getParent(). getParent()); //null 1. 不存在2.java程序获收不到rt.jar
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
双亲委派机制
package java.lang;
public class String {
//双亲委派机制:安全
// 1. APP(当前类)-->EXC(扩展)---B0OT(最终执行)
public String toString() {
return "Hello";
}
pub1ic static void main(String[] args) {
String s = new String();
s.tostring();
}
}
//我们自己写了一个与javaString同包同名的String类,想要调用他的toString方法,但是会报错,这是由于他的双亲委派机制,会向上寻找,最终执行的是java的String的方法。当且仅当根加载器,和扩展类加载器中都没有该类才会执行应用程序加载器
/*
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成, -直向上委托,直到启动类加载器
3.启动加载器检查是否能够加载当前这个类,能加载就结束, 使用当前的加载器,否则, 抛出异常, 通知子加载器进行加载
4.重复步骤3
*/
沙箱安全机制
Native
public static void main(String[] args) {
new Thread(()->{
},
name: "my thread name"). start();
//线程start调用的就是start0方法,但是它又没有函数体
private native void start0();
native :凡是带了native 关键字的, 说java的作用范围达不到了,回去调用底层C语言的库!会进入本地方法栈会调用本地方法接口(JNI)
JNI的作用:扩展Java的使用,融合不同变成语言为Java所用!
Java诞生的时候C、C++ 横行,想要立足,必须要有调//C、C++的程序 它在内存区城中专门开辟了一块标记区城: Native Method Stack,登记native 方法 在最终执行的时候,加代本地方法库中的方法通过JNI
PC寄存器
PC寄存器也叫程序计数器: Program Counter Register 每个线程都有一个程序计数器,是线程私有的,就是一一个指针, 指向方法区中的方法字节码(用来存储指向一条指令的地址,即即将要执行的指令代码),在执行引擎中读取下一条指令,是一个非常小的(占用)内存空间,几乎可以忽略不计。
方法区
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及-些特殊方法,如构造函数,接口代码也在此定义, 简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存 中,和方法区无关
static final, Class, 常量池
栈
栈:先进后出、后进先出:桶 队列:先进先出( FIFO : First Input First Output )
喝多了吐就是栈,吃多了拉就是队列
栈:栈内存,主管程序的运行,生命周期和线程同步; 线程结束,栈内存也就是释放,对于栈来说不存在垃圾回收
栈: 8大基本类型 +对象引用+实例的方法
每一个线程就会产生一个栈
java每执行一个方法就会产生一个栈帧,按照栈先进后出的顺序慢慢被压栈,现在正在在执行的方法永远在栈的顶部
栈运行原理:栈帧 栈满了: StackOverflowError
java的本质是值传递
简单类对象的实例化过程
1、在方法区加载类;
2、在栈内存申请空间,声明变量P;
3、在堆内存中开辟空间,分配对象地址;
4、在对象空间中,对对象的属性进行默认初始化,类成员变量显示初始化;
5、构造方法进栈,进行初始化;
6、初始化完成后,将堆内存中的地址赋给引用变量,构造方法出栈;
子类对象的实例化过程
1、在方法区先加载父类,再加载子类;
2、在栈中申请空间,声明变量P;
3、在堆内存中开辟空间,分配对象地址;
4、在对象空间中,对对象的属性(包括父类的属性)进行默认初始化;
5、子类构造方法进栈;
6、显示初始化父类的属性;
7、父类构造方法进栈,执行完毕出栈;
8、显示初始化子类的属性;
9、初始化完毕后,将堆内存中的地址值赋给引用变量P,子类构造方法出栈;
三种JVM
查看虚拟机版本
java -version
- Sun公司HotSpot Java HotSpot™ 64-Bit Server VM (bui1d 25. 181-b13,mixed mode)
- BEA JRockit
- IBMJ9VM
我们学习都是: Hotspot
堆
Heap, 一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆中?类, 方法,常量,变量~,保存我们所有引用类型的真实对象;
堆内存中还要细分为三个区域:
- 新生区(伊甸园区)
- 养老区
- 永久区
GC垃圾回收,主要是在伊甸园区和养老区~
假设内存满了,会报错OOM(java.lang.OutOfMemoryError: Java heap space),堆内存不够!
在JDK8以后,永久存储区改了个名字(元空间)
新生代
- 类:诞生和成长的地方,甚至死亡;
- 伊甸园,所有的对象都是在伊甸园区new出来的!
- 幸存者区(0,1)
真理:经过研究,99%的对象都是临时对象!
老年代
老年代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收老年代和年轻代中不再被使用的对象资源。
永久代
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据, 存储的是Java运行时的一些环境或类信息~,这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域的内存
一个启动类,加载了大量的第三方jar包,Romcat部署了太多的应用,大量动态生成的发射类。不断地被加载。直到内存满,就会出现OOM
jdk1.6之前:永久代,常量池是在方法区;
jdk1.7 .永久代,但是慢慢的退化了,去永久代, 常量池在堆中
jdk1.8之后:无永久代,常量池在元空间
持久代又被称为非堆:其中有方法区,方法区中又有一部分区域为常量池
持久代:逻辑上存在但是物理上不存在:即堆内存=新生区+老年代
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long max = Runtime . getRuntime() . maxMemory(); //字节 1024 * 1024
//返回jvm的初始化总内存
1ong total = Runtime . getRuntime(). totalMemory();
System . out. print1n( "max="+max+"字节\t"+( max/(double)1024/1024)+"MB");
System. out. print1n("tota1="+max+"字节\t"+(total/ ( double)1024/1024)+"MB");
}
//默认情况下:分配的总内存是电脑内存的1/4,而初始化的内存:1/64
遇到OOM怎么办
尝试扩大堆内存看结果
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
分析内存,看一下哪一个地方出现了问题(专业工具)
轻GC,重GC(full GC)
伊甸园区满了会触发触发轻GC,将垃圾回收放到幸存区中,重复几次;当伊甸园区和两个幸存区都满了之后就会触发重GC,触发重GC之后就会触发轻GC了;就这样循环,直到重GC也清理不了了:年轻代满了,老年代也满了元数据也满了才会报错OOM
堆内存调优
在一个项目中,突然出现了OOM故障,那么该如何排除~研究为什么出错
- 能够看到代码第几行出错:内存快照分析工具:MAT(eclipse),Jprofiler
- Debug,一行行分析代码!
MAT,Jprofile作用:
- 分析Dump内存文件,快速定位内存泄露;
- 获得堆中的数据
- 获得大的对象~
Throwable(异常地顶级接口)两大类:Exception 、 error
idea安装jprofiler插件,win10安装jprofiler程序(安装位置不能有空格),在idea中设置win10安装jprofiler地exe
-Xms1m -Xmx:8m -XX:+HeapDumpOnOutOfMemoryError
//-Xms1m -Xmx:8m -XX:+HeapDumpOnOutOfMemoryError
pub1ic class Demo03 {
byte[] array = new byte[1* 1024*1024]; //1m
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new Demo03()); //问题所在
count = count+1;
}
}catch (Error e){
System.out.println("count: "+count);
e.printStackTrace();
}
}
}
在src同级目录下会出现一个文件
// -Xms 设置初始化内存分配大小/164
// -Xmx 设置最大分配内存,默认1/4
// -XX: +PrintGCDetails //打E印IGC垃圾回收信息
// -XX; +HeapDumpOnOutOfMemoryError //oom DUMP 当出现OutOfMemoryError时就dump文件
GC
垃圾回收主要在堆中
JVM在进行GC时,并不是对这三个区域统一一回收。 大部分时候,回收都是新生代~
- 新生代
- 幸存区(from,to)
- 老年区
GC两种类:轻GC (普通的GC), 重GC (全局GC)
GC题目:
- JVM的内存模型和分区~详细到每个区放什么?
- 堆里面的分区有哪些? Eden, form, to, 老年区,说说他们的特点!
- GC的算法有哪些?标记清除法,标记整理,复制算法,引用计数器,怎么用地?
- 轻GC和重GC分别在什么时候发生?
常用算法
引用计数法
用地较少
复制算法
幸存区(from和to地判断):谁空谁是to
始终保存幸存区中有一个是空的
好处:没有内存的碎片~
坏处:浪费了内存空间~多了一半空间永远是空的(to)
假设对象100%存货(极端情况这种算法的弊端就很大)
复制算法最佳使用场景:对象存活度较低的时候;新生区
标记清除法
世界上第一个GC算法,由 JohnMcCarthy 在1960年发布。
- 优点:不需要额外的空间!|
- 缺点:两次扫描,严重浪费时间,会产生内存碎片
标记压缩算法
再优化
但是多了一个移动成本
标记清除压缩算法
先标记清除几次,再进行压缩
总结
- 内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
- 内存整齐度:复制算法=标记压缩算法>标记清除算法
- 内存利用率:标记压缩算法=标记清除算法>复制算法
思考一个问题:难道没有最优算法吗?
答案:没有,没有最好的算法,只有最合适的算法
GC :分代收集算法
年轻代:
- 存活率低
- 复制算法!
老年代:
- 区域大:存活率
- 标记清除(内存碎片不是太多) +标记压缩混合实现
JMM
java Memory Model(java内存模型)