JVM探究
Posted 轻舟一曲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM探究相关的知识,希望对你有一定的参考价值。
探究
-
谈谈对JVM的理解?java8虚拟机和之前的变化更新?
-
什么是OOM,什么是栈溢出?怎么分析?
-
JVM的常用调优参数有哪些?
-
内存快照如何抓取,怎么分析Dump文件?
-
谈谈JVM的类加载器? rt-jat ext application
-
百度
-
思维导图 (在线找或者自己动手画)
JVM脑图 | ProcessOn免费在线作图,在线流程图,在线思维导图
jvm | ProcessOn免费在线作图,在线流程图,在线思维导图
JVM的位置
JVM的体系结构
方法调优99%都是在调堆。
JVM的类加载器
作用:加载class文件。
- 虚拟机自带的加载器
- 启动类(根)加载器
- 拓展类加载器
- 应用程序(系统类)加载器
第3个在rt.jar包内,第2个拓展加载器在jre/lib/ext文件夹下
双亲委派机制
定义
当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
安全:
加载器:APP->EXT->BOOT(最终执行)
若BOOT没有::(最终执行)APP<-EXT<-BOOT
沙箱安全机制
沙箱演进
JDK1.0
JDK1.1
JDK1.2
JDK1.6
沙箱组件
-
字节码校验器
:确保Java类文件遵循Java语言规范,帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。 (检查时异常) -
类加载器
:虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,他们之间互不可见。在3个方面对沙箱起作用:
- 防止恶意代码去干涉善意代码; //双亲委派机制
- 守护被信任的类库边界; //双亲委派机制
- 将代码归入保护域,确定代码可以进行哪些操作。 //沙箱机制
native本地方法接口
public static void main(String[] args) {
new Thread(()->{
System.out.println("Thread start..");
},"my thread name").start();
}
native:凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库->进入本地方法栈->调用本地方法本地接口JNI。
native在内存区域中专门开辟了一块标记区域:Native Method Stack,登记native方法,在最终执行的时候,加载本地方法库中的方法通过JNI接口。
JNI作用:拓展Java的使用,融合不同的编程语言为Java所用。
应用了解即可:Java程序驱动机,管理系统,Robot。
PC寄存器
方法区
方法区只存放:static final class 常量池
是堆中占内存比较小的。
栈
数据结构。
栈–队列;
LIFO:后进先出;
栈:栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存释放。
对于栈来说不存在垃圾回收,线程结束栈释放。
栈存放的东西:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧。
程序正在执行的方法一定是在栈顶部。
栈堆:
main方法先执行,后结束。
无限压栈溢出:
面试题:详解JAVA对象实例化过程
1 对象的实例化过程
- 对象的实例化过程是分成两部分:类的加载初始化,对象的初始化
- 要创建类的对象实例需要先加载并初始化该类,main方法所在的类需要先加载和初始化
- 类初始化就是执行方法,对象实例化是执行方法
- 一个子类要初始化需要先初始化父类
2 类的加载过程
-
类的加载机制:如果没有相应类的class,则加载class到方法区。对应着加载->验证->准备->解析–>初始化阶段
-
加载:载入class对象,不一定是从class文件获取,可以是jar包,或者动态生成的class
-
验证:校验class字节流是否符合当前jvm规范
-
准备:为 类变量 分配内存并设置变量的初始值( 默认值 )。如果是final修饰的对象则是赋值声明值
-
解析:将常量池的符号引用替换为直接引用
-
初始化:执行类构造器( 注意不是对象构造器 ),为 类变量 赋值,执行静态代码块。jvm会保证子类的执行之前,父类的先执行完毕
-
-
其中验证、准备、解析3个部分称为 连接
-
方法由 静态变量赋值代码和静态代码块 组成;先执行类静态变量显示赋值代码,再到静态代码块代码。
3 触发类加载的条件
-
第一次创建类的新对象时, 会触发类的加载初始化和对象的初始化函数执行,这个是实例初始化,其他6个都是类初始化
-
JVM启动时会先加载初始化包含main方法的类
-
调用类的静态方法(如执行invokestatic指令)
-
对类或接口的静态字段执行读写操作(即执行getstatic、putstatic指令);不过final修饰的静态字段的除外(已经赋值,String和基本类型,不包含包装类型),它被初始化为一个编译时常量表达式
- 注意 :操作静态字段时,只有直接定义这个字段的类才会被初始化;如通过其子类来操作父类中定义的静态字段,只会触发父类的初始化而不是子类的初始化
-
调用JavaAPI中的反射方法时(比调用java.lang.Class中的方法(Class.forName),或者java.lang.reflect包中其他类的方法)
-
当初始化一个类时,其父类没有初始化,则需先触发父类的初始化(接口例外)
4对象的实例化过程
-
对象实例化过程其实就是执行类构造函数 对应在字节码文件中的()方法(称之为实例构造器);()方法由 非静态变量、非静态代码块以及对应的构造器组成
- ()方法可以重载多个,类有几个构造器就有几个()方法
- ()方法中的代码执行顺序为:父类变量初始化,父类代码块,父类构造器,子类变量初始化,子类代码块,子类构造器。
-
静态变量,静态代码块,普通变量,普通代码块,构造器的执行顺序:
-
具有父类的子类的实例化顺序如下:
5类加载器和双亲委派规则,如何打破双亲委派规则
-
类加载器
-
通过一个类的全限定名来获取 描述此类的二进制字节流 ,实现这个动作的代码模块称为类加载器
-
任意一个类都需要其加载器和类本身来确定类在JVM的唯一性;每个类加载器都有自己的类名称空间,同一个类class由不同的加载器加载,则被JVM判断为不同的类
-
-
双亲委派模型
-
启动类加载器有C++代码实现,是虚拟机的一部分。负责加载lib下的类库
-
其他的类加载器有java语言实现,独立于JVM,并且继承ClassLoader
-
extention ClassLoader负责加载libext目录下的类库
-
application ClassLoader 负责加载用户路径下(ClassPath)的代码
-
不同的类加载器加载同一个class文件会导致出现两个类。而java给出解决方法是下层的加载器加委托上级的加载器去加载类,如果父类无法加载(在自己负责的目录找不到对应的类),而交还下层类加载器去加载。如下图:
-
-
打破双亲委派模型
- 双亲委派模型并不是一个强制的约束模型,而是java设计者推荐给开发者的类加载实现方式
- 双亲委派模型很好的解决各个类加载基础类的同一问题(越基础的类由越上层的加载器加载),但是基础类总是作为用户代码调用的API,但是如果它的具体实现是下层的代码,此时基础类需要调用下层的代码,则需要打破双亲委派模型
- 如JNDI服务,JNDI的代码有启动类去加载(rt.jar),它需要调用由独立厂商部署在应用程序classpath下的JNDI的SPI(Service Provider Interface)代码。为了解决SPI代码加载问题,java引入了线程上下文类加载器去加载SPI代码。也就是父类加载器请求子类去完成类的加载动作
- 线程上下文类加载器,线程创建时会从父线程继承,如果全局范围没有设置过,则默认设置为application Class Loader
3种JVM
堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
堆内存中细分三个区域:新生区(伊甸园区),老年区,永久区。
GC垃圾回收主要是在伊甸园区和老年区。
假设堆内存满了,OOM,内存爆了
JDK8以后,永久区改了名字(元空间)
新生区
- 类诞生和成长甚至死亡的地方;
- 伊甸园区:所有对象都是在这个区new出来的;
- 幸存者区0,幸存者区1。
老年区
永久区
关闭JVM才会释放这个内存。
一个启动类,加载了大量的三方jar包。比如Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载,直到内存满,报OOM错误。
方法区里面还有一小块放常量池。
测试堆大小
默认分配总内存是电脑内存的1/4.
初始化内存是1/64.
修改参数
OOM报错解决思路
JProfiler
面试问题
关联IDEA:
安装:
测试:
-Xms设置初始化内存分配大小默认1/64,
-Xmx设置最大分配内存默认1/4
-XX:+PrintGCDetails
-XX:+HeapDumOnOutOfMemoryError
GC垃圾回收器常用算法
GC常见面试题:
- JVM的内存模型和分区,详细到每个区放什么东西?
- 堆里面的分区有哪些?Eden,from,to 老年区,都有什么特点!
- GC算法有哪几种?标记清除算法,标记压缩,复制算法,引用计数器,怎么用的?
- 轻GC和重GC分别在什么时候发生?
JVM并不采用这种方式,并不高效。
GC复制算法
新生代用的主要是复制算法:
没有被GC掉的对象就会被复制到下一个区域。to区不能有对象,当GC时先将to区对象复制到from区。
每次GC过后:Eden区和to区是空的。
好处:没有内存碎片。
坏处:多一个幸存区,浪费一半的空间。
极端情况:对象100%存活就都复制!
最佳使用场景:对象存活度较低的时候。
GC标记清除算法
优点:不需要额外的空间。
缺点:时间成本,两次扫描,产生内存碎片。
GC标记压缩
对前者的优化。
压缩:防止内存碎片。
多了一个移动成本。
GC总结
面试问题:有没有最优的GC算法?没有最好的算法,只有最合适的算法。
所以GC也被称为分代收集算法。
内存调优调的就是最后标记清除什么时候清除。
JMM
Java内存模型。
定义
作用
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
JMM内存模型带来的问题:
- 可见性问题:CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中:要解决共享对象可见性这个问题,我们可以使用java volatile关键字或者是加锁。
JMM是一个抽象的概念,本质是需要学习线程同步机制:
学习三部曲:
- 是什么?
- 有什么用?
- 面试问什么?
以上是关于JVM探究的主要内容,如果未能解决你的问题,请参考以下文章