JVM笔记
1.JVM的位置
2.JVM体系结构
3.类加载器
作用:加载class文件
class Car{
public static void main(String args[]){
//类是模板,对象才是具体的
//new是car的实例
Car car1 = new Car();
Car car2 = new Car();
//car1与car2对象是在堆中的两个完全不同的实例对象,hashCode也不同
//通过car实例得到反射对象
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
//aClass1与aClass2是相同的类模板,hashCode是相同的
//通过class获得类加载器
ClassLoader classLoader = aClass1.getClassLoader();
sout(classLoader);//AppClassLoader应用程序加载器
sout(classLoader.getParent());//Exc扩展类加载器
sout(classLoader.getParent().getParent());//null//java调用不到RoorClassLoader,因为RoorClassLoader是用C写的
}
}
分类:
1.虚拟机自带的加载器
2.启动类(根)加载器:RootClassLoader
3.扩展类加载器:ExcclassLoader
4.应用程序加载器:AppClassLoader
AppClassLoader->ExcclassLoader->RootClassLoader
4.双亲委派机制
1.类加载器收到类加载的请求,将这个请求向上委托给父类加载器去完成,直到启动类加载器
2.启动类加载器检查是够能够加载当前这个类,能加载就结束,否则抛出异常,通知子加载器进行加载
3.重复以上步骤
假设自定义了一个String类,重写toString方法且有main方法,当运行时会报错,提示找不到main方法,这是因为加载到了RootClassLoader中的String方法
5.沙箱安全机制
6.Native
带有native关键字的方法,表明这是一个本地方法,java的作用范围达不到,需要调用c语言的库
进入本地方法栈,调用本地方法接口 JNI(java native interface)
由来:java刚诞生的时候,C,C++横行,要想立足,必须要有调用C,C++的程序,
java程序驱动打印机,管理系统等会使用到native方法
7.三种JVM
- sun公司 HotSpot
- BEA公司 JRockit
- IBM公司 J9VM
我们使用的大多都是HotSpot JVM
8.堆栈(虚拟机栈)
1.栈属于线程私有,不能实现线程间的共享
2.JVM为每一个线程创建一个栈,用于存放该线程执行方法的信息 (实际参数,局部变量等)
3.栈描述的是方法执行的内存模型,每个方法被调用都会创建一个栈帧(局部变量表,操作数栈,方法出口等)
4.栈的存储特性是"先进后出,后进先出"
5.栈是有系统自动分配,速度快,栈是一个连续的内存空间
9.方法区
方法区(又叫静态区):
1.JVM只有一个方法区,被所有线程共享
2.方法区实际也是堆,只是用于存储类,常量相关的信息
3.用来存放程序中 static静态资源,final常量,class类加载器,常量池
4.1.8之前永久代是方法区的具体实现,1.8之后是元空间
10.程序计数器
占用内存较小,是当前线程锁执行的字节码(JVM 指令)行号指示器,JVM 通过改变计数器的值来选取下一条要执行的指令。
多线程之间的程序计数器相互独立,互不影响,为了保证每个线程恢复之后都能回到之前中断的位置,进而继续执行。
11.堆
1.JVM只有一个堆,被所有线程共享
2.堆用于存储创建好的 对象和数组(数组也是对象)
3.堆是一个不连续的内存空间,分配灵活,速度慢
Java 堆又分为年轻代和老年代,永久代。
年轻代
又分为 Eden 区和 Survivor 区
Eden:伊甸园
Survivor:幸存者(Survivor From、Survivor To)
Eden:对象刚创建的时候,存放在 Eden 区
Surivor:GC 回收的时候,将 Eden 不需要回收(存活)的对象存入 Survivor From 区,在下一次回收的时候,将 From 区中不需要回收的对象存入到 To 区,然后清理 From 区,在下下一次回收的时候,将 To 区中不需要回收的对象存入 From 区,再清理 To 区,以此循环…
每次回收之后,存活下来的对象年龄都会 +1,年龄增加到一定程度,移动到老年代中。
老年代
存放生命周期较长的对象和一些大对象,Full GC触发垃圾回收
永久代
JDK 1.7 之前将类的信息存放在永久代中,JDK 1.8 之后去掉了永久代,改为元空间。
1.7 之前字符串常量池存放于永久代中,1.8 去掉了永久代,1.8 之后的字符串常量池放在堆中,为什么要这样处理?
因为永久代空间有限,创建字符串对象,需要调用 inter 方法。
元空间使用的是本地内存,永久代使用的是 JVM 内存
本地内存有多大,元空间就有多大,不再受限于 JVM 内存的大小,相当于开辟出更多的区域可以使用,效率更高。
堆内存调优
堆空间默认分配最大内存为电脑总内存的1/4,初始化内存为1/64
Runtime.getRuntime().maxMemory();//最大内存
Runtime.getRuntime().totalMemory();//初始内存
OOM(OutOfMemoryError):堆空间溢出(常见于死循环造成的)
1.尝试扩大堆内存看结果
在配置中(与Tomcat同位置),Modify options,选中add VM options,输入
-xms1024m -Xmx1024m -XX:+PrintGCDetails
初始化1024m 最大内存1024m 打印GC详细信息
注意!!!
PSYoungGen年轻代大小305664k
ParOldGen老年代大小699392k
(年轻代+老年代)/1024 = (1005056k)/1024 = 981.5MB
年轻代+老年代就已经占满了JVM内存空间,可知元空间大小为0,但是显示Metaspace使用了3464k
元空间物理上不存在,是逻辑上存在
2.扩大内存还报错,分析内存,看那个地方出现了问题(jprofiler)
内存快照分析工具:MAT,jprofiler
早期的eclipse中使用MAT分析JVM内存
现在的idea可以使用jprofiler分析内存
作用:
1.安装插件
2.网上下载jprofiler软件
安装路径要求没有空格,没有中文
测试(先写一个会堆溢出的程序)
import java.util.ArrayList;
public class Demo{
byte[] array = new byte[1*1024*1024];//1M的数据
public static void main(String [] args){
ArrayList<Demo> list = new ArrayList<>();
try{//向list数组中一直加数据,会导致堆溢出
while(true){
list.add(new Demo());
count = count +1;
}
}catch(Exception e){
System.out.println("count:"+count);
e.printstacktrace();
}
}
}
在配置中(与Tomcat同位置),Modify options,选中add VM options,输入
-xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
初始内存1024m 最大内存1024m 当堆满时生成一个dump文件
HeapDumpOnOutOfMemoryError:如果向dump其他异常,只需要把后面的OutOfMemoryError换成其他的异常信息就可以了
运行程序后会在在src目录下生成一个dump文件,使用jprofiler打开dump文件即可查看内存使用情况
项目在实际运行过程中是查看不了控制台信息的,使用jprofiler可以清晰的查看程序运行过程中产生的问题,定位错误信息
JMM
GC的算法
学习 GC 需要从以下 4 个方面入手:
1、如何判断某个对象是垃圾,需要被回收?
2、垃圾回收算法。
3、不同内存区域的回收方式。
4、垃圾收集器的分类。
如何判断对象是垃圾
Java 对象被判定为垃圾的标准:没有被其他对象引用,判断方法有两种:
1、引用计数算法
通过判断对象的引用数量来决定是否要被回收,每一个对象实例都有一个计数器,被引用则+1,完成引用则-1。
什么是完成引用?
当该对象的引用超过了生命周期,或者引用指向了其他对象,在某方法中定义一个对象的引用变量,方法结束之后变量被虚拟机栈自动释放,则改对象的引用也就结束了,所以任何一个引用计数为 0 的对象是可以被当作垃圾回收的。
2、可达性分析算法
通过判断对象的引用链是否可达来决定对象是否要被回收,这个算法的基本思想就是通过一系列的称为 GC Root 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Root 没有任何引用链相连的话,则证明此对象是不可达的,即认为它不可用,如下图所示。
什么对象可以作为 GC Root ?
1、虚拟机栈中的引用对象
2、方法区中的常量引用对象
4、本地方法栈中的引用对象
5、活跃线程中的引用对象
垃圾回收算法
1、标记-清除算法(Mark and Sweep)
清除:对堆内存进行遍历,回收不可达对象内存。
缺点:清除后会产生大量不连续的内存碎片,可能导致后续在创建较大对象是无法找到足够的连续内存而触发再一次的垃圾回收,如下图所示。
2、复制算法
将可用内存分为对象面和空闲面,在对象面上创建对象,当对象面没有空间的时候,将还存活的对象复制到空闲面,将对象面所有对象清除。
解决了碎片化问题,顺序分配内存,简单高效,适用于对象存活率较低的场景,因为复制的内容少,所以效率高,如下图所示。
3、标记-整理算法
清除:移动所有存活的对象,按内存地址依次排列,然后将末端地址以后的内存全部回收。
在标记-清除的基础上完成了移动,解决了内存碎片的问题,但是成本更高,适用于对象存活率较高的场景,如下图所示。
4、分代收集算法是一种组合的回收机制,也是 GC 的主流回收算法,将不同生命周期的对象分配到堆中的不同区域,采用不同的垃圾回收算法,提高 JVM 垃圾回收效率。
不同内存区域的回收方式
年轻代
使用 Minor GC 进行回收,采用复制算法,年轻代分为 Eden 区和 Survivor 区。
Eden区:对象刚被创建的时候,存放在 Eden 区,如果 Eden 区放不下,则放在 Survivor 区,甚至老年代中。
Survivor 区:Minor 回收时使用,将 Eden 中存活的对象存入 Survior 中(From),再一次 Minor 时,将 Survior From 中的对象存入 Survior To 中,清除 Survior From ,下一次 Minor 时重复次步骤,Survior From 变成 Survior To,Survior To 变成 Survior From,依次循环,同时每次 Minor,对象的年龄都 +1,年龄增加到一定程度的对象,移动到老年代中。
老年代
存放生命周期较长的对象,使用标记-清理算法或者标记-整理算法进行回收。
年轻代常见的垃圾收集器
1、Serial 收集器(复制算法):单线程收集,进行垃圾收集时,必须暂停所有工作线程。
2、ParNew 收集器(复制算法):多线程收集,垃圾收集和工作线程可同时执行。
3、Parallel Scavenge 收集器(复制算法):多线程收集,更关注系统的吞吐量。
Serial 收集器和 ParNew 收集器更关注用户线程停顿时间,停顿时间越短,响应速度越快,用户体验越好,适用于直接与用户交互的程序。
Parallel Scavenge 收集器更关注系统的吞吐量,可提升 cpu 的效率,尽快完成运算任务,适合在后台运行,不需要太多交互的程序。
老年代常见的垃圾收集器
1、Serial Old 收集器(标记-整理算法):单线程收集,进行垃圾收集时,必须暂停所有工作线程。
2、ParNew Old 收集器(标记-整理算法):多线程收集,垃圾收集和工作线程可同时执行,吞吐量优先。
3、CMS 收集器(标记-清除算法):垃圾回收线程和用户线程几乎可以同时工作。
4、Garbage First 收集器(复制+标记-整理算法):并发和并行,使用多个 cpu 来缩短 Stop-the-World 的停顿时间,与用户线程并发执行,并且可采用不同的方式去处理新产生的对象。同时有利于空间整合,基于标记-整理算法,可以解决内存碎片的问题。
任何一种 GC 算法中都会发生,当 Stop-the-World 发生时,除了 GC 的线程以外, 所有线程都处于等待状态,直到 GC 任务完成,多数 GC 优化就是通过减少 Stop-the-World 发生的时间来提高程序性能。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。