介绍
JVM概念:Java Virtual Machine
:java程序的运行环境(java二进制字节码的运行环境)。
区别:
JVM + 基础类库 = JRE
JVM + 基础类库 + 编译归工具 = JDK
类加载器:ClassLoader
JVM内存结构:方法区(Method Area)、堆(Heap)、虚拟机栈(JVM Stacks)、程序计数器(PC Register)、本地方法栈(Nataive Method Stacks)。
执行引擎:解释器(Interpreter)、即时编译器(JIT Compiler)、垃圾回收(GC)。
JVM内存结构
JVM内存结构之程序计数器
指令执行流程:jvm指令交给解释器,解释器再翻译成机器码,机器码再交给CPU运行。
程序计数器(寄存器)(Program Counter Register)作用:记住下一条jvm指令的执行地址。
程序计数器特点:每个线程都有自己的程序计数器,是线程私有的。不会存在内存溢出。
JVM内存结构之虚拟机栈
Java虚拟机栈(Java Virtual Machine Stacks):每个线程运行时需要的内存空间,称为虚拟机栈。
栈帧:每个方法调用运行时候需要的内存。一个栈内可能有多个栈帧。比如,方法一调用了方法二,方法二调用了方法三。
活动栈帧:正在执行的那个方法,即栈顶部的栈帧。每个线程只能有一个活动栈帧。
垃圾回收是否涉及栈内存? 不需要,弹出栈。
栈内存分配越大越好吗?递归时候大内存较好,但是栈内存大了会影响线程数量。
方法内的局部变量是否线程安全? 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。下面代码,只有m1()
方法是线程安全的。
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
栈内存溢出(java.lang.StackOverflowError):栈帧过多导致栈内存溢出(比如,递归);栈帧过大导致栈内存溢出(该情况较少)。
JVM内存结构之本地方法栈
本地方法:不是java代码编写的方法。这些本地方法使用的内存就是本地方法栈。
protected native Object clone() throws CloneNotSupportedException;
JVM内存结构之堆
堆(Heap):通过new关键字,创建对象都会使用堆内存。
堆特点:它是线程共享的,堆中对象都需要考虑线程安全的问题;有垃圾回收机制。
内存溢出(java.lang.OutOfMemoryError)
JVM内存结构之方法区
方法区:所有Java虚拟机线程共享的区域。它存储了和类结构相关的信息,如运行时常量池,成员变量,方法,构造方法。
方法区在虚拟机启动时候就被创建了。方法区也会内存溢出。
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池:常量池是*.class
文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
常量池中的信息,都会被加载到运行时常量池中。
StringTable
JVM1.6中StringTable存放在永久代方法区中;1.8中将StringTable存放在堆中了。
StringTable是hashtable结构,不能扩容。
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() -> new String("ab")
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s3 == s4);//false,s3在串池中,s4在堆中
System.out.println(s3 == s5);//true
intern()
:将字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回。
String x = "ab";
String s = new String("a") + new String("b");// 堆
String s2 = s.intern();
System.out.println( s2 == x);//true
System.out.println( s == x );//false
面试题
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2; // new String("ab")
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd")
String x1 = "cd";
x2.intern();//x2入池失败
System.out.println(x1 == x2);//false
// 调换了位置
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();//x2入池成功
String x1 = "cd";
System.out.println(x1 == x2);//true
StringTable 特性:
常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是 StringBuilder (1.8)
字符串常量拼接的原理是编译期优化
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
StringTable会GC垃圾回收。
垃圾回收
判断对象是否可以回收之引用计数法
被引用几次就计数几,当计数零时候就没有引用的了,可以回收。弊端:两个对象相互引用,就无法回收。
判断对象是否可以回收之可达性分析算法
Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
根对象(GC Root):肯定不能当成垃圾被回收的对象称为根对象。一个对象没有被根对象直接或者间接引用,就可以被回收。
扫描堆中的对象,看是否能够沿着GC Root
对象为起点的引用链找到该对象,找不到,表示可以回收该对象。
五种引用
强引用:只有所有 GC Roots
对象都不通过强引用
引用该对象,该对象才能被垃圾回收。
软引用(SoftReference):仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象;可以配合引用队列来释放软引用自身。
弱引用(WeakReference):仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。可以配合引用队列来释放弱引用自身。
虚引用(PhantomReference):必须配合引用队列使用,主要配合 ByteBuffer
使用,被引用对象回收时,会将虚引用入队,由Reference Handler
线程调用虚引用相关方法释放直接内存。
终结器引用(FinalReference):无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer
线程通过终结器引用找到被引用对象并调用它的 finalize
方法,第二次GC时才能回收被引用对象。
强引用和软引用代码:
private static final int _4MB = 4 * 1024 * 1024;
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
// 软引用:list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
软引用配合引用队列使用,自动清除无用的软引用:
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while( poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("===========================");
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
弱引用演示代码:
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
垃圾回收算法之标记清除
Mark Sweep
:先标记垃圾,再清除垃圾。优点:速度快。缺点:空间不连续,产生内存碎片。
垃圾回收算法之标记整理
Mark Compact
:优点:不会产生内存碎片,缺点:速度慢。
垃圾回收算法之复制
Copy
:内存空间划分为FROM
和TO
两个区域。在FROM垃圾回收后,复制到TO区域,不会有内存碎片,然后TO变成了FROM。需要占用双倍内存空间。
分代垃圾回收
堆内存分为:老年代(长时间使用的内存区域)和新生代(分为:伊甸园eden、幸存区From、幸存区To)。
新生代new generation
垃圾回收:Minor GC
。
老年代tenured generation
垃圾回收:Full GC
。
对象首先分配在伊甸园eden区域。新生代空间不足时,触发 minor gc
,使用复制垃圾回收算法
,将伊甸园
和from
存活的对象使用 copy
复制到 to
中,存活的对象年龄加1
并且交换 from
to
。
minor gc
会引发 stop the world
,暂停其它用户的线程(因为对象地址会改变),等垃圾回收结束,用户线程才恢复运行。
当新生代中的对象年龄超过阈值时,会晋升至老年代,最大寿命是15(4bit)。
当老年代空间不足,会先尝试触发 minor gc
,如果之后空间仍不足,那么触发 full gc
,stop the world
的时间更长。老年代垃圾回收的算法是标记清除或者标记整理。
首次存储一个新对象:如果新生代空间不足,老年代充足,会直接存储在老年代中;如果新生代空间不足,老年代空间也不足,会OOM。
子线程的OOM不会导致主线程的结束。
相关VM参数:
堆初始大小 | -Xms |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
垃圾回收器
串行:单线程,堆内存较小,适合个人电脑。XX:+UseSerialGC = Serial(新生代-复制算法) + SerialOld(老年代-标记整理算法)
。
吞吐量优先:多线程,堆内存较大,多核cpu。让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高。
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
响应时间优先:多线程,堆内存较大,多核cpu。尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5。
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark