JVM
2020-03-18 11:31:12 302 举报
AI智能生成
JVM相关
作者其他创作
大纲/内容
垃圾回收机制
基本概念:
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收
什么对象可以看作垃圾?
没有被任何其他对象引用的对象
如何判断对象是否是垃圾
引用计数算法
判断对象的引用数量,每个对象实例都有一个引用计数器,当它被引用时+1,完成引用时-1.
优点:
执行效率高,程序执行受影响较小。
执行效率高,程序执行受影响较小。
缺点:
无法解决循环引用的问题,会导致内存泄漏(很致命,已经被摒弃)
无法解决循环引用的问题,会导致内存泄漏(很致命,已经被摒弃)
可达性算法
以gc root为起点,判断对象的引用链是否可达来决定对象是否可被回收
可以作为gc root的对象
虚拟机栈中引用的对象
方法区中的常量引用的对象
方法区中类静态属性引用的对象
native方法的引用对象
方法区中的常量引用的对象
方法区中类静态属性引用的对象
native方法的引用对象
垃圾回收算法
标记-清楚算法
先按照可达性算法标记所有被引用的对象,然后遍历堆,清除未被标记的算法。它的会产生内存碎片,以及出现大对象找不到连续空间的问题。(标记过程会暂停所有进程,也就是我们说的stop-the-world)
标记-整理算法
它的标记过程和标记-清楚算法相同,不同的是遍历堆时,它不仅会清除掉未被标记的对象,还会将存活的对象在堆中顺序存放。这样避免了内存碎片的问题,也不像复制算法需要两块内存空间,造成内存的浪费
复制算法
这种算法将内存分为相等的两部分,对象面和空闲面。对象在对象面上创建,将还要使用 的对象复制到空闲面,然后清除所有对象面的对象。它解决了上述内存碎片的问题,但是当要复制的对象很多时,效率会大大降低,所以适合对象存活率低的场景,比如之后要说的年轻代
分代收集算法(主流算法)
它将堆分为新生代和老年代,新生代又分为Eden空间和两块Survivor 空间,它们的比例大概是8:1:1。新生代中的对象大多存活率不高,所以我们一般采用复制算法。每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
gc分类
Minor GC
当Eden区满时,会触发Minor GC,对新生代进行垃圾回收。
MajorGC
对老年代进行垃圾回收
full GC
对整个堆进行垃圾回收
触发条件
1.老年代空间不足
2.方法区空间不足
3.调用system.gc(),这个只是建议JVM执行full GC,但不是一定就会立刻执行。
4.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
2.方法区空间不足
3.调用system.gc(),这个只是建议JVM执行full GC,但不是一定就会立刻执行。
4.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
对象如何进入老年代
1.每进行一次Minor GC对象的年龄就会加1,一般达到15就可以进入老年代(数值可以自己用调优参数设定)
2.Survivor区存放不下的对象,因为每次Minor GC的时候会将Eden区和一个from区的存存活对象放入to区,所以当to区装不下的对象时就会进入老年代
3.新生成的大对象
2.Survivor区存放不下的对象,因为每次Minor GC的时候会将Eden区和一个from区的存存活对象放入to区,所以当to区装不下的对象时就会进入老年代
3.新生成的大对象
两个名词
stop-the-world
JVM由于要执行gc而会暂停应用程序的执行,这一过程叫做stop-the-world。它会在任何一种GC算法中出现,而我们对gc的优化也是致力于减少它发生的时间来提高程序性能。
safepoint
当我们对某一个对象进行可达性分析时,它的引用关系此时最好是不能变化的,也就是它必须处于安全点的。也就是说safepoint就是分析过程中对象引用不会发生变化的点(场景:方法调用,循环跳转,异常跳转等)
垃圾回收器
Serial收集器(-XX:+UseSerialGC,复制算法)
它采用单线程收集,必须暂停所有工作线程。
ParNew收集器(-XX:+UseParNewGC,复制算法)
采用多线程收集,其他与Serial相同,在单CPU情况下效率不如Serial,多CPU下才能发挥优势
Parallel收集器(-XX:+UseParallelGC,复制算法)
它关注于系统吞吐量(代码执行时间/代码执行时间+垃圾回收时间),多线程收集,是jVMserver模式下的默认的年轻代收集器
Serial Old收集器(-XX:+UseSerialOldGC,标记整理算法)
与Serial收集器基本一样,除了采用标记整理算法。
Parallel Old 收集器(-XX:+UseParallel Old,标记整理算法)
从图书可以看出只能和Parallel合作,多线程,吞吐量优先。
CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)
它与其他老年代收集器不同的时,它采用标记清除算法,也就是说它存在内存碎片化的问题,果然要分配一个比较大的对象的内存,就只能触发GC。
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
它可以同时完成年轻代和老年代的垃圾回收,它将整个heap内存分为多个大小相等的Region,让年轻代和老年代不再是物理隔离
它使用了多个cpu来缩短stop-the-world的时间,与用户线程并发执行,它建立了可预测的时间停顿模型,让使用者知道指定在一顿m毫秒的时间上,垃圾回收的时候不超过m毫秒
七个垃圾回收器作用范围和关系
JVM调优
常用命令
jps
显示当前所有java进程pid的命令
jps -v 显示虚拟机参数
jps -m 显示传递给main()函数的参数
jps -l 显示主类的全路径
jstat
显示进程中的类装载、内存、垃圾收集、JIT编译等运行数据
jstat -gc 5828 250 5 每250毫秒查询一次进程5828垃圾收集状况,一共查询5次
jstat -class 29256 查询类装载、类卸载、总空间以及所消耗的时间
jstat -gccause 29256 输出导致上一次GC产生的原因
jmap
生成堆转储快照(heapdump)
jmap -heap 29256 查看java 堆(heap)使用情况
jmap -histo 29256 查看堆内存(histogram)中的对象数量及大小
jmap -histo:live 29256 这个命令执行,JVM会先触发gc,然后再统计信息。
jmap -dump:format=b,file=heapDump 29256 会生成一个dump文件,可以使用其他软件查看。或者用jhat命令可以参看 jhat -port 5000 heapDump 在浏览器中访问:http://localhost:5000/ 查看详细信息
jhat
一般与jmap搭配使用,用来分析jmap生成的堆转储文件
jhat heapDump 会启动一个webserver 然后可以在浏览器上查看(可以自己指定端口)
jstack
生成当前时刻的线程快照(用来查找死锁原因很方便)
jstack 3331 查看线程的情况
工具
jconsole
用于对 JVM 中的内存、线程和类等进行监控;
jvisualvm
JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
常见问题定位过程
1.使用jps查看线程ID
2.使用jstat -gc 3331 250 20 查看gc情况,一般比较关注MET区的情况,查看GC的增长情况。
3.使用jstat -gccause:额外输出上次GC原因
4.使用jmap -dump:format=b,file=heapDump 3331生成堆转储文件
5.使用jhat或者可视化工具(Eclipse Memory Analyzer 、IBM HeapAnalyzer)分析堆情况。
6.结合代码解决内存溢出或泄露问题
dump下来文件分析
gc前后如果堆空间大小变化不大,可能是发生了内存泄漏 定位泄露代码点
如果堆空间大小变化很大,可能是年轻代和老年代大小设置不合理
gc前后如果堆空间大小变化不大,可能是发生了内存泄漏 定位泄露代码点
如果堆空间大小变化很大,可能是年轻代和老年代大小设置不合理
JVM调优参数
-Xss:规定每个线程虚拟栈的大小,一般情况下,256k足够,它会影响进程中线程的并发数量
-Xms:堆的初始值,
-Xmx:堆能达到的最大值,一旦对象超过-Xms指定的大小,就将堆扩容至此参数。但是为了防止heap扩容引起的内存抖动,影响程序运行的稳定性,所以一般设置为-xms一样的值。
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
-XX:MaxTenuringThreshlod:对象从年轻代升到老年代经过GC次数的最大阙值
内存泄漏
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
引用类型
强引用:发生 gc 的时候不会被回收。
软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
弱引用:有用但不是必须的对象,在下一次GC时会被回收
虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
内存空间分配
JVM内存:
一般java文件被编译成.class文件之后,会由类加载器加载进内存中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。
一般java文件被编译成.class文件之后,会由类加载器加载进内存中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。
线程私有
程序计数器
是每个线程都具有一个私有的程序计数器,因为JVM执行代码是一行一行的执行,所以需要计数器来记录当前执行的行数
虚拟机栈
每个方法在执行的同时都会创建一个栈帧,用来存放局部变量,对象的引用之类的方法信息
本地方法栈
和虚拟机栈的作用相似,不过是针对native方法,一般可不用关心。
线程公有
堆
它也是线程共享的,一般堆内存空间比它们内存空间要大,它主要负责存放对象实例。所以gc主要负责这块区域的垃圾回收。
方法区
方法区是线程共享的区域,它一般用于储存与类相关的信息,像编译后的代码、静态变量等等,在jdk1.7中它的实现就是放入永久代中,这样的好处就是可以直接使用堆中的GC算法来进行管理,但坏处就是经常会出现内存溢出,即PermGen Space异常。在jdk8中,使用上图的metaspace代替,也就是元空间。元空间使用本地内存,理论上电脑有多少内存,它就有多少内存,避免的内存溢出问题
过程
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
元空间相比永久代的优势
1.字符串常量池在永久代中容易出现性能问题和内存溢出,而元空间使用本地内存,所以不用担心这个问题。
2.永久代会给gc带来不必要的复杂性
3.类和方法信息大小难以确定,给永久代的大小指定带来困难
2.永久代会给gc带来不必要的复杂性
3.类和方法信息大小难以确定,给永久代的大小指定带来困难
深拷贝和浅拷贝的区别
深拷贝
是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存
浅拷贝
只是增加了一个指针指向已存在的内存地址,
堆栈的区别
物理地址
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
内存分别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
空间释放
堆由gc释放内存空间,栈自动释放空间
类加载机制
概念:
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
类加载器
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器。
Bootstrap ClassLoader
它是根类加载器,由c++编写,JVM启动时加载它,然后它加载另外两个类加载器,它还会加载$JAVA_HOME中jre/lib/rt.jar里所有的class。
Extension ClassLoader
扩展类加载器,负责加载$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
App ClassLoader
应用类加载器,一般我们用的类都是由它加载的,当然我们可以自定义类加载器,此时就是以它为父类加载器。
自定义类加载器
可以通过继承 ClassLoader并重写findClass方法来实现。
类加载过程
JVM将类加载过程分为三个阶段,装载,链接,初始化。链接又可以分为三个阶段验证准备解析。
装载:根据查找路径找到响应的.class文件加载进内存,并为之创建Class对象
验证:保证被加载类的正确性
准备:为类的静态变量分配内存,并初始化为默认值。这一步和初始化有区别。比如static int a=1。那么到这部分配默认值是0,初始化的时候才是1.
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
类加载的时机
1.new一个对象的时候,也就是创建类实例的时候
2.访问某个类或接口的静态变量,或者对该静态变量赋值
3.调用类的静态方法
4.反射(Class.forName(“xxx.xxx”))
5.初始化一个类的子类(会首先初始化子类的父类)
6.JVM启动时标明的启动类,即文件名和类名相同的那个类
2.访问某个类或接口的静态变量,或者对该静态变量赋值
3.调用类的静态方法
4.反射(Class.forName(“xxx.xxx”))
5.初始化一个类的子类(会首先初始化子类的父类)
6.JVM启动时标明的启动类,即文件名和类名相同的那个类
双亲委派模型
工作原理
当一个类加载器要加载某一个类的时候,它会先去找它的父类加载器去执行加载,如果父类加载器还有父类,就依次向上请求,直到.Bootstrap ClassLoader,如果父类能够完成加载就返回请求,如果不行,则由子类加载。
好处
1.可以避免重复加载,如果父类加载器已经加载过此类的时候,子类加载器则不用再加载一次。则保证了java中类名的唯一性
2.保证java核心api中定义类型不会被随意替换,因为我们的根类记载器是会加载一些核心API的,如果此时传来一个与核心API类名相同的类,我们不会加载它,避免了核心API类不被修改。
对象的创建
Java中创建对象的方式
1.使用new关键字
2.使用Class的newInstance方法
3.使用Constructor类的newInstance方法
4.使用clone方法
5.使用反序列化
对象的创建过程
1.类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
举个例子,我们的string就算是对象创建,既是 String str= new String("abc"); 首先也会在常量池中定位是否存在值。若不存在,则会在常量池也建立一个abc.
举个例子,我们的string就算是对象创建,既是 String str= new String("abc"); 首先也会在常量池中定位是否存在值。若不存在,则会在常量池也建立一个abc.
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来
分配方式
指针碰撞
如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
空闲列表
如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5.执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来
对象的访问定位
句柄
如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
直接指针
如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销
0 条评论
下一页