什么是JVM
JVM
内存结构
虚拟机的前世今生
从虚拟机的发展到未来的技术发展
- Java SE体系架构
- JDK:Java开发环境
- JRE:(class)Java运行环境
- JVM: 解析class翻译成操作系统认识的指令
- 为什么要了解虚拟机?
- 写出更好,更优雅的Java程序
- 排查问题,Java应用性能优化
- 面试必问
- 虚拟机的发展
- HotSpot VM(SUM) 现(ORAVLE)
以前使用范围最广的虚拟机
目前使用最广泛的虚拟机 - JRockit VM(BEA)
号称“世界上最快的Java虚拟机” - J9 VM(IBM)
- Dalvik VM (Google)
- HotSpot VM(SUM) 现(ORAVLE)
未来的Java技术
更强的垃圾回收:ZGC TB 10毫秒 有色指针、加载屏障
JVM整体介绍
-
JVM整体介绍
-
JVM运行时数据区
JVM在运行过程中会把它所管理的内存划分成若干不同的数据区域!- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:堆、方法区
-
程序计数器(唯一不会OOM区域)
指向当前线程正在执行的字节码指令的地址(行号)
为什么需要程序计数器(面试)
- Java是多线程的,意味着线程切换
- 确保多线程的情况下程序正常执行
栈(Stack):数据结构
- 入口和出口只有一个
- 入栈
- 出栈
特点:先进后出(FILLO)
为什么JVM要使用栈?
C、B、A方法一个个入栈、A最先出栈,其次是B,最后是C
虚拟机栈(大小设置 -Xss 1M)
存储当前线程运行方法所需的数据,指令,返回地址
栈帧
每个方法在执行的同时都会创建一个栈帧
栈帧还可以划分:
- 局部变量表
- 操作数栈
- 动态连接
- 返回地址
代码理解
public class JavaStack {
public void good(int money){
money=money-100;
}
public static void main(String[] args) {
JavaStack javaStack=new JavaStack();
javaStack.good(10000);
}
}
使用javap 反编译JavaStack.class 文件
javap -v JavaStack.class >a.txt
得到txt文本文件
本地方法栈
- 本地方法栈保存的是native方法的信息,当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单的动态链接并直接调用native方法。
虚拟机规范无强制规定,各版本虚拟机自由实现
HotSpot直接把本地方法栈和虚拟机栈合二为一
方法区(永久代、元空间)
- 类信息
- 常量
- 静态变量
- 即时编译期编译后的代码
Java堆(-Xms: -Xmx: -Xmn)
堆是需要重点关注的一块区域,因为涉及到内存的分配(new 关键字,反射等)与回收(回收算法,收集器等)
JVM各版本内存区域的变化
运行时常量池
Class文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
符号引用
字面量
JDK1.6
运行时常量池在方法区中
JDK1.7
运行时常量池在堆中
JDK1.8
去永久代:使用元空间代替永久代
永久代参数 -XX:PermSize: -XX:MaxPerSize
元空间参数 -XX:MetaspaceSize: -XX:MaxMetaspaceSize
why?
永久代来存储类信息、常量、静态变量等数据不是个好主意,很容易遇到内存溢出问题。
对永久代进行调优是很困难的,同时将元空间与堆的垃圾回收进行了隔离,避免永久代引发的Full GC和OOM等问题。
直接内存
JVM直接管理不了的
直接内存:不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域:
- 如果使用了NIO这块区域会被频繁的使用,在java堆内可以用directByteBuffer对象直接引用并操作;
- 这块内存不受java堆大小限制,但受本机总内存限制,可以通过MaxDriectMenorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;
- 避免了在Java堆和Native堆中来回复制数据,能够提高效率
站在线程的角度看
深入分析栈和堆
功能
- 以栈帧的方式存储方法调用过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
- 而堆内存用来存储Java中的对象。无论是成员变量,局部变量还是类变量,它们指向的对象都存储在堆内存中;
线程独享还是共享
-
栈内存属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解为线程的私有内存。
-
SE技术体系以及JVM整体认识
-
玩转堆和栈的内存结构
-
虚拟机栈执行过程
-
深入辨析堆和栈
JVM中的对象
JVM中对象的分配
指针碰撞-空闲列表
本地线程分配缓冲
Thread Local Allocation Buffer TLAB
- 对象的内存布局
对象头(8个字节的整数)
hashcode、GC垃圾回收(年龄)、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
类型指针- 对象是哪个类的实例拥有的。
对象数据
对齐填充
对象的大小必须是8个字节。实例数据7个字节 填充1个,1个填充7个 - 对象的访问方式
HotSpot 使用直接指针,更关心对象的访问速度 - 堆内存分配策略
- 堆进一步划分
- 新生代(PSYoungGen)
- Eden空间
- From Survivor空间
- To Survivor空间
- 老年代(ParOldGen)
- 新生代(PSYoungGen)
- 堆进一步划分
堆中参数配置:
新生代大小:-Xmn20m 表示新生代大小为20m(初始和最大)
-XX:SurvivorRatio=8 表示Eden和Survivor的比值,缺省为8表示 Eden:From=8:1:1
2 Eden:From:To=2:1:1
- 对象优先在Eden区分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
- 动态对象年龄绑定
- 空间分配担保
示例1 对象优先在Eden区分配
打印GC日志 观察Eden from to 分别的比例
/**
* @program: mydemo
* @author: Mr.zeng
* @create: 2021-01-20 16:16
* -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
* 对象优先在Eden区分配
**/
public class EdenAllocation {
private static final int _1MB=1024*1024;//1MB
public static void main(String[] args) {
byte[] b1,b2,b3,b4;
b1=new byte[_1MB];
b2=new byte[_1MB];
b3=new byte[_1MB];
b4=new byte[_1MB];
}
}
VM参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
打印结果:
Heap
PSYoungGen total 9216K, used 6262K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 76% used [0x00000000ff600000,0x00000000ffc1dbe8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
Metaspace used 3282K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
示例2 大对象直接进入老年代
/**
* @program: mydemo
* @author: Mr.zeng
* @create: 2021-01-20 16:30
*
* 大对象直接进入老年代
-Xms20m
-Xmx20m
-Xmn10m
-XX:+PrintGCDetails
-XX:PretenureSizeThreshold=4m
-XX:+UseSerialGC
**/
public class BigAllocation {
private static final int _1MB=1024*1024;//1M大小
/*大对象直接进入老年代()*/
public static void main(String[] args) {
byte[] b1,b2,b3;
b1=new byte[1*_1MB];//这个对象在Eden区
b2=new byte[1*_1MB];//这个对象在Eden区
b3=new byte[5*_1MB];//这个对象直接进入老年代
}
}
VM参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
打印结果:
Heap
def new generation total 9216K, used 4214K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff01dbc8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 5120K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 50% used [0x00000000ff600000, 0x00000000ffb00010, 0x00000000ffb00200, 0x0000000100000000)
Metaspace used 3281K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
空间担保
HandlePromotionFailure
Java中的泛型
K,V,T, E<K,V>
- 泛型是什么
- 泛型类和泛型接口、泛型方法
- 我们为什么需要泛型
- 虚拟机是如何实现泛型的?
- 泛型擦除
- 弱记忆(版本的兼容性)
垃圾回收算法与垃圾回收器
GC以及GC的算法
- 学习垃圾回收的意义
Java与C++的区别
GC(Garbage Collection)- GC的自动化时代
- 谁需要GC?
- 栈(线程)—不需要
- 堆(对象)、方法区(效率低)。
- GC要做的事
- Where/Which?
- When?
- How?
- 为什么我们要去了解GC和内存分配?
- GC如何判断对象的存活
- 引用计数算法
- 可达性分析(Java)
在Java中,可作为GC Root的对象包括:- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 虚拟机栈(本地变量表引用的对象)
- 本地方法栈JNI(Native方法)中引用的对象
- 请忘记 “finalize” 、
- 各种引用(Reference)
引用
传统定义:Reference中存储的数据代表的是另一块内存的起始地址。- 强引用 =
- 软引用 SoftReference
- 弱引用 WeakReference
- 虚引用 PhantomReference
- 什么时候会发生GC ?
- Minor GC
- Full GC
- 想要了解GC,先需了解GC算法
示例代码1 (软引用) 虚拟机内存不够的时候,GC时就会把软引用回收掉
场景:缓存
import java.lang.ref.SoftReference;
import java.util.LinkedList;
import java.util.List;
/**
* @author
* 软引用
*
* VM参数
-Xms10m -Xmx10m -XX:+PrintGCDetails
*/
public class TestSoftRef {
//对象
public static class User{
public int id = 0;
public String name = "";
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}
}
//
public static void main(String[] args) {
User u = new User(1,"King"); //new是强引用
SoftReference<User> userSoft = new SoftReference<User>(u);
u = null;//干掉强引用,确保这个实例只有userSoft的软引用
System.out.println(userSoft.get()); //看一下这个对象是否还在
System.gc();//进行一次GC垃圾回收 千万不要写在业务代码中。
System.out.println("After gc");
System.out.println(userSoft.get());
//往堆中填充数据,导致OOM
List<byte[]> list = new LinkedList<>();
try {
for(int i=0;i<100;i++) {
System.out.println("*************"+userSoft.get());
list.add(new byte[1024*1024*1]); //1M的对象
}
} catch (Throwable e) {
//抛出了OOM异常时打印软引用对象
System.out.println("Exception*************"+userSoft.get());
}
}
}
示例代码2 (弱引用)每一次进行垃圾回收时都会被回收
场景:ThreadLocal,weakHashMap
public class TestWeakRef {
public static class User{
public int id = 0;
public String name = "";
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}
}
public static void main(String[] args) {
User u = new User(1,"King");
WeakReference<User> userWeak = new WeakReference<User>(u);
u = null;//干掉强引用,确保这个实例只有userWeak的弱引用
System.out.println(userWeak.get());
System.gc();//进行一次GC垃圾回收
System.out.println("After gc");
System.out.println(userWeak.get());
}
}
-
复制算法(Copying)
- 优点
简单高效,不会出现内存碎片问题 - 缺点
内存利用率低,只有一半
存活对象较多时,效率明显会降低
新生代使用(From To)
新生代Eden from to 8:1:1
Java中大部分的对象98%是不需要回收的。2%
保险起见:10%的对象需要回收10%(from)+10% (to)(预留)
回收的区域小一点,效率就越高
- 优点
-
标记-清除算法(Mark-Sweep)
- 优点
利用率百分之百 - 缺点
标记和清除的效率都不高(对比复制算法)
会产生大量的不连续的内存碎片
- 优点
-
标记-整理算法(Mark-Comact)
- 优点
利用率百分之百
没有内存碎片 - 缺点
标记和清除的效率都不高
效率相对标记清除效率要低
- 优点
-
把算法们都用上(JVM中的垃圾回收器)
- 分代收集
单线程与多线程
并行和并发
并行:垃圾收集的多线程同时进行
并发: 垃圾收集的多线程和应用的多线程同时进行。
- 分代收集
-
简单的垃圾回收器工作示意图
-
CMS垃圾回收器工作示意图
Concurrent Mark Sweep(CMS)垃圾回收过程
- 初始标记:仅仅只是标记一下 GCRoots 能直接关联到的对象,速度很快需要停顿(STW,Stop the World)
- 并发标记:从GCRoot开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW),这个阶段的停顿时间会比初始标记阶段稍微长一些,但远比并发标记的时间短。
- 并发清除:不需要停顿