赞
踩
目录
3、MinorGC、Mixed GC 、FulIGC的区别是什么?
3、Young Collection + Concurrent Mark (新生代垃圾回收+并发标记)
Java Virtual Machine:Java程序的运行环境 (java二进制字节码的运行环境)
好处:
程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。
对于每个方法,都会将代码转换为字节码,字节码中的行号就是代码的执行顺序,而每个线程根据自己的程序计数器来确定执行的行号
如上图,对于线程1,从第1行开始执行,执行到第10行之后切换到了线程2。线程2从第1行开始执行,执行到了第9行,然后又切换到了线程1,但是此刻线程1不是从第1行开始执行的,而是根据自己的程序计数器来确定继续从第10行往下执行。
什么是程序计数器?
Java堆是一个线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
如上图是Java7和Java8的内存结构
首先说Java8,Java8中有年轻代和老年代。年轻代分为伊甸园(Eden)和两个幸存区(Survivor )。新产生的对象就会存放在伊甸园区,当进行Minor GC的时候就会将伊甸园中的部分垃圾回收,存留下来的会保存到幸存区。当幸存区的对象年龄超过15次时候没被垃圾回收就会进入老年代。详细步骤后面会进行讲解。
方法区保存的是Class类。Java8将方法区/永久代取消了,转到了本地内存。因为Java7中的方法区/永久代如果空间设置少了,就会造成堆空间内存溢出出现OOM,空间设置大了,就会造成堆空间浪费。所以移动到了本地内存。
你能给我详细的介绍Java堆吗?
Jdk1.7和1.8的区别
Java Virtual machine Stacks (java 虚拟机栈)
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放
未必,默认的栈内存通常为1024k栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半
如上图:
什么是虚拟机栈?
垃圾回收是否涉及栈内存?
栈内存分配越大越好吗?
方法内的局部变量是否线程安全?
什么情况下会导致栈内存溢出?
堆栈的区别是什么?
JDK1.8中将方法区移动到了直接内存的元空间中
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
首先如左图,里面有方法的机器指令,当执行行号为0的字节码指令的时候,就会根据该行字节码#2然后到常量池表中找到#2所在的位置,然后进行翻译,这样才能执行该行的指令,知道该行指令的作用。
常量池是*.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
如上图,运行时常量池将符号改为内存地址。因为只有获得真正的内存地址才能执行代码指令
能不能解释一下方法区?
介绍一下运行时常量池
堆存储的是使用new关键字创建的类(对象或者类的实例)或者数组的(含成员变量)
栈存放的是基本类型的变量数据和对象的引用。
对象的声明引用解释:
对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中),在Java中,当我们使用new关键字创建一个对象时,这个对象会被存储在堆中,并返回一个引用,这个引用会被存储在栈中。这个引用指向了堆中的对象,我们可以通过这个引用来访问和操作这个对象 。
- public class StackExample {
- public static void main(String[] args) {
- // 声明一个int类型的变量num,存储在栈中
- int num = 10;
- // 声明一个String类型的变量name,存储在栈中
- String name = "Bing";
- // 输出num和name的值
- System.out.println(num + " " + name);
- }
- }
Java方法区(Method Area)用于存储已被虚拟机加载的类信息、常量、静态变量、动态生成的类等数据。方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
直接内存:并不属于JM中的内存结构,不由JVM进行管理。是虚拟机的系统内存,常见于 NIO 操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高。
Java代码完成文件拷贝
java本身不具备磁盘的读写功能,如果调用磁盘读写,必须调用操作系统所提供的函数。
当调用操作系统所提供的IO方法的时候,就会设计CPU的状态切换,首先从用户态切换到内核态,当切换到内核态的时候,就通过系统提供的函数读取磁盘中的文件,读取的文件会分批次保存在操作系统内存划出的一片系统缓存区。但是系统缓存区java无法去获取,所以java会在堆中分配一片内存名为java缓冲区,将系统缓存区读到java缓冲区中。
读到java缓冲区后CPU切换到用户态,通过输出流写出操作,反复读取,然后将文件读到目标位置。
但是有个问题就是两个缓冲区会对性能产生巨大的影响
NIO不在使用两个缓冲区,而是直接使用直接内存来代替缓冲区,因此性能大幅提高
你听过直接内存吗?
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。
什么是类加载器
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来
类加载器有哪些
双亲委派:加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类。
比如自己自定义的student类,就会首先去它的上一级加载器,扩展类加载器去加载。然后扩展类还有上一级,就去启动类加载器去加载这个类。但是启动类加载器没有上一级了,这时候启动类加载器无法加载,就去下一级的扩展类加载器加载,扩展器加载器加载不了最后到应用类加载器加载。
如果是String类,那么就会去上一级扩展类加载器尝试加载,再去启动类加载器加载。发现启动类加载器可以加载,就在启动类加载器进行加载。然后直接返回给应用类加载器让它使用即可。
因为类加载器的加载顺序是 Bootstrap ClassLoader------>Extension ClassLoader--------->Application ClassLoader。因此当写了一个与JRE同名的类的时候,就会向上递归进行类加载。并且系统核心类和扩展类会在应用类加载前完成加载。所以当书写一个String类的时候,启动类加载器会判断该String类已经加载过了,所以不会在应用类加载器进行加载。这样保证了唯一性和安全性。
什么是双亲委派模型?
JVM为什么采用双亲委派机制?
通过类的全名,获取类的二进制数据流。
解析类的二进制数据流为方法区内的数据结构(Java类模型)
创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
如上图,类的信息保存在方法区中,方法区保存着类的类型,构造器,方法等类的结构信息。
如上图,目前Person创建了两个对象保存在堆中,分别name为张三和李四。
当方法区要创建两个对象,首先在堆中创建Person类的两个实例,然后在方法区中根据其类的信息创造其数据结构,然后对其进行堆中两个对象的地址进行引用,这样才可以加载成功,就可以使用这个对象了。
验证类是否符合 JVM规范,安全性检查
其中验证有四项验证:
文件格式验证、元数据验证、字节码验证是格式检查,如:文件格式是否错误、语法是否错误、字节码是否合规。
符号引用验证:Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,检查它们是否存在
为类变量分配内存并设置类变量初始值
static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成static变量是
final的引用类型,那么赋值也会在初始化阶段完成
如上图是已经完成赋值的操作
把类中的符号引用转换为直接引用
比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
如上图,类中的字节码文件右边#对应着常量池中的符号指令。解析就是将常量池中的符号引用转变为找到真正要执行的操作
如上图,字节码文件根据#4找到常量池表#4所对应的位置,并且该位置有着#24,#25两个符号引用,那么继续查表寻找
如上图是#24和#25在常量池的位置,它后面也有#31#32#33的符号位置,那么继续查找,直到最终找到要调用的指令。这就是解析,将符号引用转为直接引用,可以直接调用的指令的位置。
对类的静态变量,静态代码块执行初始化操作
如上图所示代码,有静态变量和静态代码块,并且有子类继承父类
JVM 开始从入口方法开始执行用户的程序代码
当用户代码执行完成,开始销毁Class对象
说一下类装载的执行过程?
简单一句就是,如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
如上图,栈中对象a对象b都对Demo类有引用,所以引用计数法+1为1。然后出现了a.instance=b和b.instance=a,所以引用计数法+1为2。当执行到了a=null和b=null的时候,各自类的计数法-1为1。
如上图,对象a和对象b都对该类没有引用,但是出现了各自引用,所以会出现对象之间相互循环引用的问题,会出现内存泄漏。
内存泄漏是指程序中已经不再使用的内存没有得到及时释放,导致系统内存的浪费。在Java中,内存泄漏通常是由于程序中存在一些不合理的对象引用导致的,例如:
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾
X,Y这两个节点是可回收的
对象什么时候可以被垃圾器回收
定位垃圾的方式有两种
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除
如上图,根据可达性算法,将内存空间中可达的对象标记出来,未标记的对象进行GC。如右图未清理完后的情况。
优点:
缺点:
标记整理算法,是将垃圾回收分为3个阶段
优点:
缺点:
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
优点:
缺点:
JVM 垃圾回收算法有哪些?
在java8时,堆被分为了两份: 新生代和老年代[1: 2]
对于新生代,内部又被分为了三个区域:
如上图。新建的对象都会放在Eden区
当Eden区满的时候,发生MinorGC,根据可达性算法进行标记,然后清空未标记的对象,清理后将未清空的对象保存在to区
当Eden区又满了的时候
根据可达性算法,将保留的对象从Eden区和to区保存到from区
当from区或者to区的对象保留次数超过15次的时候,就会将其移动到老年区
如上图,A已经保留15次,下次GC的时候,已经是第16次了,将A移动到Old区,from区和to区交换
流程:
STW (Stop-The-World): 停所有应用程序线程,等待垃圾回收的完成
因为Survivor存放的时候存活时间比较长的对象,但是也会出现垃圾回收的情况造成空间碎片。为了解决这种情况就会使用两个Survivor区,将Eden区和一个Survivor区存活的对象放到另一个Survivor区。这样就可以解决了这种情况。
为了解决空间碎片的问题,原因同上。
说一下JVM中的分代回收
在jvm中,实现了多种垃圾收集器,包括:
Serial和Serial old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
如上图,刚开始有多个线程在执行,当该线程发生GC的时候,该垃圾回收器就会停止其他线程,让垃圾回收线程运行,当垃圾回收完成以后,终止该线程的垃圾回收线程,让所有线程继续运行
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
如上图,刚开始有多个线程在执行,当该线程发生GC的时候,该垃圾回收器就会停止其他线程,让所有线程开启垃圾回收线程,当垃圾回收完成以后,终止所有线程的垃圾回收线程,让所有线程继续运行。相较于串行垃圾回收器,性能提高了很多。
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行
如上图,标记分为三次,分别为初始标记,并发标记,重新标记,其中初始标记是标记GC Roots直接关联的对象,并发标记标记的是与GC Roots直接关联的对象相关联的对象。如GC Roots——>A——>B,那么标记的就是B对象。重新标记是标记那些在并发标记过程中可能产生关联变化的对象。当全部标记完成后就进入并发清理,这个过程不会停止其他对象。
如上图,初始标记的是与GC Roots直接关联的对象A,并发标记标记的是与A对象相关联的对象B、C、D。之所以有重新标记,是因为在并发标记阶段其他线程正在运行,可能会出现刚开始的X没有与A相关联,在并发标记阶段后期A与X对象又有了关联这种情况,所以就出现了重新标记,这个阶段会处于STW阶段,所有线程被阻塞。
或者出现刚开始D对象和B对象有关联,到了重新标记阶段B对象和D对象没有关联了这种情况都有可能发生。
最后进行并发清除,这个不会阻塞其他线程,保证性能。
刚开始的时候,堆空间是空白的。堆空间被划分大小相等的区域。
当创建对象的时候,就会在堆中挑出一些空闲的区域作为Eden区域。上图中,在堆中标出了3个E代表创建了三个对象在Eden区。G1垃圾回收器中新生代的内存占比不是固定的而是波动的,大概为5%-6%之间,真实大小G1垃圾回收器在5%-6%之间自动调整。
因此当超出上限的时候就会进行新生代的垃圾回收。
如上图,G1垃圾回收器的新生代垃圾回收会根据可达性算法将垃圾对象标记出来,保留下的对象根据标记复制算法移动到Survivor区。
因此其他空间的内存就会被释放掉。注意在标记的过程中和垃圾清理的过程中要进行STW,由于新生代的垃圾比较少,所以暂停的时间很短。
随着时间的流逝,Eden区的垃圾又充满了,这时候再次进行新生代垃圾回收,将保存下的对象保存到Survivor区中。
Survivor区域中有年龄过大的对象,那么就会晋升到Old区。比如S区里面的所有对象都超过了15岁,那么就会全部晋升的Old区,并把Survivor区域的内存释放。
如上图,将Survivor区域的垃圾回收到Old区域。
当老年代占用内存超过闽值(默认是45%)后,触发并发标记,这时无需暂停用户线程
并发标记阶段之后,会有重新标记阶段,这个阶段需要暂停用户线程,这个阶段为了解决漏标问题。
这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少或垃圾对象多)的区域 (这也是Gabage First 名称的由来)
这个阶段是混合阶段,意味着不仅对Old区进行垃圾回收,还会对Eden区和Survivor区进行垃圾回收。
如上图,新增了两个区域,分别为Survivor区域和Old区域
对于混合收集,会将Eden区和Survivor区的存活对象复制到新的Survivor区域。因为Survivor中from区和to区总有一个区域是空的,所以复制算法会交换from区和to区,并将Eden存活的对象复制到Survivor区域中。
Survivor区域可能会有超出15岁年龄的对象,这些高龄对象和对其他Old区域进行GC存留下的对象都保存到新创建的Old区。
但是一般会进行多次混合收集,将剩下的Old区进行重新标记,因为有预期时间的限制,无法最大化的处理老年代的垃圾回收。G1垃圾回收器会执行多次混合收集的原因是为了尽可能地减少Full GC的次数,从而减少应用程序的停顿时间。
当所有混合收集完成以后,进入下一轮的新生代回收、并发标记、混合收集。
当老年代新生对象的速度大于垃圾回收的速度时,会导致老年代区域被填满,从而触发一次Full GC,Full GC的垃圾回收会很长。
当一个对象很大的时候,就会存储到一个巨型对象中,如果一个区域不够的话就会分配一块连续的区域用来保存该对象。如果没有连续的区域来存储巨型对象,JVM会将该对象直接分配到老年代。如果连老年代都没有足够的空间来存储巨型对象,那么就会抛出OutOfMemoryError。在这种情况下,JVM会尝试执行Full GC,以释放内存并为巨型对象腾出空间。
只有所有GCRoots 对象都不通过强引用引用该对象,该对象才能被垃圾回收
如上图,这个User对象无法被垃圾回收
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收
如上图,当开始进行垃圾回收的时候,不会对User对象进行垃圾回收。当垃圾回收之后仍然内存不够那么就会触发对User对象的垃圾回收。
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
如上图,该User对象是一个弱引用对象。当触发垃圾回收的时候,不管是否内存不够,都会对User对象进行垃圾回收
必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被回收以后,提醒进行下一步某些事情的操作。
如上图,比如当User1和User2被垃圾回收以后,就会将虚引用对象放入引用队列。当加入引用队列后,就会配合Reference Handler来释放虚引用对象关联的一些外部资源。比如说User1和User2只是占用Java虚拟机中的内存,但是User1和User2还关联着外部资源,如直接内存。那么就需要将垃圾回收后的虚引用对象放入引用队列,Reference Handler用来监测引用队列,看哪些对象被回收就直接找引用队列就可以了。当Reference Handler发现引用队列有X和Y对象,那么就会释放与X和Y外部相关联的资源。
强引用、软引用、弱引用、虚引用的区别?
修改TOMCAT HOME/bin/catalina.sh文件
通常在linux系统下直接加参数启动springboot项目
主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型
设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值。
不指定单位默认为字节指定单位,按照指定的单位设置
堆空间设置多少合适?
每个线程默认会开启1M的内存,用于存放栈、调用参数、局部变量等,但一般256K就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。通过增大Eden区的大小来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。
如果改为3代表survivor:eden=2:3
默认为15
取值范围0-15
通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器
命令工具
可视化工具
jsp能够查看当前正在运行的进程,前面的数字为进程ID
在该进程的所有进程中,可以看到创建的3个线程详细信息
用于生成堆转内存快照、内存使用情况
如上图,可以看到里面有当前堆的所有情况信息,包括设置的参数,初始化大小,当前剩余大小
如上图,可以看到里面的新生代的survivor区和eden区的使用情况,以及老年代的使用情况。
如上图 使用jmap -dump功能,file文件后面是设置的路径,最后面的数字是该进程的端口号。
是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等.
用于对jvm的内存,线程,类的监控,是一个基于jmx的GUI性能监控工具
打开方式:java安装目录 bin目录下直接启动jconsole.exe 就行
能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下直接启动ivisualvm.exe就行
Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中
使用jmap命令获取运行中程序的dump文件
当项目启动起来了,但是项目里的部分模块没有启动起来,那么这个命令就无法查询这个未启动的模块的信息
该命令只能查看已经运行的进程,因此可以使用下面方法查看未启动的进程详细信息
使用vm参数获取dump文件
- import java.util.ArrayList;
- import java.util.List;
-
- public class Test2 {
- public static void main(String[] args) {
- List<byte[]> list = new ArrayList<byte[]>();
- for (int i = 0; i < 1000000; i++) {
- byte[] bytes = new byte[1024 * 1024];
- list.add(bytes);
- }
- }
- }
JVM的参数设置:
-Xmx10m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\桌面\测试文件
报错:
可以看到保存的文件:
打开VisualVM
选择装入,找到hprof文件
如图:
这里显示的出现OOM
点击main,就可以看到错误的位置
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。