0%

jvm学习

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

Java运行机制

既是解释性语言,又是编译性语言

  1. 编写.java

  2. 编译

    使用java编译器对.java文件进行编译,生成.class字节码文件

  3. 运行

    JVM解释器将.class字节码文件翻译成机器码,并执行程序显示结果

  4. 字节码文件:二进制文件

  5. Java虚拟机:编译后的Java程序指令不在计算机的CPU上执行,而是在JVM上执行

JVM的位置

运行在操作系统之上,JVM用C写的

JRE包含了JVM

默认hotspot实现

JVM的体系结构

栈和程序计数器不会存在垃圾

JVM调优仅存在在堆中

类加载器

作用:加载class文件~ new Student();

  1. 虚拟机自带的加载器

  2. 启动类(根)加载器jre/lib下的rt.jar、resources.jar、charsets.jar和class等

  3. 扩展类加载器jre/lib/ext的jar包和class文件

  4. 应用程序(系统类)加载器当前应用的classpath下的所有类

双亲委派机制

  1. 用户自定义类加载器 User (继承classLoader重写findclass方法,加载对应类)

  2. 类加载器收到类加载的请求 Application app

  3. 将这个请求向上委托为父类加载器去完成,一直向上委托,直到启动类加载器

  4. 启动类加载器检查是否能够加载这个类,能加载就会儿三个月,使用当前的加载器,否则,抛出异常,通知自家在其进行加载

  5. 重复步骤 3

    Class Not Found Exception~

    null: java调用不到~ 因为c、c++写的

    Java = C++— 去掉繁琐的东西:指针,内存管理

存在意义:

  1. 通过委派的方式,可以避免类的重复加载,当附加在其已经加载过某一个类时,子加载器就不会再重新加载这个类
  2. 保证安全性:bootstrap ClassLoader被加载时,只会加载JAVA_HOME/lib中的jar包的类,那么这些类不会被随意替换,避免有人自定义一个有破坏功能的java.lang.被加载。可以*有效防止核心JAVA API被篡改

有时也需要打破双亲委派机制:

沙箱安全机制

沙箱机制就是将java代码限定在虚拟机(jvm)特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样的措施来保证对代码的有效隔离,防止对系统造成破坏

沙箱的基本组件:

  • 字节码校验器:确保Java类文件遵循java语言规范。这样可以帮助Java实现内存保护。但并不是所有类文件都会经过字节码校验,比如核心类。

  • 类装载器

Native关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class demo {
public static void main(String[] args) {
new Thread(()->{

}, "my thread").start();

}
// native: 凡是带了native关键字的,说明java的作用范围达不到了,会调用底层C语言的库
// 会进入本地方法栈
// 调用本地方法接口 JNI
// JNI作用: 扩展Java的使用,融合不同的编程语言为Java所用
// 在内存区域中专门开辟了一块标记区域 : Native Method Stack, 登记native方法
// 在最终执行的时候,通过JNI加载本地方法库中的方法
// Java程序驱动打印机,管理系统,当我即可,在企业级应用中较为少见
private native void start0();
// 调用其他接口: Socket WebService http
}

Native Method Area/Stack

它具体做法是Native Method Stack中登记native方法,在执行引擎执行的时候加载Native Libraies

PC寄存器

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

方法区

Method Area

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

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

程序 = 数据结构 + 算法:持续学习

程序 = 框架 + 业务逻辑:吃饭用

为什么main()限制性,最后结束

栈:栈内存,主管程序的运行,生命周期和线程同步

线程结束,占内存也就释放了;对于栈来说,不存在垃圾回收问题

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

  • 每个线程都包含一个栈区,栈中只保存基本数据类型的值和对象的应用以及基础数据的引用
  • 每个栈中的数据(基本数据类型和对象的应用)都是私有的,其他栈是无法进行访问的
  • 栈分为三个部分:基础类型变量区、执行环境上下文,操作指令区(存放操作指令)

栈运行:java中,每执行一个方法都会产生一个栈帧

栈满抛异常:StackOverflowError

栈 + 堆 + 方法区的交互关系:

画出一个对象在内存中实例化的过程

https://blog.csdn.net/qq_43430343/article/details/119709715

https://yangyongli.blog.csdn.net/article/details/126465785

三种JVM

  • Sun公司 HotSpot
  • BEA JRockit
  • IBM J9 VM

我们学习都是HotSpot

Heap,一个JVM只有一个堆内存,被所有线程所共享,堆内存的大小是可以调节的,通过-Xmx(堆的最大容量)和-Xms(堆的初始容量)控制

类加载器读取了类文件后,一般会把:类、方法、常量、变量等 保存我们所有引用类型的展示对象

