冯诺依曼模型与计算机处理数据过程相关联:
- 冯诺依曼模型:
- 输入/输出设备
- 存储器
- 运算器
- 控制器
- 处理过程:
- 提取阶段:输入设备传入原始数据,存储到存储器
- 解码阶段:由CPU的指令集架构ISA将数值解码成指令
- 执行阶段:控制器把需要计算或处理的数据调入运算器
- 最终阶段:由输出设备把运算结果返回
所以JVM是什么呢?
- 首先,可以分为前端编译和后端编译,像生成.class文件就是前端编译,后端编译通过java虚拟机执行
- 将java文件编译成.class文件之后,JVM会对字节码文件进行解释,翻译成对应平台的机器指令开始执行。屏蔽了底层操作系统,实现跨平台运行。
- 注意java是跨平台的,jvm不是跨平台的,相当于class文件就是一种规范,在不同的平台上根据这种规范进行后端编译!
- class-》java文件javap反编译
.java文件是如何变成.class文件的?
- 前端编译
类文件解读:
-
16进制
-
magic、版本号、常量池数量57个、常量池(字面量(文本、字符串、final)和符号引用(类、接口、字段、方法))、字段表集合、方法表集合
-
符号引用就是类信息、修饰符,名称等等,不直接指向
类加载机制(类加载子系统):
所谓的类加载机制就是将class文件加载进内存,并对数据进行校验,转换解析和初始化,形成JVM可以直接使用。
- 当字节码文件加载到内存的时候,是不是需要一个访问入口呀?
- 那如何将字节码文件加载进内存呢?
- 常见的从本地系统加载
- 动态代理技术,运算时计算而成
- 从jar包中加载
- 类加载过程:
- 装载:
- 通过类全限定名获取类的二进制字节流!这个过程可以做拦截增强
- 将字节流代表的静态存储结构转换为方法区的运行时数据结构
- 在堆中生成java.lang.class对象,作为对方法区中这些数据的访问入口(defineClass)
- 链接
- 验证:格式验证、字节码验证:跳转验证、符号引用验证、元数据验证:语法。-Xverify:none 取消验证。穿插在整个过程中
- 准备:为静态变量赋值,初始化成默认值。
- 这里不包含final修饰的static,final修饰的static都在编译的时候就分配了
- 不会为实例变量分配初始化,类变量在方法区中,实例变量随着对象在堆中(即在实例构造器方法中进行的)
- 解析
- 将符号引用(名称、描述符、全限定名啥的)转换成直接引用,直接指向目标的指针(方法区里的指向)
- 对解析结果进行缓存
- 初始化
- 对比于准备阶段,这里是通过指定主观计划去初始化变量和其他资源。有点懒加载的味道,需要使用的时候才加载
- 对类变量设置初始值的两种方式:
- 直接声明
- static代码块
- 步骤
- 先装载和链接本类
- 然后初始化父类
- 初始化自己
- 什么时候加载(初始化):(主动引用)
- 创建实例;final在调用构造方法前就要弄好
- 使用静态变量或者静态方法的时候
- 调用子类父类也会初始化;
- 标明启动类的类,或者像object和class类启动时候就加载了
- 反射的时候
- (被动引用)
- 定义类数组不会初始化
- 使用父类的静态变量,不会初始化
- static final不会初始化
- 卸载:
- 用完之后就卸载回收
- 所有实例被回收,classloader被gc回收,class对象没有被任何地方引用
- 装载:
- 类加载器:
- 就是读取字节码,转换成java.lang.class类的一个实例的代码模块
- 一个类在同一个类加载器中具有唯一性,不同类加载器是允许同名类存在的,类加载器不同,就不会是同一个类。
- 分类:
- bootstrap classloader:负责java_home的所有class,核心类库
- Platform classloader:负责一些扩展的包
- app classloader:classpath中指定的包
- custom classloader:根据程序自定义类加载器
- 为什么要分层?
- 如果自己编写了一个java.lang.String类,如果只有一个的话无法判定究竟要加载哪个。所以分层对信任级别进行划分
- 即使打破了双亲委派也不能重写String,defineClass限制了限定类名不能以java开头,除非自己将二进制流转换成class对象
-
protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { //此方法负责将二进制的字节码转换为Class对象 return defineClass(name, classData, 0, classData.length); } } private byte[] loadClassData(String className) { String fileName = root + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try { InputStream ins = getClass().getClassLoader().getResourceAsStream(fileName); if (ins != null) { return ins.readAllBytes(); } } catch (IOException e) { e.printStackTrace(); } return null; }
- 三个特性:
- 全盘负责:
- 当一个类加载器加载某个class的时候,该class所依赖的和引用其他的class都由该类加载器负责载入。除非显式使用另一个类加载器
- 父类委托(双亲委派):
- 首先传入类的全限定名
- 为什么:实现带有优先级的层次关系
- 从app层依次往上找是否加载过,然后又从上往下看谁能加载,最后还没有就抛异常。
- 可以重写loadclass打破双亲委派
- 打破:
- SPI,JDK提供了一套接口,定义实现类,在META-INF/services中注册实现类的相关信息,比如JDBC的DriverManager
- OSGI:热部署、热替换
- 如果怕打破可以重写findclass方法
- 缓存机制:
- 加载过的class文件在内存中(方法区)缓存
- 每一个类加载器都有自己的缓存
- 打破父类委托:
import java.io.IOException; import java.io.InputStream; public class CustomClassLoader extends ClassLoader { public CustomClassLoader(ClassLoader parent) { super(parent); } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { // 打破双亲委派 if (name.startsWith("com.example")) { // 尝试自己加载类 try { String fileName = name.replace('.', '/') + ".class"; InputStream is = getClass().getClassLoader().getResourceAsStream(fileName); if (is == null) { return super.loadClass(name); } byte[] bytes = new byte[is.available()]; is.read(bytes); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } // 委派给父类加载器 return super.loadClass(name); } public static void main(String[] args) throws Exception { CustomClassLoader customClassLoader = new CustomClassLoader(CustomClassLoader.class.getClassLoader()); Class<?> clazz = customClassLoader.loadClass("com.example.MyClass"); Object instance = clazz.newInstance(); System.out.println(instance.getClass().getName()); } }
- 全盘负责:
运行时数据区:
- 常量池:
- 静态常量池:class文件的一部分,由字面量(文本、字符串以及final修饰的)和符号引用(描述信息)组成
- 运行时常量池:静态常量池被加载到内存后就变成了运行时常量池了,class文件内容落地到内存了
- 字符串常量池:现在在堆中
- 面试常问的:
-
String a ="aaaa";
解析:最多创建一个字符串对象。先查找有没有这个,没有就创建一个,返回引用。
-
`String a =new String("aaaa");`
解析:最多会创建两个对象。常量池没有的话,就在堆中创建一个字符串对象(放字符串常量池里),再在堆中创建一个对象(独立出去),返回后面的引用。有没有都要多创建一个。
-
intern()用于将字符串对象手动添加到字符串常量池中
-
返回的是字符串常量池里的String s1 = new String("yzt"); String s2 = s1.intern(); System.out.println(s1 == s2); //false
-
-
- 方法区:
- 线程共享
- 堆的一个逻辑部分,但是也要和堆区分出来
- 存放虚拟机加载的类信息,常量、静态变量、JIT代码等等
- 当方法区无法满足内存分配的需求时,将抛出OOM异常
- 1.8后在系统内存(元空间)中,为了避免内存泄露和OOM
- 堆:
- 虚拟机管理内存中最大的一块,在虚拟机启动的时候被创建,线程共享
- 实例和数组都在堆上分配,注意java.class.class这个对象就是方法区中数据的访问入口
- 虚拟机栈:
- 假设现在已经初始化完成了要使用了,一个线程的运行状态就由虚拟机栈来保存,线程私有,随着线程的创建而创建。
- 线程执行的每一个方法,就是栈中的一个栈桢,可以看作是一个方法的运行空间。
- 程序计数器:
- 线程切换的时候,记录正在执行的字节码指令的地址
- 本地方法栈:
- 如果执行Native就在本地方法栈执行
- 如果在java方法执行的时候调用native方法,就是动态链接。
- 方法区:
- 面试常问的:
内存布局:
- 对象内存布局:
- 指针压缩:
- 在64位的操作系统中,一个指针一般是八个字节,在很多情况下,并不需要使用这么多的地址空间。压缩的主要是reference
- 因为处理器通常在对齐边界上访问内存时更快。
- 减少内存消耗,压缩成4字节。对于大型应用,节省空间。
- 更好的缓存应用:能存放更多的对象
- 提高访问速度:较小的数据结构代表着读写能够更快的完成
- 减少垃圾回收开销:扫描和标记耗时变小
- 无效情况:
- 32位超过32G
- 怎么去理解?
- 第一种理解:对象长度一定是8的整数倍,所以只用存第一个,4G*8=32G
- 第二种理解:当存入64位寄存器的时候,左移三位,末尾三个0是不需要的,索引寻址空间提高了8倍;
- java采用的大端存储,便于符号判断,小端便于类型转换
- classpointer设计:
- 句柄池访问:对象移动的时候只需要修改一个指针,但是多一次定位的时间开销
- 直接指针:节省了一次定位开销,在对象移动后,还需要修改引用
- 句柄池访问:对象移动的时候只需要修改一个指针,但是多一次定位的时间开销
- 对齐填充:
- 8字节,举个例子,如果针对开区域存储,那么就需要读两次内存,现在读一次就可以了。也可以选择策略(基本在填充前面,引用更换位置)
- 0:基本类型>填充类型>引用类型
- 1:引用类型>基本类型>填充类型
- 2:父类的引用类型和子类的引用类型放在一起,父类采用0,子类采用1,从而降低空间的开销。
- 8字节,举个例子,如果针对开区域存储,那么就需要读两次内存,现在读一次就可以了。也可以选择策略(基本在填充前面,引用更换位置)
- 怎么分配内存的:
- 指针碰撞:指针指向能用的初始位置,然后偏移
- 空闲列表:类似于维护一个控制块,哪些空闲哪些分配
运行时数据区:
- 根据GC的悲观策略,98%的对象不能达到分代年龄,如果将不同时期的放在一起分析和回收,效率就太低了
- 内存担保机制:假设在young gc后,新生代仍然有大量的对象存活,就需要老年代进行分配担保。
什么时候Full GC?
-
每次晋升的对象平均大小>剩余空间,基于历史水平计算
-
上面内存担保机制
-
永久代内存不足
-
System.GC
-
younggc后老年代空间不足
-
如何理解Minor/Major/Full GC
Minor GC:新生代 Major GC:老年代 Full GC:新生代+老年代
-
为什么需要Survivor区?只有Eden不行吗?
如果没有survivor区域,那么就会导致,每进行一次minor gc,存活的对象就要进入老年代,这样老年代就会很快填满,容易发生full gc,老年代空间又比较大。
支持对象的晋升策略!!!
可能你会说,那就对老年代的空间进行增加或者较少咯。 假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长。 假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。 所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。
-
为什么需要两个Survivor区?
最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。
-
新生代中Eden:S1:S2为什么是8:1:1?
新生代中的可用内存:复制算法用来担保的内存为9:1 可用内存中Eden:S1区为8:1 即新生代中Eden:S1:S2 = 8:1:1 现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是“朝生夕死”的
-
堆内存中都是线程共享的区域吗?
JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。 对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
JVM中有一个refill_waste的值,最大浪费空间,当TLAB的值不足这个的时候就会放弃进行重新分配。
常用的命令及文件?
- dump文件:java虚拟机的运行时快照,保存状态信息和信息到文件。
- 线程dump:包括所有线程的运行状态,纯文本格式
- 堆dump:包含线程dump,并包含所有堆对象的状态。二进制
- 主要在多线程开发、内存泄露。
- -XX:+HeapDumpOnOutOfMemoryError内存不足时直接打印dump
- jps:
- 显示当前运行的java进程及相关参数
- jstack:
- 生成栈dump,定位线程长时间停顿的原因,死锁、死循环等等
- jmap:
- 生成堆dump,内存不足、GC异常,怀疑内存泄露
- jhat:
- 将jmap生成的堆dump文件转换成html的形式,通过http访问。
- javap:
- 反编译
- jstat:
- 监控虚拟机各种运行状态信息。类加载、内存、垃圾回收、JIT等
CPU飙高怎么处理?
CPU飙高一定是某个进程长时间占用了CPU资源,有可能是内存泄露,也有可能是活锁之类的
首先top列出进程占用资源的情况,然后通过top -Hp pid找出对应的是哪个线程占用了,先把线程ID转换成16进制printf "%x\n" 10086,然后jstack -l PID >./xxx.txt打印线程信息,然后根据线程的堆栈信息对应的去查找代码问题。
内存飙高怎么排查?
查看堆外内存jcmd <pid> VM.native_memory summary
查看方法区 jcmd <pid> GC.class_histogram
设置方法区内存:java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
设置直接内存:-XX: MaxDirectMemorySize=2048M
飙高大概率就是一次性创建了大量的对象,垃圾回收的速度赶不上对象创建的速度了。
- 先观察垃圾回收的情况,通过jstat -gc PID 1000,每一秒打印一次GC信息
- jmap -histo PID|head -20 查看前二十个占用堆内存空间最大的对象
- 如果GC频繁,每次回收的内存空间也正常,那就是创建的太快了。如果回收的很少,就有可能是内存泄漏导致出现了问题
- 然后导出堆内存文件快照
- jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump 堆内存信息到文件。
- 然后使用visualVM对dump文件进行分析
- 最后可以调整如调整
-Xmx
和-Xms
参数来增加最大和初始堆内存。然后换别的垃圾收集器
频繁minor gc怎么办?
新生代空间小,通过 -Xmn增大来降低回收频率
频繁fullGC怎么办?
- 首先是看是什么原因导致的?是大对象,还是内存泄露?或者说是代码中调用了System.gc还是说参数设置问题导致的。
- 可以用jstat、jmap等去查看。JVisualVM
- 然后就相应的去看哪里出现了问题。
内存泄露定位?内存溢出定位?
严重的内存泄漏可能会导致CPU飙升,因为一直GC去了,同时也会有OOM的错误
严重的内存泄露往往伴随着频繁的Full GC,所以先从Full GC入手,首先通过jps查看java运行的pid,然后用top -p pid查看进程的CPU和内存使用情况,然后就是top -Hp pid查看线程的使用情况,将线程id转换成16进制,通过jstack 抓取线程栈信息,jstat输出GC信息,通常会有YoungGC很慢,FULL GC很快的情况,最后就生成dump文件进行分析了。
手写内存溢出的例子?
常见的内存溢出情景:
- 堆内存溢出
- 虚拟机栈内存溢出
- 方法区溢出
- 直接内存溢出,ByteBuffer.allocateDirect
- 本地方法栈溢出
对于复杂的对象,特别是需要动态调整大小和跨越方法边界的对象(如ArrayList),它们通常会被分配在堆上
public class HeapSpaceErrorGenerator {
public static void main(String[] args) {
List<byte[]> bigObjects = new ArrayList<>();
try {
while (true) {
// 创建一个大约 10MB 的数组
byte[] bigObject = new byte[10 * 1024 * 1024];
bigObjects.add(bigObject);
}
} catch (OutOfMemoryError e) {
System.out.println("OutOfMemoryError 发生在 " + bigObjects.size() + " 对象后");
throw e;
}
}
}
通过参数-Xmx128M设置堆内存的大小
内存泄露的原因?
public class OOM {
static List list = new ArrayList();
public void oomTests(){
Object obj = new Object();
list.add(obj);
}
}
- 静态集合类:在这段代码里面,静态集合会一直持有obj的引用,所以无法释放
- 单例模式:
- 回顾单例模式三要素
- 私有的构造方法
- 私有的静态实例变量
- 共有的静态方法
- 初始化后,会随着JVM的生命周期一直存在,不会类卸载
- 回顾单例模式三要素
try {
Connection conn = null;
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("url", "", "");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("....");
} catch (Exception e) {
}finally {
//不关闭连接
}
- 数据连接、IO、SOCKET等
- 比如JDBC的连接
- 变量不合理的作用域:
- 定义的作用域大于使用范围,但是又不置为null
- 比如本来栈上的对象,分配到了堆上,不会立马释放!
- ThreadLocal使用不当
- ThreadLocalMap中key是ThreadLocal,value是存储的东西
- remove的时候是移除map中的整个entry
- 不移除虽然有自动清理,但是还是会存在一会儿
可以作为GCROOTS的有哪些?
- 虚拟机栈中的引用
- 活着的线程
- 被synchronized持有的对象
- 本地方法栈中的JNI引用
- 记忆集!!!
- 类静态变量
- 运行时常量池中的常量,比如String、Class类型
public class ConstantPoolReference {
public static final String CONSTANT_STRING = "Hello, World"; // 常量,存在于运行时常量池中
public static final Class<?> CONSTANT_CLASS = Object.class; // 类类型常量
public static void main(String[] args) {
System.out.println(CONSTANT_STRING);
System.out.println(CONSTANT_CLASS.getName());
}
}
四种引用
首先讲一下ReferenceQueue,它是Java中的一个类,用于配合软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)一起使用,用于跟踪对象的引用是否被垃圾回收器回收。
- 强引用就是普通new对象
- 软引用在内存溢出之前,描述一些还有用,但是非必须的对象,做缓存
- 弱引用就是描述一些非必须的对象,垃圾回收就回收
- 幽灵引用,没什么影响,做一些对象的回收监控,常常配合ReferenceQueue一起使用
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
//软
SoftReference reference = new SoftReference(obj, queue);
//弱
WeakReference reference = new WeakReference(obj, queue);
//虚
PhantomReference reference = new PhantomReference(obj, queue);
//强引用对象滞空,保留软引用
obj = null;
finalize()
正式宣告一个对象死亡,首先是通过可达性分析,然后进行一次筛选,筛选的条件是是否有必要执行finalize方法,如果执行过了就跳过,没有就放到一个队列里面进行第二次标记,如果与引用链上的任何一个相关联,就不回收。免死金牌!
可达性分析后,判断有没有finalize,然后判断有没有执行过,如果和引用链关联起来了就不回收。
进入老年代的时机?
- 15次长期存活的对象,对象头中MarkWord的中有年龄
- 大对象直接进入,一般是数组,长字符串
- 如果survivor区域的对象小于等于某个年龄的占据一半以上,那么比这个年龄大的就会进去堆里
- 空间担保机制,survivor无法容纳的直接送入老年代
什么是STW?什么是OopMap?什么是安全点?
垃圾回收过程中,会涉及到对象的移动,为了保证引用更新的正确性,必须暂停线程。
OopMap是一个数据结构表,类完成类加载动作的时候就会将对象内什么偏移量上是什么类型的数据计算出来记录到OopMap。在JIT编译过程中也会在特定位置生成OopMap,记录下栈上和寄存器那里是引用。这些特定的位置主要在
- 循环的末尾
- 方法临返回前/调用方法的call指令后
- 可能抛出异常的位置
safepoint就是代码执行的安全位置,当线程执行到这里的时候,可以认为处于安全状态,就可以暂停,简单来说就是JVM需要对线程进行挂起的时候,会等到安全点再执行。
为什么有了CMS还要有G1?
- CMS主要是针对老年代,采用了标记清除
- CMS吞吐量设计的不是很好,并发标记影响系统性能
- CMS可终止的并发预处理会导致5s的停顿
- foreground会导致fullgc
- 全局MSG整理
- CMS单线程双线程效率很低
- G1分代收集,younggc、mixedgc、fullgc前两者标记复制,后面标记整理清除碎片
- G1是原始快照、CMS是增量更新
- G1支持动态调整,可预测的停顿
项目用的垃圾回收器?
Parallel Scavenge + Parallel Old默认
也可以说Parallel New
+CMS,降低停顿时间
创建对象过程?
- 查看类是否被加载过
- 分配内存:指针碰撞、空闲列表
- 内存空间初始化为0值
- 设置对象头
- 调用构造方法(创建对象)
- 返回对象引用
- 最后两步有线程安全问题
为什么JVM不使用操作系统的内存设计?
- 更细粒度的控制内存,主要是针对于GC和对象生命周期
- java是跨平台的