1、JVM初识
jvm本身和其他程序一样,本身就是一段程序,运行在操作系统上。只不过是自己写的代码运行在虚拟机上而已。
2、JVM的体系结构
3、类加载机制
由.java 编译为 java.class文件,经过ClassLoader类加载器加载,完成初始化,生成Class对象,然后实例化对象。
public class Test {
public static void main(String[] args) {
//类是模板,对象是具体的
Test test = new Test();
Test test2 = new Test();
Test test3 = new Test();
System.out.println(test.hashCode());
System.out.println(test2.hashCode());
System.out.println(test3.hashCode());
//当前类加载器 AppClassloader
ClassLoader classLoader = test.getClass().getClassLoader();
//扩展类加载器 ExtClassloader
System.out.println(classLoader.getParent());
//根类加载器 BootStractClassloader 1、要么不存在,2、要么java获取不到,调用c的类库
System.out.println(classLoader.getParent().getParent());
}
Connected to the target VM, address: '127.0.0.1:5779', transport: 'socket'
2094777811
984213526
400136488
sun.misc.Launcher$ExtClassLoader@3834d63f
null
4、双亲委派机制
双亲委派机制是一种安全机制,防止开发人员恶意的操作底层类库而造成不可预估的损失。
刚才说到类加载器一共有三个:
AppClassLoader 应用程序类加载器
ExcClassLoader 扩展类加载器
BootStracpClassLoader 根类加载器
每当我们new一个类,由.java编译为.class,接下来将.class文件交给应用程序类加载器去加载,应用程序类加载器会将向上询问自己的父类加载器 (委托父类),父类加载器然后去询问根加载器(委托父类),如果有就直接加载,优先使用根类加载器的类,优先级从大到小 :根类加载器 》扩展类 》应用程序类。如果没有直接抛出异常 Class NotFoundException
沙箱安全机制
5 、Native
凡是带了Native关键字的,说明java的作用范围达不到了,此时他去调用底层C的类库,会进入本地方法栈。
调用本地方法 本地接口JNI
JNI作用:扩展java的使用,融合不同的编程语言为java所用,毕竟早期是C 和C++的天下。
所用要想立足,必须要有调用C 、C++的程序。
java在内存区域中专门开辟了一块标记区域:Native Method Stack,登记native方法,
在执行引擎执行的时候加载Native libraies.(本地库)
在最终执行的时候,加载本地方法库中的方法通过JNI。如:调用打印机、管理系统。
private native void start0();
6、方法区 Method Area
方法区被所有线程共享,所有 字段和方法字节码,以及一些特殊方法,如构造函数、接口代码也在此定义,
简单的说,所有定义的方法的信息都保存在该区域,***此区域属于共享区间***✔。
静态变量、常量、类信息(构造方法,接口定义)、运行时的常量池存在方法区中,但是实例存在堆中,
和方法区无关。
7 PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码
(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,
是一个非常小的内存空间,几乎可以忽略不计。
8、 栈
栈 :先进后出、后进先出。想象成子弹的弹夹。最开始压入弹夹的子弹,最后被打出去
这也是为什么main方法先执行,最后结束的原因。
pc寄存器
队列:先进先出,后进后出。想象成水管,往往都是顺序消费。
栈:栈内存,主管程序的运行,生命周期和线程同步,线程结束,栈内存也就释放,
对于栈来说,不存在垃圾回收问题。因为一旦线程结束,栈就结束了。
栈:8大基本类型+对象的引用+实例的方法
栈运行原理:栈帧
一旦栈中满了 就会报StackOverflowError,此时程序已经停掉,无法运行。典型的像线程之间互相调用,
且都不释放锁资源,或者递归调用。
9、堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了文件后,一般会把类、方法、常量,变量-, 保存在应用类型的真实对象;
堆内存主要还要细分三个区域:
新生区(伊甸园区)Young/New
养老区 old
永久区
新生区
类:诞生和成长的地方,甚至死亡
伊甸园,所有对象都是在伊甸园区new 出来的。
幸存者区(0,1)位置上互换。
GC垃圾回收,主要是在伊甸园区和养老区
假如内存满了,OOM,堆内存不够了,java.lang.OoutOfMemoryError:java heap space
在JDK8以后,永久存储区改了名字(元空间)
永久区
这个区域常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或者类信息,这个区域不存在垃圾回收,关闭JVm虚拟机就会释放这个区域的内存。
JDK1.6之前: 永久代,常量池在方法区
JDK1.7: 永久代,当时慢慢的退化了,去永久带,常量池在堆中。
JDK1.8: 无永久代,常量池在元空间
。
public class HeapTest {
public static void main(String[] args) {
//虚拟机试图使用的最大内存
//字节 1024*1024
long max = Runtime.getRuntime().maxMemory();
//jvm的初始化总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max=" + max + "字节\t" + (max / (double) 1024 / 1024) + "MB");
System.out.println("total=" + max + "字节\t" + (total / (double) 1024 / 1024) + "MB");
}
}
控制台输出
max=1771044864字节 1689.0MB
total=1771044864字节 115.0MB
假如报出来OOM:
1、调节虚拟机堆的内存以及总内存
2、分析内存,看一下哪个地方出现了问题
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
打印信息到控制台,如下所示:
Heap
PSYoungGen total 305664K, used 31457K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 12% used [0x00000000eab00000,0x00000000ec9b86e8,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3159K, capacity 4556K, committed 4864K, reserved 1056768K
class space used 329K, capacity 392K, committed 512K, reserved 1048576K
下面模拟堆内存溢出
public class TestHeap {
public static void main(String[] args) {
List<User> list = new ArrayList<>();
int count=0;
try {
while (true) {
list.add(new User());
count=count+1;
}
} catch (Error error) {
error.printStackTrace();
System.out.println("count:"+count);
}
}
}
在编辑器中添加必要的参数命令 ,大致意思说,只要堆内存溢出,就会dump文件,建议初始化的堆内存不要设置过大,否则会很卡。
-Xms2m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
执行完毕,查看当前项目下生成的Jporfiler文件,直接双击打开。
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to java_pid21192.hprof ...
java.lang.OutOfMemoryError: GC overhead limit exceeded
Heap dump file created [12397181 bytes in 0.033 secs]
at com.riemann.springbootdemo.util.common.TestHeap.main(TestHeap.java:14)
Disconnected from the target VM, address: '127.0.0.1:9047', transport: 'socket'
count:317484
可以看到哪个类的实例数,以及最大对象的占比,此时显示arraylist占比88%,是有问题的
继续看线程转储,此时只有一个main方法,点击查看细节,发现main 方法中第14行出现了问题。
10、GC
jvm在进行GC时,并不是对这三个区域统一回收,大部分时候,回收都是新生代
新生代
幸存区
老年区
GC两种类:轻GC(普通的GC),重GC(全局GC)
GC算法:标记清除法、标记压缩法、复制算法、应用计数器
引用计数法:
对于每个使用的对象应用,进行计数,如果检测到对象应用计数为0时,就清除这个对象引用。
标记清除法:
优点:不需要额外的空间。
缺点:两次扫描,严重浪费,会产生内存碎片。
标记压缩
标记压缩是对标记清除的优化。
总结
内存效率:复制算法》标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法》标记清除算法
内存利用率:标记压缩算法=标记清除算法》复制算法
没有最完美的算法,只有最合适的算法》GC分代收集算法
年轻代:
·存活率低
·复制算法
老年代:
·区域大:存活率
·标记清除(内存碎片不是太多)+标记压缩混合实现。
序消费。