新生代(young)、老年代(old)、元空间

新生代:被细分为Eden和两个Survior区域,这两个Survivor区域分别被命名为from和to以示区分(默认Edem:from:to=8:1:1)

GC垃圾回收,主要是在伊甸园区和养老区

假设内存满了,OOM:堆内存不够(java.lang.OutOfMemoryError: Java heap space)

在JDK8后,永久存储区改了个名字:元空间

永久区:这个区域常驻内存。用来存放JDK自身携带的Class对象、Interface元数据,存储得到是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭VM虚拟机就会释放这个区域的内存

  • jdk1.6之前:永久代,常量池在方法区
  • jdk1.7:永久代,但是慢慢的退化了,去永久代常量池在堆中
  • jdk1.8之后:无永久代,方法区成为了概念,变成了元空间存在堆中,常量池在元空间(方法区和堆的关系https://blog.csdn.net/wangkexiang21/article/details/124014726)

方法区的OOM原因:如果一个启动类,加载了大量的第三方jar包;Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。知道内存满,就会出现OOM

(字符串常量池在jdk1.6存在于方法区,1.8知乎元空间化字符串常量池移存到了堆,但是运行时常量池和类文件常量池放在元空间中)

元空间逻辑上存在,物理上不存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class demo02 {
public static void main(String[] args) {
//返回虚拟机视图使用的最大内存
long max = Runtime.getRuntime().maxMemory();
//返回jvm总内存
long total = Runtime.getRuntime().totalMemory();

System.out.println("max="+ max + "字节\t" + (max/(double)1024*1024) + "MB");
System.out.println("total=" + total + "字节\t" + (total/(double)1024*1024) + "MB");

//默认情况下: 分配的总内存是电脑内存的1/4, 而初始化的内存: 1/64

//OOM:
//1.尝试扩大堆内存看结果
//2. 分析内存,看一下哪个地方出了问题(专业工具)
//3. 修改代码

// -Xms1024m -Xmx1024m -XX:+PrintGCDetails

}
}

堆内存调优

在一个项目中,突然出现了OOM故障,那么该如何排除研究为什么出错

  • 能够看到代码第几行出错:内存快照分析工具(MAT,Jprofier)
  • Debug,一行行分析代码

MAT,Jprofiler作用:

  • 分析Dump内存文件,快速定位内存泄漏
  • 获取堆中的数据
  • 获得大的对象

// -Xms1024m -Xmx1024m -XX:+PrintGCDetails查看GC详细信息

-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError生成对应JProfile文件查看OOM故障原因(JVM在发生内存溢出时自动的生成堆内存快照)

GC(垃圾回收机制)

GC的作用区域:堆、方法区

JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代

  • 新生代
  • 幸存区(from,to)
  • 老年区

GC两种类:轻GC(普通的GC),重GC(全局GC)

题目:

  • JVM的内存模型和分区,详细到每个区放什么
  • 对立面的分区有哪些?Eden,from,to,老年区,说说他们的特点
  • GC的算法有哪些?标记清除法,标记压缩,复制算法,引用计数器在,怎么用的?
  • 轻GC和重GC分别在什么时候发生?

常有算法

https://blog.csdn.net/weixin_37288522/article/details/103944956

  • 引用计数法(每个对象分配一个计数器记录调用次数)

  • 复制算法

    image-20221211204224114

image-20221211204820791

  • 好处:没有内存的碎片
  • 坏处:浪费了内存空间:多了一半空间永远是空to(假设对象100%存活->极端情况)空间换时间

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

  • 标记清除算法

    • 优点:不需要额外的空间
    • 缺点:两次扫描严重浪费时间,会产生内存碎片
  • 标记压缩/标记整理

    再优化:1.标记 2.压缩 3.清除

总结

内存效率:

复制算法>标记清除算法>标记压缩算法(时间复杂度)

内存整齐度:

复制算法=标记压缩算法>标记清除算法

内存利用率:

标记压缩算法=标记清除算法>复制算法

没有最好的算法,只有最合适的算法

GC: 分代收集算法

年轻代:

  • 存活率低
  • 复制算法!

老年代:

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

推荐书籍《深入理解JVM》

JMM(Java Memory Model)

  • 什么是JMM

    Java内存模型

  • 用途:

    缓存一致性协议,用于定义数据读写的规则

    JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory)

    解决共享对象可见性的问题:voliate

  • 学习过程:

    voliate:

    “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
    lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
    1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    2)它会强制将对缓存的修改操作立即写入主存;
    3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

    详见https://blog.csdn.net/qq_35757264/article/details/124493844