JVM整理
运行时数据去有哪几部分构成?
虚拟机栈
堆
方法区
程序计数器
本地方法栈
如何判断一个对象是否可以被回收?
引用计数算法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
可达性分析算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
GC的触发条件是什么?
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时或者对象分配不了,就将触发一次 Minor GC。
而 Full GC 则相对复杂,有以下条件:
调用 System.gc()只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
老年代空间不足老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
Java是根据什么来执行可达性分析的?
根据GC ROOTS。
GC ROOTS可以的对象有:
虚拟机栈中的引用对象
方法区的类变量的引用
方法区中的常量引用
本地方法栈中的对象引用
JVM七种垃圾收集器?
Serial 收集器 (复制算法,单线程,新生代)
ParNew 收集器 (复制算法,多线程,新生代)
Parallel Scavenge 收集器(多线程,复制算法,新生代,高吞吐量)
Serial Old 收集器(标记-整理算法,老年代)
Parallel Old 收集器(标记-整理算法,老年代,注重吞吐量的场景下,jdk8默认采用 Parallel Scavenge + Parallel Old 的组合)
CMS 收集器(标记-清除算法,老年代,垃圾回收线程几乎能做到与用户线程同时工作,吞吐量低,内存碎片)以牺牲吞吐量为代价来获得最短回收停顿时间-XX:+UseConcMarkSweepGC
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.9 默认垃圾收集器G1
JVM年轻代到年老代的晋升过程的判断条件是什么?
部分对象会在 From 和 To 区域中复制来复制去,如此交换 15 次(由 JVM 参数 MaxTenuringThreshold 决定,这个参数默认是 15),最终如果还是存活,就存入到老年代;
如果对象的大小大于 Eden 的二分之一会直接分配在 old,如果 old 也分配不下,会做一次 majorGC,如果小于 eden 的一半但是没有足够的空间,就进行 minorgc 也就是新生代 GC;
minor gc 后,survivor 仍然放不下,则放到老年代;
动态年龄判断 ,大于等于某个年龄的对象超过了 survivor 空间一半 ,大于等于某个年龄的对象直接进入老年代;
垃圾收集算法都有哪些
引用计数法
概述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。
优缺点:
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.而且每次加减非常浪费内存。
根搜索算法
在Java中可作为GC roots的对象可以有以下几种
虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈的引用对象
标记清除算法标记-清除(Mark-Sweep)
算法顾名思义,主要就是两个动作,一个是标记,另一个就是清除。标记就是根据特定的算法(如:引用计数算法,可达性分析算法等)标出内存中哪些对象可以回收,哪些对象还要继续用。标记指示回收,那就直接收掉;标记指示对象还能用,那就原地不动留下。
缺点1.标记与清除效率低;2.清除之后内存会产生大量碎片(因为容易造成不连续空间);所以碎片这个问题还得处理,怎么处理,看标记-整理算法。
复制算法
S0和s1将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。复制算法的缺点显而易见,可使用的内存降为原来一半。复制算法用于在新生代垃圾回收
存在于新生代S0和S1之间,缺点是空间利用率比较低。
标记-整理算法
标记压缩法在标记清除基础之上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理。(java中老年代使用的就是标记压缩法)
其实就是标记清除算法的升级版,一次GC之后整理空间然后在回收。
分代收集算法
根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。对于新生代和老年代来说,新生代回收频率很高,但是每次回收耗时很短,而老年代回收频率较低,但是耗时会相对较长,所以应该尽量减少老年代的GC.
垃圾回收时的停顿现象垃圾回收的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以更高效的执行,大部分情况下,会要求系统进如一个停顿的状态。停顿的目的是为了终止所有的应用线程,只有这样的系统才不会有新垃圾的产生。同时停顿保证了系统状态在某一个瞬间的一致性,也有利于更好的标记垃圾对象。因此在垃圾回收时,都会产生应用程序的停顿。
什么是双亲委派
在JVM虚拟机中,如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。也就是说,对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。这就是双亲委派模型。
向上委托:实际上就是查找缓存,看上层是否加载了该类,有则直接返回,没有则继续向上。当到达顶层之后,还是没有的话则开始向下查找
向下查找:查找加载路径,有则加载返回,没有则继续向下查找。
JVM组成
1、类加载子系统:
负责从文件系统或者网络加载Class信息,加载的信息存放在一块称之方法区的内存空间。
2、方法区:
就是存放类的信息、常量信息、常量池信息、包括字符串字面量和数字常量等。通常和永久区(Perm)连在一起。
3、Java堆:
和程序开发密切相关
应用系统对象都保存在Java堆中
所有线程共享Java堆
对分代GC来说,堆也是分代的
GC的主要工作区间
4、直接内存:
JavaNio库允许Java程序直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。
5、Java栈
线程私有
栈由一系列帧组成(因此Java栈也叫做帧栈)
帧保存一个方法的局部变量、操作数栈、常量池指针
每一次方法调用创建一个帧,并压栈
6、本地方法栈
最大不同为本地方法栈用于本地方法调用。
7、垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制。
8、PC(Program Couneter)寄存器也是每个线程私有的空间, Java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个Java线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法,PC寄存器总会执行当前正在被执行的指令,如果是本地方法,则PC寄存器值为Underfined,寄存器存放如果当前执行环境指针、程序技术器、操作栈指针、计算的变量指针等信息。
9、虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。
YoungGC的过程是什么?
首先,所有新创建的对象都会陪分配到年轻代的 Eden 空间,而两个 survior 空间一开始都为空。
下图表示的是运行一段实际后的年轻代内存情况,新创建的对象会被放在 Eden 空间,“from” survior space 里面的数字表示当前内存块经历的垃圾回收次数。没经历一次垃圾回收,内存块的数字就加一,所有数字越大,说明存在的时间越久。
当 Eden 空间被填满,就会触发次垃圾回收。
触发次回收,需要清理 Eden 空间的全部对象,没有被用的对象会比清除,幸存下来的对象全部被移到 s0 幸存空间,并标记经历的回收次数为 1 。若 s0 幸存空间无法装下所有从 Eden 迁移过来的幸存对象,多出来的对象会被直接迁移到老年代空间。这里忽略了上图 3, 1 内存块。
第二次进行次垃圾回收时,Eden 空间发生同样的事情,没有被引用的对象被删除,还被引用的对象被移到幸存区空间。这次移到另一个幸存区空间 s1 。若 s1 的空间不足以装下所有的来自 Eden 的幸存对象,剩余对象直接被移到老年代。上一次存储幸存空间的 s0 也被进行垃圾回收,幸存下来的对象年龄加1,并被移到 s1 幸存区。同样地,若 s1 无法装下所有移过来的幸存对象,剩余对象被直接移到老年代。清空 s0 空间。
下一次次垃圾回收,处理流程一样,只是幸存区调转,还被引用的对象被移到 s0 空间。幸存下来的对象年龄加1,Eden 和 s1 空间被清空。
根据4、5步可见,无论何时幸存区 s0, s1 中,总有一个是为空的。
这一步演示对象升迁。在某次次垃圾回收后,当一些年长的对象达到某个年纪阀门(本例子是 8 ),它们便从年轻代升迁到老年代。
随着次垃圾回收的进行,不停地升迁对象到老年代空间。
上面,几乎涵盖了年轻代的全部流程步骤。最终,会执行主垃圾回收,清理并压缩老年代空间。
类的生命周期
加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸载(Unloading)
Java的类加载器有哪些?
Jdk默认的有三个类加载器,BootstrapClassLoader,ExtClassLoader,AppClassLoader
BootstrapClassLoader 是ExtClassLoader的父类加载器,默认负责加载java_home下的lib和class文件。
ExtClassLoader是AppClassLoader的父类,负责加载java_home的lib下的ext文件夹下jar和class文件
AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。
GC Roots的根结点有哪些?
线程的本地变量,静态变量,本地方发栈
单例模式
//dcl
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
什么是值传递和引用传递?
对象被值传递,意味着传递了对象的一个副本。因此,就算是改变了对象副本,也不会影响源对象的值。
对象被引用传递,意味着传递的并不是实际的对象,而是对象的引用。因此,外部对引用对象所做的改变会反映到所有的对象上。
重载与重写?
重载:存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
重写:存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
序列化
把对象转换为字节序列的过程称为对象的序列化。 对象转成二进制流 序列化 二进制流转对象 反序列化 serialVersionUID 是一个序列化版本号,Java 通过这个 UID 来判定反序列化时的字节流与本地类的一致性,如果相同则认为一致, 可以进行反序列化,如果不同就会抛出异常。
Java序列化算法 所有保存到磁盘的对象都有一个序列化编码号 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。 如果此对象已经序列化过,则直接输出编号即可。
常用的序列化方式都有哪些?
1) Java 原生序列化方式请参考以下代码:
User user = new User();
user.setName("老王");
user.setAge(30);
System.out.println(user);
// 创建输出流(序列化内容到磁盘)
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
"test.out"));
// 序列化对象
oos.writeObject(user);
oos.flush();
oos.close();
// 创建输入流(从磁盘反序列化)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"test.out"));
// 反序列化
User user2 = (User) ois.readObject();
ois.close();
System.out.println(user2);
2) JSON 格式,可使用 fastjson 或 GSON
User user = new User();
user.setName("老王");
user.setAge(30);
System.out.println(user);
String jsonSerialize = JSON.toJSONString(user);
User user3 = (User) JSON.parseObject(jsonSerialize, User.class);
System.out.println(user3);
3)Hessian 方式序列化Hessian 序列化的优点是可以跨编程语言,比 Java 原生的序列化和反序列化效率高.
// 序列化
ByteArrayOutputStream bo = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(bo);
hessianOutput.writeObject(user);
byte[] hessianBytes = bo.toByteArray();
// 反序列化
ByteArrayInputStream bi = new ByteArrayInputStream(hessianBytes);
HessianInput hessianInput = new HessianInput(bi);
User user4 = (User) hessianInput.readObject();
System.out.println(user4);
深拷贝与浅拷贝
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
var a1 = {b: {c: {}};
var a2 = shallowClone(a1); // 浅拷贝方法
a2.b.c === a1.b.c // true 新旧对象还是共享同一块内存
var a3 = deepClone(a3); // 深拷贝方法
a3.b.c === a1.b.c // false 新对象跟原对象不共享内存