JVM
看图回顾
类加载器
- 加载:查找并加载类的二进制数据
- 链接:
- 验证:保证被加载的类的正确性;
- 准备: 给类静态变量分配内存空间,赋值一个默认的初始值;
- 解析:把类中的符号引用转换为直接引用
- 初始化:给类的静态变量赋值正确的值;
public class Test{
public static int a = 1;
}
// 1、加载 编译文件为 .class 文件,通过类加载,加载到JVM
// 2、连接
验证(1) 保证Class类文件没有问题
准备(2) 给int类型分配内存空间,a = 0;
解析(3) 符号引用转换为直接引用
// 3、初始化
经过这个阶段的解析,把1 赋值给 变量 a;
类的加载(static)
package com.threestooges.core.JVM.lesson1;
import java.util.UUID;
/**
* @author ${Author}
* @Date 2020/3/10
* @Description:
*/
public class Lesson {
public static void main(String[] args) {
System.out.println(MyParent02.str);
/**
* MyParent1 static
* MyChild1 static
* hello,str2
*/
System.out.println(MyParent03.str);
/**
* hello world
*/
System.out.println(MyParent04.str);
/**
* MyParent04 static
* 7a292044-36c6-4326-b3e1-31596f5e55ee
*/
}
}
class MyParent01{
public static String str = "hello world 1";
static {
System.out.println("MyParent01 static"); // 这句话会输出吗?
}
/**
* final 常量在编译阶段的时候 常量池;
* 这个代码中将常量放到了 Demo03 的常量池中。之后 Demo03与MyParent02 就没有关系了
*/
}
class MyParent02 extends MyParent01{
public static String str = "hello world";
static {
System.out.println("MyParent02 static"); // 这句话会输出吗?
}
}
class MyParent03 {
public static final String str = "hello world";
static {
System.out.println("MyParent03 static"); // 这句话会输出吗?
}
/**
* final 常量在编译阶段的时候 常量池;
* 这个代码中将常量放到了 Demo03 的常量池中。之后 Demo03与MyParent02 就没有关系了
*/
}
class MyParent04{
/**
* 当一个常量的值并非编译期间可以确定的,那这个值就不会被方法调用类的常量池中!
* 程序运行期间的时候,回主动使用常用所在的类
*/
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("MyParent04 static"); // 这句话会输出吗?
}
}
ClassLoader 分类
1、java虚拟机自带的加载器
- BootStrap: 根加载器 (加载系统的包,JDK 核心库中的类 rt.jar)
-
rt.jar jdk 出厂自带的,最高级别的类加载器要加载的!
-
- Ext: 扩展类加载器 (加载一些扩展jar包中的类)
- Sys/App: 系统(应用类)加载器 (我们自己编写的类)
2、用户自己定义的加载器
- ClassLoader,只需要继承这个抽象类即可,自定义自己的类加载
委派双亲机制:
一个类加载器在接到加载类的请求的时候,首先他不会加载,他会委托给父类加载器,依次递归.
如果父类加载器可以加载,就会完成加载工作并成功返回。如果父类完成不了,才自己加载。
// Demo01
public class Demo01 {
public static void main(String[] args) {
Object o = new Object(); // jdk 自带的
Demo01 demo01 = new Demo01(); // 实例化一个自己定义的对象
// null 在这里并不代表没有,只是Java触及不到!
System.out.println(o.getClass().getClassLoader()); // null
System.out.println(demo01.getClass().getClassLoader()); // AppClassLoader
System.out.println(demo01.getClass().getClassLoader().getParent()); // ExtClassLoader
System.out.println(demo01.getClass().getClassLoader().getParent().getParent()); // null
// 思考:为什么我们刚才自己定义的 java.lang.String 没有生效?
// jvm 中有机制可以保护自己的安全;
// 双亲委派机制 : 一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推
// Demo01
// AppClassLoader 03
// ExtClassLoader 02
// BootStrap (最顶层) 01 java.lang.String rt.jar
// 双亲委派机制 可以保护java的核心类不会被自己定义的类所替代
}
}
native方法
native : 只要是带了这个关键字的,说明 java的作用范围达不到,只能去调用底层 C 语言的库!
JNI : Java Native Interface (Java 本地方法接口)
程序计数器
每个线程都有一个程序计数器,是线程私有的。
程序计数器就是一块十分小的内存空间;几乎可以不计。
- 作用: 看做当前字节码执行的行号指示器;
- 分支、循环、跳转、异常处理!都需要依赖于程序计数器来完成!
bipush
将 int、float、String、常量值推送值栈顶;
istore
将一个数值从操作数栈存储到局部变量表;
iadd
加
imul
乘
方法区(Method Area)
Method Area 方法区 是 Java虚拟机规范中定义的运行是数据区域之一,和堆(heap)一样可以在线程之间共享!
JDK1.7之前
永久代:用于存储一些虚拟机加载类信息,常量,字符串、静态变量等等。。。。这些东西都会放到永久代中;
永久代大小空间是有限的:如果满了 OutOfMemoryError:PermGen
JDK1.8之后
彻底将永久代移除 HotSpot jvm ,Java Heap 中或者 Metaspcace(Native Heap)元空间;
元空间就是方法区在 HotSpot jvm 的实现;
方法区重要就是来存:类信息,常量,字符串、静态变量、符号引用、方法代码。。。。。。
元空间和永久代,都是对JVM规范中方法区的实现。
元空间和永久代最大的区别:元空间并不在Java虚拟机中,使用的是本地内存!
栈Stack(※)
栈和队列都是基本的数据结构;
队列:FIFO(First Input First OutPut)
栈就是管理程序运行的
栈存储 基本类型和引用类型:
基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress
引用类型包括:类类型,接口类型和数组。
1. 程序的运行就是压栈的过程;
2. 栈的优势: 存取速度快,仅次于寄存器;
3. 栈的数据不能共享;
4. 没有回收来及的问题,只要线程结束就清空;
5. 组成元素“栈帧”
栈+堆+方法区关系图(Hotspot)
堆 heap(※)
java 7 前: 一个JVM实例是只有一个堆,堆内存大小是可以通过参数调整, 可以存类、方法、常量、保存类型引用的真实信息。
堆内存在“逻辑上”分为三部分:
物理上只有 新生、养老;元空间在本地内存中,不在JVM中!
GC 垃圾回收主要是在 新生区和养老区,又分为 普通的GC 和 Full GC,如果堆满了,就会爆出 OutOfMemory
新生区
新生区 就是一个类诞生、成长、消亡的地方!
新生区细分: Eden、s(from to),所有的类Eden被 new 出来的,慢慢的当 Eden 满了,程序还需要创建对象的时候,就会触发一次轻量级GC;清理完一次垃圾之后,会将活下来的对象,会放入幸存者区(),… 清理了 20次之后,出现了一些极其顽强的对象,有些对象突破了15次的垃圾回收!这时候就会将这个对象送入养老区!运行了几个月之后,养老区满了,就会触发一次 Full GC;假设项目1年后,整个空间彻彻底底的满了,突然有一天系统 OOM,排除OOM问题,或者重启;
Sun HotSpot 虚拟机中,内存管理(分代管理机制:不同的区域使用不同的算法!)
99% 的对象在 Eden 都是临时对象;
养老区
15次都幸存下来的对象进入养老区,养老区满了之后,触发 Full GC。
默认是15次,可以修改
永久区(Perm)
放一些 JDK 自身携带的 Class、Interface的元数据;
几乎不会被垃圾回收的;
JDK1.6之前: 有永久代、常量池在方法区;
JDK1.7:有永久代、但是开始尝试去永久代,常量池在堆中;
JDK1.8 之后:永久代没有了,取而代之的是元空间;常量池在元空间中;
OutOfMemoryError:PermGen
在项目启动的时候永久代不够用了?加载大量的第三方包!
堆内存调优(初识,基于HotSpot, jdk1.8)
maxMemory: 虚拟机试图使用的最大的内存量 一般是物理内存的 1/4
totalMemory: 虚拟机试图默认的内存总量 一般是物理内存的 1/64
/**
* 默认情况:
* maxMemory : 1808.0MB (虚拟机试图使用的最大的内存量 一般是物理内存的 1/4)
* totalMemory : 123.0MB (虚拟机试图默认的内存总量 一般是物理内存的 1/64)
*/
// 我们可以自定堆内存的总量
// -XX:+PrintGCDetails; // 输出详细的垃圾回收信息
// -Xmx: 最大分配内存; 1/4
// -Xms: 初始分配的内存大小; 1/64
// -Xmx1024m -Xms1024m -XX:+PrintGCDetails
public class Demo01 {
public static void main(String[] args) {
// 获取堆内存的初始大小和最大大小
long maxMemory = Runtime.getRuntime().maxMemory();
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("maxMemory="+maxMemory+"(字节)、"+(maxMemory/1024/(double)1024)+"MB");
System.out.println("totalMemory="+totalMemory+"(字节)、"+(totalMemory/1024/(double)1024)+"MB");
}
}
/*
* -Xmx8m -Xms8m -XX:+PrintGCDetails
*
* 分析GC日志:
*
* [Times: user=0.00 sys=0.00, real=0.00 secs]
* 1、GC 类型 GC:普通的GC,Full GC :重GC
* 2、1536K 执行 GC之前的大小
* 3、504K 执行 GC之后的大小
* 4、(2048K) young 的total大小
* 5、0.0012643 secs 清理的时间
* 6、user 总计GC所占用CPU的时间 sys OS调用等待的时间 real 应用暂停的时间
*
* GC :串行执行 STW(Stop The World) 并行执行 G1
*/
public class Demo02 {
public static void main(String[] args) {
System.gc(); // 手动唤醒GC(),等待cpu的调用
String str = "ilovecoding";
while (true){
str += str
+ new Random().nextInt(999999999)
+ new Random().nextInt(999999999);
}
// 出现问题:java.lang.OutOfMemoryError: Java heap space
}
}
堆小结
- JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation)
- 非堆内存就一个永久代(Permanent Generation)
- 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
- 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
- 年轻代GC次数比较频繁,老年代GC次数比较少。
- 关于垃圾回收:
分代收集算法
不同的区域使用不同的算法
Minor GC : 清理年轻代
Major GC: 清理老年代;老年代存储长期存活的对象,占满时会触发Major GC=Full GC
Full GC : 清理整个堆空间。
所有GC都会停止应用所有线程 。
- 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
- 在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
- 元空间有注意有两个参数:
- MetaspaceSize :初始化元空间大小,控制发生GC阈值
- MaxMetaspaceSize :限制元空间大小上限,防止异常占用过多物理内存
Dump内存快照
使用一些工具来查看:
1、Jconsole
2、idea debug
3、Eclipse(MAT插件)
4、IDEA(Jprofiler插件)
GC
GC四大算法
引用计数法 JVM 一般不采用这种方式
- 特点: 每个对象都有一个引用计数器,每当对象被引用,则计数器+1,当引用失效,计数器-1.当计数器为0时,GC时就会被清理掉.
- 缺点: 计数器维护麻烦; 循环引用无法处理.
复制算法
1、一般普通GC 之后,差不多Eden几乎都是空的了!
2、每次存活的对象,都会被从 from 区和 Eden区等复制到 to区,from 和 to 会发生一次交换;记住一个点就好,谁空谁是to,每当幸存一次,就会导致这个对象的年龄+1;如果这个年龄值大于15(默认值),就会进入养老区!
-
优点:没有标记和清除的过程!效率高!没有内存碎片!
-
缺点:需要浪费双倍的空间
Eden 区,对象存活率极低! 统计:99% 对象都会在使用一次之后,引用失效!
推荐使用 复制算法
标记清除
- 优点:不需要额外的空间!
- 缺点:两次扫描,耗时较为严重,会产生内存碎片,不连续; 碎片过多时,当程序运行时需要分配比较大对象时,因为找不到连续的内存空间,导致会再次触发GC。
标记清除压缩
- 特点:不需要额外的空间!解决内存碎片问题。
- 缺点:耗时严重。假设这个空间中很少,不经常发生GC,那么可以考虑使用这个算法!
GC ROOT
1. 方法区中的常量
2. 类中静态属性的引用对象
3. 虚拟机栈中的引用对象
4. 本地方法栈中Native方法的引用对象
public class GCRoots{
private byte[] array = new byte[100*1024*1024]; // 开辟内空间!
private static GCRoots2 t2; // GC root;
private static final GCRoots3 t3 = new GCRoots3(); // GC root;
// 引用远远不止于此,强引用,软引用,弱引用,虚引用! 四个类的使用!
public static void m1(){
GCRoots g1 = new GCRoots(); //GCroot
System.gc();
}
public static void main(String[] args){
m1();
}
}
垃圾收集器
串行 (STW: stop the world) :单线程工作
并行(多线程工作 但是也是 STW)
并发(回收垃圾的同时,可以正常执行线程,并行处理,但是如果是单核CPU,只能交替执行!)
G1 (※) (将堆内存分割成不同的区域,然后并发的对其进行垃圾回收)
G1(Garbage-First)收集器 ,面向服务器端的应用的收集器;
G1优点:
1、没有内存碎片!
2、可以精准空垃圾回收时间
OOM(Out Of Memory)
OutOfMemoryError:PermGen;永久代大小空间是有限的,满了报OOM
OutOfMemoryError :java heap space ;
public static void main(String[] args) {
HashMap<String,String> map = new HashMap<>();
for (int i = 0; ; i++) {
map.put(String.valueOf(i), String.valueOf(i));
System.out.println(i);
}
}
java.lang.StackOverflowError
public class OOMDemo {
public static void main(String[] args) {
a();
}
private static void a() {
a();
}
}
java.lang.OutOfMemoryError: GC overhead limit exceeded
GC 回收时间过长也会导致 OOM;
可能CPU占用率一直是100%,GC 但是没有什么效果!
// -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
public class OomDemo {
public static void main(String[] args) throws Throwable {
int i = 0;
List<String> list = new ArrayList<String>();
try {
while (true){
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) {
System.out.println("i=>"+i);
e.printStackTrace();
throw e;
}
}
}
java.lang.OutOfMemoryError: Direct buffer memory 基础缓冲区的错误!
java.lang.OutOfMemoryError: unable to create native Thread
高并发 , unable to create native Thread这个错误更多的时候和平台有关!
1、应用创建的线程太多!
2、服务器不允许你创建这么多线程!
public class TDemo {
public static void main(String[] args) {
for (int i = 1; ; i++) {
System.out.println("i=>"+i);
new Thread(()->{
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
},""+i).start();
}
}
}
//Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
//1、服务器线程不够了,超过了限制,也会爆出OOM异常!
java.lang.OutOfMemoryError: Metaspace 元空间报错
// -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
public class OomDemo {
static class OOMTest{}
public static void main(String[] args) throws Throwable {
int i = 0; // 模拟计数器
try {
while (true){
i++;
// 不断的加载对象! Spring的 cglib;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return method.invoke(o,args);
}
});
enhancer.create();
}
} catch (Throwable e) {
System.out.println("i=>"+i);
e.printStackTrace();
}
}
}
常用jvm 参数
JVM参数有三种
1. 标配参数
2. X参数
3.XX参数
标配参数:在各种版本之间都很稳定,很少有变化
-version
-help
-showversion
X参数(了解)
-Xint # 解释执行
-Xcomp # 第一次使用就编译成本地的代码
-Xmixed # 混合模式(Java默认)
重点(XX参数之布尔型)
参数 | 备注 |
---|---|
-XX:+TraceClassLoading | 用于追踪类的加载信息并打印出来 |
-XX:+PrintGCDetails | 打印GC详细信息 |
-XX:+PrintFlagsInitial | 查看 java 环境初始默认值 |
-XX:+PrintFlagsFinal | 查看修改过的值 |
-Xmx(XX:MaxHeapSize) | 最大分配内存(默認占比1/6) |
-Xms(-XX:InitialHeapSize) | 初始分配的内存大小; 1/64 |
-Xss | 线程栈大小设置,默认 512k~1024k |
-Xmn | 设置年轻代的大小,一般不用动! |
-XX:MetasapceSize10m | 初始化元空间大小,控制发生GC阈值(设置为10m) |
-XX:MaxMetaspaceSize | 限制元空间大小上限,防止异常占用过多物理内存 |
-XX:+PrintCommandLineFlags -version | 打印出用户手动选项的 XX 选项 |
-XX:SurvivorRatio | 设置新生代中的 s0/s1 空间的比例 |
-XX:NewRatio | 设置年轻代与老年代的占比 |
-XX:MaxTenuringThreshold=15 | 控制新生代需要经历多少次GC晋升到老年代中的最大阈值(默认是15) |
GC相关 | : |
---|---|
-XX:+UseSerialGC | 串行收集器 |
-XX:+UseParallelGC | 并行收集器 |
-XX:+UseParallelGCThreads=8 | 并行收集器线程数,同时有多少个线程进行垃圾回收,一般与CPU数量相等 |
-XX:+UseParallelOldGC | 指定老年代为并行收集 |
-XX:+UseConcMarkSweepGC | CMS收集器(并发收集器) |
-XX:+UseCMSCompactAtFullCollection | 开启内存空间压缩和整理,防止过多内存碎片 |
-XX:CMSFullGCsBeforeCompaction=0 | 表示多少次Full GC后开始压缩和整理,0表示每次Full GC后立即执行压缩和整理 |
-XX:CMSInitiatingOccupancyFraction=80% | 表示老年代内存空间使用80%时开始执行CMS收集,防止过多的Full GC |
-XX:+UseG1GC | G1收集器 |
-XX:MaxTenuringThreshold=0 | 在年轻代经过几次GC后还存活,就进入老年代,0表示直接进入老年代 |
-XX:MaxGCPauseMillis=100 | 最大的GC停顿时间单位:毫秒,JVM尽可能的保证停顿小于这个时间! |
jinfo | : |
---|---|
jinfo -flag PrintGCDetails +pid | 查看打印GC详细信息是否开启 |
jinfo -flags | 查看所有信息 |
jinfo -flag MetaspaceSize +pid | 查看元空间大小 |
jinfo -flag MaxTenuringTheshold | 新生代存货次数 |