持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情
JVM学习
JVM底层原理参考:doocs.gitee.io/jvm/#/
JVM的位置
JVM位于操作系统之上的一层,JRE包含了JVM
查看JVM类型
PS C:\IdeaProject\WorkPlace\JavaBase> java -version
java version "11.0.8" 2020-07-14 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.8+10-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.8+10-LTS, mixed mode)
JVM体系结构
三种JVM
Sun公司 HotSpot
BEA JRockit
IBM J9VM
JVM内存区域分布
JVM内存区域分布参考:segmentfault.com/a/119000004…
Java代码执行流程
.java文件 -> .class文件 -> 类加载器(Class loader) -> 运行时数据区(Runtime,运行时异常在这里产生,不可捕获) -> 执行引擎
运行时数据区
1、方法区 Method Area
2、Java栈 Stack
3、本地方法栈 Native Method Stack
4、堆 Heap
5、程序计数器
Java栈、本地方法栈、程序计数器不存在垃圾回收,垃圾回收主要发生在方法区和堆中,JVM调优主要在方法区中和堆中
类加载机制
参考:类加载机制
类加载作用:加载class文件进内存
例如,一个Car.java文件,编译后成为Car.class文件,然后通过类加载器(Class
loader)加载到内存中,得到Car的类并初始化(Class),类是一个模板,对象是这个模板具体的实例。这个类在内存中是唯一的,但实例化的对象可以有多个,比如car1、car2、car3等。
通过 new
可以实例化类得到一个对象,通过 car1.getClass
可以得到实例化对象car1对应的类。
实例化对象的名字、地址,如car1、car2等是放在栈里面的,具体的数据是放在堆里面的。
类(class)通过getClassLoader()
能够获得类加载器,如Car.getClassLoader()
能够获得Car类的类加载器
类加载器种类
在JDK中类加载器的位置:
1、、启动类加载器(根加载器) BootstrapClassLoader
在 jdk8 中用来加载 jvm 自身需要的类,c++ 实现,用来加载 rt.jar。 在 jdk9 之后的 jdk 中,Bootstrap ClassLoader 主要用来加载 java.base 中的核心系统类。
2、扩展类加载器 ExtClassLoader 在jdk9以后移除,改用PlatformClassLoader
3、应用程序加载器(系统类加载器) AppClassLoader
双亲委派机制
参考:类加载器参考
自底向上检查类是否被加载,自顶向下加载类
任意一个 ClassLoader 在尝试加载一个类的时候,都会先尝试向上调用类加载器的相关方法去加载类,从上向下加载该类,使用最上的且能够加载该类的类加载器加载该类,后面的类加载器不再加载该类。
这样的好处:对于任意使用者自定义的 ClassLoader,都会先去尝试让 jvm 的 Bootstrap ClassLoader 去尝试加载(自定义的 ClassLoader 都继承了它们)。那么就能保证 jvm
的类会被优先加载,限制了使用者对 jvm 系统的影响。
比如自定义一个String类,加载的时候会加载JDK自带的String类,使用的是顶层的启动类加载器加载,自定义的String 类的类加载器是应用程序类加载器,所以自定义的String类不会被加载,保证JDK的String类不被破坏。这就是利用双亲委派机制避免破坏JVM系统,外层恶意同名类得不到加载从而无法使用。
ClassLoader核心方法:loadClass()
// ClassLoader.class
protected Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
// 加锁,保证线程安全
synchronized (getClassLoadingLock(name)){
// 先去找一次 class 是否已经被加载了,如果已经被加载了就不用重复加载了
// 此方法的核心逻辑由 c++ 实现
Class<?> c=findLoadedClass(name);
// 没有被加载的情况
if(c==null){
long t0=System.nanoTime(); // 记录时间
try{
// 此处体现双亲委派机制
// 如果该加载器存在父加载器,就会先去调用父加载器的相关方法
// 如果没有父加载器,就去调用 Bootstrap 加载器
if(parent!=null){
c=parent.loadClass(name,false);
}else{
// 调用 BootstrapClassLoader,此方法的核心逻辑是 c++ 实现的
c=findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
}
// 如果依旧加载不到,那么就说明父加载器仍然加载不到信息
// 那么就需要指定的加载器自己去加载了
if(c==null){
long t1=System.nanoTime();
// 该加载器加载类文件的核心逻辑
// 该方法在 ClassLoader 中是留空的,需要子类按照自身的逻辑去实现
c=findClass(name);
// 此处做一些信息记录,和主逻辑无关
PerfCounter.getParentDelegationTime().addTime(t1-t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if(resolve){
// 解析 class,也是留空的,需要子类去实现
resolveClass(c);
}
return c;
}
}
双亲委派机制: 1、类加载器收到类加载请求 2、将这个请求向上传递,传递给上层类加载器进行加载,直到传递到启动类加载器(BootstrapClassLoader) 3、启动类加载器检查是否能够加载这个类,能加载就使用这个加载器。否则,抛出异常,通知下层类加载器进行加载 4、重复 步骤3 直到应用程序类加载器,如果都找不到则报错:ClassNotFound
沙箱安全机制
native关键字
凡是带了native关键字的方法,就说明Java的所用无法达到,需要调用底层c语言的库
Java作用无法达到时,会进入本地方法栈,调用本地方法接口(JNI:Java Native Interface)
JNI作用:扩展Java的使用,融合不同的语言为Java所用,最初是为了融合c、c++
JVM运行时数据区
方法区 Method Area
元空间、方法区参考:zhuanlan.zhihu.com/p/111809384
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数、接口代码也在此定义。所有定义的方法的信息都保存在该区域,此区域属于共享空间。
方法区在物理上和堆是连在一起的一块连续的内存空间,逻辑上分开。jdk7以前使用永久代来实现方法区,jdk8以后不再有方法区,改为元空间,元空间存在于本地内存,而不再是工作内存。元空间可以看作是方法区的一个改进,里面存储的东西还是相同的。
静态变量(static)、常量(final)、类信息(class)(构造方法、接口定义)、运行时的常量池以及编译器编译后的代码都在方法区中,但是实例变量
存在堆内存中,和方法区无关。
jdk1.6前:永久代,运行时常量池在方法区 jdk1.7:去永久代,运行时常量池在堆中 jdk1.8:无永久代,改为元空间,运行时常量池在元空间中
栈
栈,也称为栈内存,生命周期和线程同步,线程结束栈内存释放。对于栈来说,不存在垃圾回收问题
栈帧: 一个栈帧随着一个方法的调用开始而创建,这个方法调用完成而销毁。栈帧内存放者方法中的局部变量,操作数栈等数据。
Java栈也称作虚拟机栈(Java Vitual Machine Stack),JVM栈只对栈帧进行存储,压栈和出栈操作。Java栈是Java方法执行的内存模型。如图:
栈溢出示例:
public class StackTest {
public static void main(String[] args) {
// 栈溢出测试
new StackTest().a();
}
/**
* a方法调用b方法,b方法中又调用a方法,那么就会不断地产生栈帧添加到栈中,同时没有栈帧出栈,所以会导致栈溢出
*/
public void a() {
b();
}
public void b() {
a();
}
}
总结:
- 每个线程包含一个栈区,栈中保存基础数据类型的变量、自定义对象的引用(不是对象)和方法的相关信息(不包括方法的实现代码,实现代码在方法区中)等,这些数据以栈帧形式存放在栈中,可以查看栈帧的组成。对象都存放在堆区中。
- 每个栈中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。
- 栈分为3个部分:基本类型变量,执行环境上下文,操作指令区(存放操作指令).
- 在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。
- 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
堆 Heap
参考:www.cnblogs.com/h--d/p/1416…
一个JVM只有一个Heap,全局唯一,多个线程共享一个堆,堆内存的大小是可以调整的。Java堆是垃圾收集器管理的内存区域。
此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空间”“To Survivor空间”等名词,这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划分
JVM堆划分
这篇文章里面讲解的比较详细,参考:www.cnblogs.com/h--d/p/1416…
每个区域都有一定的大小
首先在新生代,新生代内存满了之后,经过几次垃圾回收之后存活下来的实例会进入到养老区
GC 垃圾回收 主要发生在新生区(新生代,包括Eden区和Survivor区,Survivor包括0区和1区)的Eden区和养老区(老年代)
新生区
对象产生、成长甚至是死亡的地方。新生区又包含伊甸区和幸存者区(0区和1区)。
伊甸区:
所有对象都是在Eden区 new 出来的。如果存储的所有对象的总大小达到该区的内存大小(内存满了),则会触发一次轻量级GC。GC之后存活下来的对象会进入到幸存者0区,这时Eden区内容清空,容量恢复。
如果伊甸区和幸存者区(分为from 和 to,有时也称0区、1区)都满了之后,则会触发一次轻量级GC,清除伊甸区和幸存者区
中的所有垃圾,活下来的对象则进入养老区。
永久代(JDK8以后被元空间替换)
这个区域是常驻内存的,用来存放JDK自带的Class对象、Interface数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收。关闭虚拟机则会释放这个区域的内存。
这个区域可能出现OOM(Out Of Memory)异常的几种情况:
- 一个启动类,加载了大量的第三方jar包
- tomcat部署了大量的应用
- 大量动态生成的反射类,不断被加载
查看堆内存信息
代码示例:
/**
* 1. 设置堆空间大小的参数
* -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
* -X 是jvm的运行参数
* ms 是memory start
* -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
* 2. 默认堆空间的大小
* 初始内存大小:物理电脑内存大小 / 64
* 最大内存大小:物理电脑内存大小 / 4
* 3. 手动设置:-Xms600m -Xmx600m
* 开发中建议将初始堆内存和最大的堆内存设置成相同的值。
* 4. 查看设置的参数:
* 方式一: jps / jstat -gc 进程id
* 方式二:-XX:+PrintGCDetails
*/
public class HeapSpaceInitial {
public static void main(String[] args) {
//返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虚拟机试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
}
}
内存溢出测试
设置虚拟机参数:
-Xms10m:设置堆初始内存为10m
-Xmx10m:设置堆最大内存为10m
-XX:+PrintGCDetails:打印GC详细信息
内存溢出代码:
public class OOMTest {
public static void main(String[] args) {
System.out.println("==内存溢出测试==");
List<Picture> list = new ArrayList<>();
while (true) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture {
private byte[] pixels;
public Picture(int length) {
this.pixels = new byte[length];
}
}
内存溢出提示信息:
[0.003s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
[0.009s][info ][gc,heap] Heap region size: 1M
[0.010s][info ][gc ] Using G1
[0.010s][info ][gc,heap,coops] Heap address: 0x00000000ff600000, size: 10 MB, Compressed Oops mode: 32-bit
==内存溢出测试==
[0.604s][info ][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
[0.604s][info ][gc,task ] GC(0) Using 2 workers of 8 for evacuation
[0.607s][info ][gc ] GC(0) To-space exhausted
[0.607s][info ][gc,phases ] GC(0) Pre Evacuate Collection Set: 0.0ms
[0.607s][info ][gc,phases ] GC(0) Evacuate Collection Set: 2.5ms
[0.607s][info ][gc,phases ] GC(0) Post Evacuate Collection Set: 0.5ms
[0.607s][info ][gc,phases ] GC(0) Other: 0.3ms
[0.607s][info ][gc,heap ] GC(0) Eden regions: 4->0(1)
[0.607s][info ][gc,heap ] GC(0) Survivor regions: 0->1(1)
[0.607s][info ][gc,heap ] GC(0) Old regions: 0->3
[0.607s][info ][gc,heap ] GC(0) Humongous regions: 4->4
[0.607s][info ][gc,metaspace ] GC(0) Metaspace: 6450K->6450K(1056768K)
[0.607s][info ][gc ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 7M->7M(10M) 3.363ms
····
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.zhang.Java11JVM.JVM04Heap.OOMTest.main(OOMTest.java:22)