类加载运行全过程
![](https://i-blog.csdnimg.cn/blog_migrate/649b9123fa7de64bcded80c12ed1fe03.png)
1.java.exe调用底层jvm.dll创建java虚拟机,
2.创建引导类加载器实例,
3.完成实例的创建后需要一个启动器,这个是通过sun.misc.launcher类实现,通过该类的getLaunch()方法启动,
4.然后调用该类的getClassLoader()方法加载需要运行的类,
加载类完成后后执行main()方法。
2.问题1中第四步类加载的过程
![](https://i-blog.csdnimg.cn/blog_migrate/ff9039b0301859730d104ca1bf6bb973.png)
JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。
1、加载
加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。
类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口
假设要出国旅游,加载过程理解为把需要的东西打包装行李箱带到机场要去坐飞机了(不太恰当的比喻)
2、连接过程
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。
1. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。
2. 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配
3. 解析:虚拟机常量池的符号引用替换为字节引用过程
连接过程可以理解为整个坐飞机的过程,包括检票(验证)、准备起飞(准备阶段)、入境检查(解析阶段)。
3、初始化
初始化阶段是执行类构造器 <clinit> () 方法的过程。类构造器 <clinit> ()方法是由编译器自动收藏类中的 所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
虚拟机会保证一个类的 <clinit> () 方法在多线程环境中被正确加锁和同步
初始化的总结就是:初始化是为类的静态变量赋予正确的初始值
初始化就是到地方了找个落脚地点(类的静态变量赋予正确的初始值)
3.类加载器有哪些?
自定义加载器,应用程序加载器,扩展类加载器,启动类加载器四个
启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等(老爷爷)
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包(爷爷)
应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类(父亲)
自定义加载器:负责加载用户自定义路径下的类包(儿子)
作用:加载class文件
![](https://i-blog.csdnimg.cn/blog_migrate/2132a851bb697a2e6e92ca09242359aa.png)
4. 什么是双亲委派机制?
简单的说就是自己先不加载,先向上找自己的副加载器加载。双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载。
1)类加载器收到类的加载请求
2)将这个请求向上委托给父类(上一级加载器),直到启动类加载器
3)启动类加载器检查是否能加载这个类,能加载就结束,不能加载就抛出异常,让子加载器加载。
4)重复上一步骤。
![](https://i-blog.csdnimg.cn/blog_migrate/d7a962a29390afb3253433a10a7ad34b.png)
5. 为什么采用双亲委派机制?
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。(例如重写一个String类,用户输入密码后直接上传到服务器,直接窃取用户账号密码,但是不好意思,自己写的这个String类不会被加载。)
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
6. 如何打破双亲委派机制?
自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。(tomcat 类加载机制违背了java 推荐的双亲委派模型,知道有这么个事就行了)
7. jvm的位置(操作系统之上)
![](https://i-blog.csdnimg.cn/blog_migrate/e775ec379b0863f90be92d1d7733f9e9.png)
8. 说一下 JVM由那些部分组成,运行流程是什么?
![](https://i-blog.csdnimg.cn/blog_migrate/07c3645fd7d2bdc2056207db6d1fccba.png)
JVM包含两个子系统和两个组件:
两个子系统为Class loader(类装载)、Execution engine(执行引擎);
两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
Execution engine(执行引擎):执行classes中的指令。
Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
流程 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
9. 说一下 JVM 运行时数据区
简单的说就是我们java运行时的东西是放在那里的
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;为什么要线程计数器?因为线程是不具备记忆功能
Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息;栈帧就是Java虚拟机栈中的下一个单位
本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码
Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
程序计数器
1. 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号)
2. 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域
(简单的说,就是个存指令地址的地方)
虚拟机栈
1. Java虚拟机是线程私有的,它的生命周期和线程相同。
2. 虚拟机栈描述的是Java方法执行的内存模型: 每个方法在执行的同时 都会创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
解释:虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。
![](https://i-blog.csdnimg.cn/blog_migrate/9465038f909df51c0b3cc017edab0f40.png)
解析栈帧:
1. 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
2. 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
3. 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
4. 出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落
一个方法调用另一个方法,会创建很多栈帧吗?
答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面
栈指向堆是什么意思?
栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址
递归的调用自己会创建很多栈帧吗?
答:递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去
堆
java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
从内存回收角度来看java堆可分为:新生代和老生代。
从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
本地方法栈
1. 本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字
2. 它是虚拟机栈为虚拟机执行Java方法(也就是字节码)的服务方法
3. native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。
4. 同理可得,本地方法栈中就是C和C++的代码
方法区--1.8后叫元空间(Metaspace)
线程共享,存所有方法和字段的字节码,即所有定义的方法信息都在方法区,还包括静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池,但是实例变量在堆内存中。被static、funal修饰的变量在方法区。
10. 堆栈的区别
比 | JVM堆 | JVM栈 |
物 理 地 址 | 堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩) | 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。 |
内 存 分 别 | 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。 | 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。 |
存 放 的 内 容 | 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储 | 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。 |
程 序 的 可 见 度 | 堆对于整个应用程序都是共享、可见的。 | 栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。 |
11. 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
我的理解就是直接内存是基于物理内存和Java虚拟机内存的中间内存
12. 垃圾收集系统
程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。
垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理
有一部分原因就是因为Java垃圾回收系统的强大导致Java领先市场
13. Java会存在内存泄漏吗?请说明为什么?
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露, 尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收 ,这就是java中内存泄露的发生场景。
14. GC是什么?为什么要GC?
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
15. 垃圾回收的优点和缺点
优点:JVM的垃圾回收器都不需要我们手动处理无引用的对象了,这个就是最大的优点
缺点:程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。
16. 垃圾回收器的原理是什么?有什么办法手动进行垃圾回收?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
17. 堆内存的分配及垃圾回收
1.堆内存的分配
堆存放引用对象的真实对象,JVM调优主要是堆内存的调优,垃圾回收通常也是发生在堆内存。
![](https://i-blog.csdnimg.cn/blog_migrate/138f28563a9486c8411013fedda24158.png)
堆内存的分配:
-Xms:设置堆的初始可用大小,默认物理内存的1/64
-Xmx:设置堆的最大可用大小,默认物理内存的1/4
新生代new -Xmn:新生代大小
Eden:伊甸园区
Survivor s0: 幸存区0区
Survivor s1:幸存区1区
老年代tenured(old)
默认大小比例:
新生代:老年代=1:2 -XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
Eden : s0 : s1 =8:1:1 -XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了 复制算法 ,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用 “标记-清理”或者“标记-整理” 算法。
永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收
2. 为什么新生代要分Eden和两个 Survivor 区域?
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。
设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次MinorGC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
3. Minor GC、Major GC和Full GC 有什么不同呢?
1. Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾)
2. Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法)
3. Full GC是清理整个堆空间,包括年轻代和老年代
4. Minor GC、Major GC、Full GC区别及触发条件
Minor GC 触发条件一般为:
1. eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
2. 新创建的对象大小 > Eden所剩空间时触发Minor GC
Major GC和Full GC 触发条件一般为: Major GC通常是跟full GC是等价的
1. 每次晋升到老年代的对象平均大小>老年代剩余空间
2. MinorGC后存活的对象超过了老年代剩余空间
3. 永久代空间不足
4. 执行System.gc()
5. CMS GC异常
6. 堆内存分配很大的对象
5. JVM垃圾回收的理论依据
分代收集理论当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。
18. JVM 垃圾回收算法有哪些?
标记-清除算法
标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:
标记阶段:标记出可以回收的对象。
清除阶段:回收被标记的对象所占用的空间。
![](https://i-blog.csdnimg.cn/blog_migrate/f6c34dca834669f68d1b9818ac0ae740.png)
第一步:标记出可以回收的对象
![](https://i-blog.csdnimg.cn/blog_migrate/6f73558ba5356a9bdf1edf7cc448df59.png)
第二步:回收被标记的对象所占用的空间。
![](https://i-blog.csdnimg.cn/blog_migrate/9cf1cbc189beb070b142c3495a19c799.png)
优点:实现简单,不需要对象进行移动。
缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
标记-整理算法
标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(MarkCompact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。(在标记清除算法的基础上增加了一次对象压缩操作,解决了内存碎片化问题)
优点:解决了标记-清理算法存在的内存碎片问题。
缺点:仍需要进行局部对象移动,一定程度上降低了效率。
标记-整理算法的执行过程如下图所示
![](https://i-blog.csdnimg.cn/blog_migrate/0012dccbb3bdb2e8ecea1441c070e4ae.png)
复制算法
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
复制算法的执行过程如下图所示
![](https://i-blog.csdnimg.cn/blog_migrate/e568072881b80f859dcf050b2ba5c876.png)
三种算法总结:
内存效率(时间复杂度):复制算法>标记清除算法>标记压缩算法
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存使用率:标记压缩算法=标记清除算法>复制算法
年轻代:存活率低, 复制算法
老年代:存活率高,区域大 标记清除+标记压缩算法
19. 说一下 JVM 有哪些垃圾回收器?
垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现!!!
![](https://i-blog.csdnimg.cn/blog_migrate/504d6baa3f7b2e17555b6b8e2db9b8ff.png)
垃圾回收器 | 工作区域 | 回收算法 | 工 作 线 程 | 用户线程并行 | 描述 |
Serial | 新生代 | 复制算法 | 单 线 程 | 否 | Client模式下默认新生代收集器。 简单高效 |
ParNew | 新生代 | 复制算法 | 多 线 程 | 否 | Serial的多线程版本,Server模式下首选, 可搭配CMS的新生代收集 器 |
Parallel Scavenge | 新生代 | 复制算法 | 多 线 程 | 否 | 目标是达到可控制的吞吐量 |
CMS | 老年代 | 标记-清除 | 多线程 | 是 | 以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间 |
Serial Old | 老年代 | 标记-整理 | 单 线 程 | 否 | Serial老年代版本,给Client模式下的虚拟机使用 |
Parallel Old | 老年代 | 标记-整理 | 多 线 程 | 否 | Parallel Scavenge老年代版本,吞吐量优先 |
G1 | 新生代 + 老年代 | 标记-整理 + 复制算法 | 多 线 程 | 是 | JDK1.9默认垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征. |
JDK 1.8默认使用 Parallel(年轻代和老年代都是)
JDK 1.9默认使用 G1
20. 简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
清空 Eden 和 From Survivor 分区;
From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变
From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
21. 为什么大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。
前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。
22. 垃圾回收器的选择
在生产环境中我们要根据不同的场景来选择垃圾收集器组合,如果是运行在桌面环境处于 Client 模式的,则用 Serial + Serial Old 收集器绰绰有余,如果需要响应时间快,用户体验好的,则用 ParNew + CMS 的搭配模式,即使是号称是「驾驭一切」的 G1,也需要根据吞吐量等要求适当调整相应的 JVM 参数,没有最牛的技术,只有最合适的使用场景,切记!
23. JVM调优
JVM 调优的参数可以在那设置参数值
可以在IDEA,Eclipse,工具里设置
如果上线了是WAR包的话可以在Tomcat设置
如果是Jar包直接 :java -jar 是直接插入JVM命令就好了
java -Xms1024m -Xmx1024m ...等等等 JVM参数 -jar springboot_app.jar &
2. 说一下 JVM 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
jconsole:用于对 JVM 中的内存、线程和类等进行监控;
![](https://i-blog.csdnimg.cn/blog_migrate/edf6b596633f32bf65d2d123f23f5e6a.png)
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
3.常用的 JVM 调优的参数都有哪些?
#常用的设置
-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。
-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。
-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。51. JVM的GC收集器设置
-xx:+Use xxx GC
xxx 代表垃圾收集器名称
-XX:NewSize=n 设置年轻代初始化大小大小
-XX:MaxNewSize=n 设置年轻代最大值
-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:
3,年轻代占整个年轻代+年老代和的 1/4
-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8
表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8
-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。
-XX:ThreadStackSize=n 线程堆栈大小
-XX:PermSize=n 设置持久代初始值
-XX:MaxPermSize=n 设置持久代大小
-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经
过 Survivor 区,直接进入年老代。
#垃圾回收器设置
-XX:+UseSerialGC:设置串行收集器,年轻带收集器
-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统
配置自行设置,所以无需再设置此值。
-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量
-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。
-XX:+UseConcMarkSweepGC:设置年老代并发收集器
-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器
#下面是一些不常用的
-XX:LargePageSizeInBytes=n 设置堆内存的内存页大小
-XX:+UseFastAccessorMethods 优化原始类型的getter方法性能
-XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用
-XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集
等,jdk6纸之后默认启动
-XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用
-Xnoclassgc 是否禁用垃圾回收
-XX:+UseThreadPriorities 使用本地线程的优先级,默认启用
24. JVM中对象的创建过程
![](https://i-blog.csdnimg.cn/blog_migrate/45b916685b6b8a3413e783336c4b9218.png)
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
![](https://i-blog.csdnimg.cn/blog_migrate/a54c1a02b6b9e8c8542c5a53f82bca13.png)
5.执行方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
25. JVM对象的回收
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
引用计数法
(已经不用了,知道有这么个东西就行了)
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
可达性分析算法
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
![](https://i-blog.csdnimg.cn/blog_migrate/97f59142d546e1ed71b9c536123d86c6.png)
26. 常见引用类型-强引用、软引用、弱引用、虚引用的区别?
强引用:普通的变量引用,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
public static User user = new User();
软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
public static SoftReference<User> user = new SoftReference<User>(new User());
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
public static WeakReference<User> user = new WeakReference<User>(new User());
虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
总结:强引用不回收,软引用没空间了就回收,弱引用下次回收一定回收,虚引用不咋用
27. JVM调优实例
![](https://i-blog.csdnimg.cn/blog_migrate/458f596e5dd0216948e92765d87503b5.png)
结论:通过上面这些内容介绍,大家应该对JVM优化有些概念了,就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。