前言
谈谈你对JVM理解?Java8虚拟机和之前的变化更新
什么是OOM,什么是栈溢出?怎么分析
JVM的常用调优参数有哪些?
内存快照如何抓取,怎么分析Dump文件?知道吗?
谈谈JVM中,类加载器你的认识?rt-jar等
一、JVM探究
JVM的位置- JVM的位置
JVM的体系结构
注:99%的调优都存在于Heap中,Stack存在出栈弹栈 first-in last-out (FILO),故此Stcak中不会出现GC
· Stack内存结构
二、类加载器
1.类加载器的作用
作用:加载Class文件
· 顺便回顾一波面向对象
主要有以下四种类加载器
(1)启动类加载器(Bootstrap ClassLoader):这个类加载器负责放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库,用来加载java核心类库。无法被java程序直接引用。
(2)扩展类加载器(extensions class loader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。Java虚拟机的实现会提供一个扩展库目录。
(3)应用程序类加载器(system class loader):这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。一般来说,Java应用的类都是由它来完成加载的。
(4)自定义加载器:用户自己定义的类加载器,通过继承 java.lang.ClassLoader类的方式实现。
来个简单示例代码,如下:
第一次输出,获取类加载器可得知当前使用的是应用程序类加载器。
第二次输出,获取父类的类加载器时得知为扩展类加载器。
第三次输出,获取父类的父类的类加载器,此时输出结果null。此时有两种情况,1表面此时父类的父类没有结果。2其实是有结果,但java代码获取不到父类的父类的类加载器。
2.双亲委派机制
例如,我们编写了一个java.long.String类。如下图:
大家不妨先猜一猜运行结果~
此时运行结果:
报错信息非常离谱,明明我的方法中存在main方法,为什么程序会说它找不到呢?
哈哈此时就是双亲委派机制在搞事情了
双亲委派机制:安全。
注:它在运行一个程序之前,会层层向上找,最终执行启动类加载器(rt.jar)里的string方法
应用程序类加载器(AppClassLoader)–>扩展类加载器(ExtClassLoader)–>启动类加载器(Bootstrap ClassLoader) 其实最终运行的是Bootstrap ClassLoader。
------------------------------------------------------------------------------分隔符------------------------------------------------------------------------
得出结论:
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3.启动类加载器会检查是否能加载当前类,能加载就结束(使用当前的类加载器),否则,抛出异常,通知子加载器进行加载
4.重复步骤3
5.如果所有类加载器都找不到呢? 那么会抛出java.lang.ClassNotFoundException
看源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 存在父加载器,递归的交由父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 直到最上面的Bootstrap类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果仍然没有找到,那么按顺序调用findClass
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
【什么?还不理解双亲委派机制,话不多说上图看】
三、沙箱安全机制- 沙箱安全机制
Java安全模式额核心就是Java沙箱【sandbox】,如下图:
四、Native
有没有小伙伴好奇过线程是如何启用的? 看源码,如下:
这时,我们发现当代码走到 private native void start0();时,就进不去了,这时为什么呢?
public static void main(String[] args) {
new Thread(() -> {
}, "Thread one").start();
// private native void start0();
//native:凡是带了native关键字的,说明Java的作用范围达不到了,会去调用底层语言的库【这里是C++】
//凡是有native的,则会进入本地方法栈
//调用本地方法本地接口 JNI 【Java_Native_Interface】,本地方法接口会去本地方法库中查找(找的其实就是ext)
//JNI作用:扩展Java的作用,融合其他语言为Java所用 最初是想融合:C、C++
//Java诞生的时候C、C++横行,想立足就必须要有调用C、C++的程序
//于是Java就专本开辟了一个标记native的标记区域:Native_Method_Stack 也就是本地方法栈,用于登记native方法
//在最终执行的时候,会通过JNI加载本地方法库中的方法
//一般常用于Java驱动硬件,管理系统 企业级应用不常用
//调用其他接口: http,rpc,Socket,WebService
}
如下图:
总结:凡是带了Native关键字的方法就会进入本地方法栈,其他的就是java栈
方法区【此区域属于共享区间】
根据代码看下图:
public class Car {
private int price;
private String name = "别克";
public static void main(String[] args) {
Car car1 = new Car();
// car1.name = "洗脚城ct6";
System.out.println(car1.name);
}
}
五、栈(Stack)
栈:先进后出,后进先出【想象成一个桶】
队列:先进先出【FIFO(Firist Input First Output)想象成管道】
模拟栈溢出,StackOverflowError
public class TestStack {
public static void main(String[] args) {
//main()先加载到栈中
//main调a方法,a也加载到栈
new TestStack().a();
}
public void a(){
b();//加载b到栈
}
public void b(){
a();//加载a到栈
}
}
栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就释放了。
对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就Over!
栈中存在的数据:8大基本类型+对象的引用地址+实例的方法。
六、堆(Heap)
Heap,一个JVM只有一个堆内存,并且堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把类、方法、常量、变量保存我们所引用类型的真实对象。
堆中还分三个区域:
新生区(Young):类,新生或成长,甚至死亡的地方。新生代又被划分为三个区域Eden、From Survivor, To Survivor。所有的对象都在Eden区new出来的!
养老区 (old)
永久区(元空间)
举个栗子:Eden满了之后会触发 Minor GC,因为大部分对象在短时间内都是可收回掉的,因此Minor GC后只有极少数的对象能存活下来,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳,而被移动到S1区(采用的是复制算法)。
当触发下一次Minor GC 时,会将Eden区和S1区的存活对象移动到S2区,同时清空Eden区和S1区。当再次触发Minor GC 时,这时候处理的区域就变成了Eden区和S2区(即S1和S2进行角色交换)。每经过一次Minor GC ,存活对象的年龄就会加1。经过多次Minor GC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。当晋升到老年代的对象大于了老年代的剩余空间时,就会触发FullGC。
堆溢出,如下图:
经过研究表明:99%的对象都是临时对象,能进入养老区的并不多。
永久区:此区域常驻内存,用来存放JDK自带的class对象。interface元数据,存储的是java运行时的一些环境。
这个区域不存在GC,关闭VM虚拟机就会释放这个区域的内存。
JDK1.6::永久代,常量池在方法区中
JDK1.7::永久代,相比于JDK1.6,慢慢退化了,也叫“去永久代”,常量池在堆中
JDK1.8: 元空间,无永久代,常量池在元空间 (元空间逻辑上存在,物理上不存在)
新生区、永久区、堆内存调优
long max = Runtime.getRuntime().maxMemory();//获取运行时想用的最大内存
long total = Runtime.getRuntime().totalMemory();//获取使用的总内存
int availableProcessors = Runtime.getRuntime().availableProcessors();//可用处理器的个数
System.out.println(max + "字节==" + (double) (max / 1024 / 1024) + "M");
System.out.println(total + "字节==" + (double) (total / 1024 / 1024) + "M");
System.out.println(availableProcessors);
// 默认情况下:分配的总内存是电脑内存的1/4, 初始化的内存是电脑的1/64
我们可以手动配置堆内存大小 -Xms1024m -Xmx1024m -XX:+PrintGCDetails
-Xms:设置初始化内存分配大小 默认1/64。表示初始化JAVA堆的大小及该进程刚创建出来的时候,他的专属JAVA堆的大小,一旦对象容量超过了JAVA堆的初始容量,JAVA堆将会自动扩容到-Xmx大小。
-Xmx:配置当前程序能够使用的最大内存大小 默认1/4,不能超过本机内存。在很多情况下,通常将-Xms和-Xmx设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。
-Xss:规定了每个线程虚拟机栈及堆栈的大小,一般情况下,256k是足够的,此配置将会影响此进程中并发线程数的大小。
堆内存溢出,看下图GC走势
由此我们得出,在新生代,老年代,都已经满了,并且Full GC此时已经清理不动了,产生了OOM。
提问:OOM怎么解决?思考一下
可以这么做:
1.尝试扩大堆内存看结果
2.分析内存,用专业工具看一下那个地方出现了问题
可使用内存快照工具:网上一堆,自行百度
作用:
分析dump内存文件,快速定位内存泄漏问题
获取堆中的数据
定位大的对象
-Xms20m -Xmx30m -XX:+HeapDumpOnOutOfMemoryError
七、GC:垃圾回收
JVM在进行GC时,并不是对这三个区域进行统一回收,大部分时候都是在新生区 ;
新生区
幸存区:【from和to】
老年区
GC两种类:Minor GC(普通垃圾回收)、Full GC(全局垃圾回收)
JVM的内存模型和分区【详细到每个区放什么】
堆里面的分区有哪些?新生区【Eden,from,to】、老年区,说说他们的特点
GC算法有哪些?标记清除法、标记整理/标记压缩、复制算法、引用计数器,怎么用?
Minor GC和Full GC分别在什么时候发生?
1.引用计数法
每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次,则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。
目前虚拟机基本都是采用可达性算法,从GC Roots 作为起点开始搜索,那么整个连通图中的对象边都是活对象,对于GC Roots 无法到达的对象变成了垃圾回收对象,随时可被GC回收。
2.GC复制算法
可以简单的说GC复制算法就是作用于新生区的 from和to区
好处:没有内存碎片
坏处:
1、也就是浪费了一个幸存者区也就是to区的空间,复制算法上面也说了就是to区永远都是空的
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视,所以从以上描述不难看出。复制算法要想使用,最起码对象的存活率要非常低才行,而且 最重要的是,我们必须要克服50%的内存浪费。
最佳使用场景:新生区,毕竟对象存活率较低
3.标记清除(Mark-Sweep)
标记清除算法:
好处:不需要额外的空间
坏处:这个算法需要暂停整个应用,会产生内存碎片。两次扫描,严重浪费时间。消耗时间多,标记一次、清除一次,有内存碎片。
4.标记清除压缩
标记整理说明:老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
标记压缩算法:清除了内存碎片,但是又加长了时间,应该说是用时间换空间
小总结
内存效率:复制算法>标记清除法>标记压缩法
内存整齐度:复制算法=标记压缩法>标记清除法
内存利用率:标记压缩法=标记清除法>复制算法
所以说算法没有最优的,只有最合适的_GC:又叫分代收集算法
年轻代:存活率低,复制算法很合适
老年代:区域大,标记清除(内存碎片不是很多)+标记压缩混合使用
感谢阅读,希望此篇文章对您有所帮助~
加油,打工人!!!