1、jvm组成部分以及作用?
* 方法区 (主要用于存储类信息,常量,静态变量即编译后的代码等数据)
* 堆 (虚拟机最大的一块内存了,主要用于存储所有的对象实例)
* 虚拟机栈 (主要线程局部变量表,操作数栈,动态链接,方法等信息)
* 本地方法栈 (作用于虚拟机栈一样,但它主要用于为虚拟机调优本地(Native)的方法服务的)
* 程序计数器 (主要记录当前线程执行的信息,字节码指令,循环跳转,结束,异常处理线程恢复等)
2、JVM 主要的作用:
1、编译器将java文件(.java)编译成字节码文件(.class)
2、类加载器将字节码文件从硬件中加载至内存中
3、解析器将字节码文件解析成底层系统能识别的指令
4、交由CPU 执行(期间需要调用本地的方法库配合完成程序的执行)
3、深浅拷贝
* 浅拷贝 - > 只是增加了一个指针指向该内存地址
* 深拷贝 - > 不仅仅新增一个申请一个内存地址并且增加了一个指针指向这个新增的内存地址在释放内存的时候,深拷贝不会像浅拷贝那样释放同一个内存的错误
4、堆和栈的区别
* 物理地址
堆 -> 物理地址是不连续的(只有运行时候才能确定需要分配的大小),性能会差些,因此在GC 的时候需要考虑到
栈 -> 地址是连续的(类加载时期就知道需要分配的大小),性能会快些
* 内存
堆 -> 因为内存大小不固定,所以大小不一,导致内存不连续,一般比栈要大很多
栈 -> 内存大小固定,内存时连续的
* 作用(存放的内容)
堆 -> 主要存储运行时期的对象实例,关注的是数据的存储,线程共享的,
栈 -> 主要记录局部变量,操作数栈,返回结果,主要关注方法的执行,线程独享的
5、队列和栈的区别
队列和栈都是用来存储数据的
操作名:入队出队,进栈出栈
操作方式:队头出,队尾入,2头都可以做操作(比作:水管)。入栈出栈都在栈顶(比作:水桶)
出入顺序: 队列是FIFO(先进先出) ,栈是LIFO(后进先出)
6、对象加载的几个过程?
加载、验证、准备、解析、初始化。然后是使用和卸载了通过全限定名来加载生成class对象到内存中,然后进行验证这个class文件,包括文件格式校验、元数据验证,字节码校验等。
准备是对这个对象分配内存。
解析是将符号引用转化为直接引用(指针引用),
初始化就是开始执行构造器的代码类的实例化顺序 父类的static代码块及static变量(依赖书写顺序),当前类的static代码块及static变量(依赖书写顺序),父类普通代码块及变量,父类的构造函数,子类普通代码及变量,子类构造函数。
Java对象结构
Java对象由三个部分组成:对象头、实例数据、对齐填充。对象头由两部分组成,
第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。
第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
为什么需要双亲委派机制?
如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码
Java 虚拟机是如何判定两个 Java 类是相同的?
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。
即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。
两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。
对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。
6、创建一个对象JVM做了哪些事?Object obj = new Object()
1. 判断是否加载类。没有加载类的先加载至常量池
2. 内存是否规整,规整的话,直接分配内存(即指针碰撞)或 空闲列表
3. 处理并发问题,
4. 分配了的内存空间初始化及必要的对象设置(0,null,false)
7、内存分配判断堆是否规整
采用哪种分配方式,是根据堆是否规整来决定的,
而堆是否规整是由垃圾收集器(回收算法)来决定的来决定的.
8、分配内存竞争即并发安全问题
处理并发安全方式:(1、case (乐观锁) 2、悲观锁)
9、对象访问定位
访问对象的2种方式 :直接指针,句柄
直接指针: 直接指向该对象对应的内存地址
句柄:有点类似间接指针的意思,即有个中间层,句柄不直接指向对象,指向对象的指针,由对象的指针指向实际的内存地址
优势:
* 直接指针 ,速度快
* 句柄访问 ,稳定(当垃圾回收移动对象时,引用本身不需要改变,只需要改变句柄中的实例数据指针)
垃圾回收
10、Java 会内存溢出吗? 为什么?
原因: java内存泄露指的就是:不被使用的对象一直在内存中没有被清除,理论上来说, java是有GC的,就是通过判断该对象是否需要被回收,但是即便这样Java还是存在内存溢出 为什么?
长生命周期的对象对短生命周期的引用,这样就造成了内存泄露等
11、垃圾回收(GC : Gabage Collection)
什么是GC?
GC 就是垃圾会回收 为什么需要GC ?
目的就是,释放一定的内存,防止因为内存泄露造成系统不稳定或者奔溃,当然垃圾回收也肯定导致系统不稳定或奔溃,但是java提供GC监控对象是否超过作用域而达到回收内存的目的。
12、垃圾回收的优点和原理
优点 : 它可以让程序猿不必关注内存管理和回收,垃圾回收可以很有效的防止内存泄露,但也不是绝对的,并且可以有效的使用有效的内存。 原理:对象创建的时候,就会GC 就会监控该对象的地址,大小, 以及使用情况,然后当触发了GC的时候,就是通过判断对象是否存在引用(引用计数法,可达性分析) ,从而确定是否需要回收该对象
13、java有哪些引用?
* 强引用 -> 发生GC时不会回收(说明正在使用该对象),哪怕抛出OOM错误,强行回收可能导致系统出错
* 弱引用 -> 由于GC优先级很低,可能不会马上回收,会在下一次GC回收
* 软引用 -> 发生内存泄露之前会回收
* 虚引用 -> 如果一个对象是有虚引用引用,那么GC 可以任何时候回收,并在GC时返回一个通知
14、如何判断对象是否可以被回收?
在垃圾回收的时候,需要判断哪些对象是否需要回收,哪些还存活?
一般有2种方式:
* 引用计数法 : 为每个对象创建一个引用,引用计数器+1,释放一个引用引用计数器-1,那么当引用计数为0的时候,就可以清除了。
有个缺点就是没有解决循环引用的问题
* 可达性分析法:从GCRoot 往下遍历,遍历走过的每条链路,称为引用链,当一个对到GCRoot没有任何引用链,那么说明此对象可以回收了
哪些对象可以作为GCRoot对象。
* java虚拟机栈中引用的对象
* 方法区中静态对象引用的对象(static,类加载的时候就已存入内存中)
* 方法区中常量引用的对象
* 本地方法(Native)中引用的对象
15、垃圾回收有哪些算法?
* 标记-清除算法
先将需要清除对象,然后在清除 。缺点:内存区域不连续,碎片比较多,效率不高
* 标记-整理算法
先标记需要清除的对象,整理在一端,然后直接清除
* 复制算法
将内存区域一分为二,将存活的对象复制到其中一半区域内,然后将另外一半一次性清理掉。 缺点:内存空间利用率不高,只有原来的一半
* 分代算法
根据对象的存活周期,将内存区域划分几块,一般分新生代,老年代。新生代复制算法,老年代标记整理算法
16、垃圾回收器有哪些?
* Serial : 比较老的收集器 ,采用的复制算法。它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器
* ParNew (serial 的升级,多线程版本)
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器
* parallel scavenge (并行多线程收集器),关注吞吐量
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间) 比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。
算法:复制算法
适用范围:新生代
* srial old (serial 的老年代版本)
算法:标记-整理,其他跟serial 是一样的
* parallel old (parallel scavenge的老年代版本 ,多线程,标记-整理算法,关注是吞吐量)
* CMS (Concurrent mark sweep) 关注最短回收停顿时间
采用的是 : 标记 -清除算法 分以下几步:
(1)初始标记 CMS initial mark 标记GC Roots能关联到的对象 Stop The World--->速度快
(2)并发标记 CMS concurrent mark 进行GC Roots Tracing
(3)重新标记 CMS remark 修改并发标记因用户程序变动的内容 Stop TheWorld
(4)并发清除 CMS concurrent sweep
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
* G1 (Garbage First)标记-整理算法 ,并行与并发 ,JDK 1.7才开始提供新的收集器,使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合 。
G1工作主要分以下几步:
初始标记(Initial Marking) 标记一下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程
并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
针对以上的收集器 分个类
* 串行收集器 -> serial ,serial old
只有一个垃圾回收线程,并且用户线程会暂停 (适合内存比较小的嵌入式设备)
* 并行收集器 【停顿优先】 ->CMS ,G1
用户线程和垃圾回收器同时执行(不一定并行,可能会交替进行),垃圾线程执行时不会停顿用户线程(适用于用户交互,时间要求比较高的的任务,
如:web项目,相应时间要求比较快)停顿时间越短,客户体验越好
* 并行收集器 【吞吐量优先】-> parallel scavenge 和 parallel old
多个线程并行工作,但用户仍处于等待状态(适用于:大量的科学计算,后台计算等任务)尽量减少垃圾回收时间 ,吞吐量越大,垃圾回收时间越短
新生代 和 老年代垃圾收集器有哪些?
新生代(复制算法):serial ,parNew,parallel scavenge
老年代(标记-整理算法): CMS(标记清除) ,parallel old,serial old
整堆回收:G1(JDK 7开始使用,JDK 8非常成熟,JDK 9默认的垃圾收集器,适用于新老生代)
判断是否需要使用G1收集器
(1)50%以上的堆被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长
JVM 调优工具
* jconsole:用于对 JVM 中的内存、线程和类等进行监控;
* jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
Java中Stop-The-World机制
简称STW,是在执行垃圾收集算法时,
Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
如何开启垃圾收集器
(1)串行
-XX:+UseSerialGC
-XX:+UseSerialOldGC
(2)并行(吞吐量优先):
-XX:+UseParallelGC
-XX:+UseParallelOldGC
(3)并发收集器(响应时间优先)
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
(4) 打印GC 日志
-XX: +PrintGC
-XX: +PrintGCDetails (详细信息)
(5)设置对内存
-Xms2g (初始化内存为2G)
-Xmx2g (最大内存为2G)
jvm 实战
待更新
注: 本博客有参考大神的部分内容来源