Java进阶及JVM

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对象)

JDK8 从永久代到元空间

img

Jvm:从软件的层面屏蔽不同操作系统在底层硬件指令上的区别

image-20200513093936915

栈(filo):局部变量,为线程分配内存空间

栈帧:一个方法对应一块栈帧内存区域

深入理解Java虚拟机笔记—运行时栈帧结构

局部变量不像前面介绍的类变量那样存在“准备阶段”。类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的值。因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。

java对象的组成

image-20200513153123198

  • 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【对齐填充】)

数组对象占多少字节?

聊聊JVM(三)两种计算Java对象大小的方法

数组对象结构是

image-20200513161416430

可以发现此时对象头中有对齐填充该中情况只会在【数组对象、未开启指针压缩】对象头中才有填充

  1. 对象头(_mark), 8个字节

  2. Oop指针,如果是32G内存以下的,默认开启对象指针压缩,4个字节

  3. 数组长度,4个字节

  4. 数据区

  5. 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*1024772

如何避免虚拟机栈溢出

避免死循环,递归给出口

如何调优:设置参数

-Xss160k

class文件解析

class文件解析参照表

深入理解JVM之Java字节码(.class)文件详解参考

从一个class文件深入理解Java字节码结构参考

JVM指令手册

一个类可以声明的成员属性上限是多少?

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字节

假设现在在方法内部,你想获得对当前对象的引用。但是,对象引用是被秘密地传达给编译器——并不在参数列表中。方便的是,有一个关键字: thisthis 关键字只能在非静态方法内部使用。当你调用一个对象的方法时,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的过程

Java中new一个对象的步骤:

JVM this生成时机

java this关键字的本质

this其实就是通过方法体的第一个参数传递过来的,它指代调用该方法的对象。通过这个例子应该能够更加直观具体的体会this的含义了。

java 在执行构造函数的时候会将,this作为参数列表的第一个参数传给构造函数,也就是说this在这个时候产生的

jvm new 字节码

java new一个对象的步骤

this创建时机

JVM探究

  • 请你谈谈你对VM的理解? java8虚拟机和之 前的变化更新?
  • 什么是00M,什么是栈溢出StackOverFlowError? 怎么分析?
  • JVM的常用调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?知道吗?
  • 谈谈JVM中, 类加载器你的认识?

JVM的位置

运行在操作系统上(jre包含了jvm)

image-20200514152810310

JVM的体系结构

image-20200514153320419

栈,本地方法栈,程序计数器中没有垃圾

所谓了jvm调优就是在堆中调优(最重要)和人方法区中调优

本地方法接口(JNI)

JVM Architecture Diagram

类加载器

类是一个模板是抽象的

image-20200514154512309

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

  1. 虚拟机自带的加载器
  2. 启动类(根)加载器
  3. 扩展类加载器
  4. 应用程序加载器

双亲委派机制

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 每个线程都有一个程序计数器,是线程私有的,就是一一个指针, 指向方法区中的方法字节码(用来存储指向一条指令的地址,即即将要执行的指令代码),在执行引擎中读取下一条指令,是一个非常小的(占用)内存空间,几乎可以忽略不计。

JVM学习笔记 ——— 程序计数器

方法区

Method Area方法区

方法区是被所有线程共享,所有字段和方法字节码,以及-些特殊方法,如构造函数,接口代码也在此定义, 简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存 中,和方法区无关

static final, Class, 常量池

方法区(关于java虚拟机内存的那些事)

栈:先进后出、后进先出:桶 队列:先进先出( FIFO : First Input First Output )

喝多了吐就是栈,吃多了拉就是队列

栈:栈内存,主管程序的运行,生命周期和线程同步; 线程结束,栈内存也就是释放,对于栈来说不存在垃圾回收

栈: 8大基本类型 +对象引用+实例的方法

image-20200514164209545

每一个线程就会产生一个栈

java每执行一个方法就会产生一个栈帧,按照栈先进后出的顺序慢慢被压栈,现在正在在执行的方法永远在栈的顶部

栈运行原理:栈帧 栈满了: StackOverflowError

java的本质是值传递

简单类对象的实例化过程

  1、在方法区加载类;

  2、在栈内存申请空间,声明变量P;

  3、在堆内存中开辟空间,分配对象地址;

  4、在对象空间中,对对象的属性进行默认初始化,类成员变量显示初始化;

  5、构造方法进栈,进行初始化;

  6、初始化完成后,将堆内存中的地址赋给引用变量,构造方法出栈;

子类对象的实例化过程

  1、在方法区先加载父类,再加载子类;

  2、在栈中申请空间,声明变量P;

  3、在堆内存中开辟空间,分配对象地址;

  4、在对象空间中,对对象的属性(包括父类的属性)进行默认初始化;

  5、子类构造方法进栈;

  6、显示初始化父类的属性;

  7、父类构造方法进栈,执行完毕出栈;

  8、显示初始化子类的属性;

  9、初始化完毕后,将堆内存中的地址值赋给引用变量P,子类构造方法出栈;

img

三种JVM

查看虚拟机版本

java -version
  • Sun公司HotSpot Java HotSpot™ 64-Bit Server VM (bui1d 25. 181-b13,mixed mode)
  • BEA JRockit
  • IBMJ9VM

我们学习都是: Hotspot

Heap, 一个JVM只有一个堆内存,堆内存的大小是可以调节的。

类加载器读取了类文件后,一般会把什么东西放到堆中?类, 方法,常量,变量~,保存我们所有引用类型的真实对象;

堆内存中还要细分为三个区域:

  • 新生区(伊甸园区)
  • 养老区
  • 永久区

image-20200514195610628

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之后:无永久代,常量池在元空间

    image-20200514213652635

持久代又被称为非堆:其中有方法区,方法区中又有一部分区域为常量池

持久代:逻辑上存在但是物理上不存在:即堆内存=新生区+老年代

image-20200514213853055

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怎么办

  1. 尝试扩大堆内存看结果

    -Xms1024m -Xmx1024m -XX:+PrintGCDetails
    
    
  2. 分析内存,看一下哪一个地方出现了问题(专业工具)

轻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分别在什么时候发生?

常用算法

引用计数法

用地较少

[java中垃圾回收机制中的引用计数法和可达性分析法

image-20200514222508212

复制算法

幸存区(from和to地判断):谁空谁是to

image-20200514223109887

始终保存幸存区中有一个是空的

image-20200514223438855

  • 好处:没有内存的碎片~

  • 坏处:浪费了内存空间~多了一半空间永远是空的(to)

假设对象100%存货(极端情况这种算法的弊端就很大)

复制算法最佳使用场景:对象存活度较低的时候;新生区

标记清除法

世界上第一个GC算法,由 JohnMcCarthy 在1960年发布。

GC算法精解(五分钟让你彻底明白标记/清除算法)

image-20200514224047020

  • 优点:不需要额外的空间!|
  • 缺点:两次扫描,严重浪费时间,会产生内存碎片

标记压缩算法

标记-压缩算法

再优化

image-20200514224300336

但是多了一个移动成本

标记清除压缩算法

先标记清除几次,再进行压缩

总结

  • 内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
  • 内存整齐度:复制算法=标记压缩算法>标记清除算法
  • 内存利用率:标记压缩算法=标记清除算法>复制算法

思考一个问题:难道没有最优算法吗?

答案:没有,没有最好的算法,只有最合适的算法

GC :分代收集算法

年轻代:

  • 存活率低
  • 复制算法!

老年代:

  • 区域大:存活率
  • 标记清除(内存碎片不是太多) +标记压缩混合实现

JMM

java Memory Model(java内存模型)