菜鸟笔记
提升您的技术认知

jvm原理和调优-ag真人游戏

面试必问:jvm原理和调优(附面试题)

  • 一、详解jvm内存模型
  • 二、jvm中一次完整的gc流程是怎样的
  • 三、gc垃圾回收的算法有哪些
  • 四、简单说说你了解的类加载器
  • 五、双亲委派机制是什么,有什么好处,怎么打破
  • 六、说说你jvm调优的几种主要的jvm参数
  • 七、jvm调优
  • 八、类加载的机制及过程
  • 九、jdk1.7到jdk1.8 java虚拟机发⽣了什么变化?
  • 十、你们项目如何排查jvm问题 ?
  • 十一、深拷贝和浅拷贝
  • 十二、说⼀下jvm中,哪些可以作为gc root
  • 十三、jvm诊断工具有哪些?
  • 十四、为什么要使用stw?

jvm有本地方法栈、虚拟机栈、程序计数器、堆、方法区。
jvm内存分为共享区(可以被所有方法(线程)直接访问)和私有区(对线程来说是私有的,其他线程无法直接访问)。
在共享区里包含着堆和方法区,在私有区里包含着程序计数器、虚拟机栈和本地方法栈。

程序计数器pc:是一个行号计数器,程序在进行跳转时,我们要记住跳转的行号,它方便我们的程序进行还原。
虚拟机栈:包含了java方法执行时的状态,每一个java方法都会在虚拟机栈里面创建一个栈帧,里面存放局部变量表、操作数栈、动态链接、方法出口等。
本地方法栈:跟虚拟机栈类型,在用于调用操作系统的底层方法时才会创建栈帧。
堆:用来保存着java程序运行时的变量,比如new的对象。
方法区:则保存着静态的东西,比如静态变量、常量、类的信息、方法的申明等。

面试题必问:java虚拟机内存模型


java堆 = 老年代 新生代
新生代 = eden s0 s1

当 eden 区的空间满了, java虚拟机会触发一次 minor gc,以收集新生代的垃圾,存活下来的对象,则会转移到 survivor区。
大对象(需要大量连续内存空间的java对象,如那种很长的字符串)直接进入老年态;
如果对象在eden出生,并经过第一次minor gc后仍然存活,并且被survivor容纳的话,年龄设为1,每熬过一次minor gc,年龄 1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。
老年代满了而无法容纳更多的对象,minor gc 之后通常就会进行full gc,full gc 清理整个内存堆 – 包括年轻代和老年代老年代。
major gc 发生在老年代的gc,清理老年区,经常会伴随至少一次minor gc,比minor gc慢10倍以上。

引用计数算法
跟踪回收算法
压缩回收算法
复制回收算法
按代回收算法
浅析java的垃圾回收机制(gc)和五种常用的垃圾回收算法

类加载器 就是根据指定全限定名称将class文件加载到jvm内存,转为class对象。

启动类加载器(bootstrap classloader):由c 语言实现(针对hotspot),负责将存放在\lib目录或-xbootclasspath参数指定的路径中的类库加载到内存中。

扩展类加载器(extension classloader):负责加载\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。

应用程序类加载器(application classloader): 负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

双亲委派机制:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即classnotfoundexception),子加载器才会尝试自己去加载。

双亲委派模型的好处:
●主要是为了安全性,避免用户自己编写的类动态替换java的一 些核心类,比如string。
●同时也避免了类的重复加载,因为jvm中区分不同类,不仅仅是根据类名,相同的class文件被不同的classloader加载就是不同的两个类。

打破双亲委派机制则不仅要继承classloader类,还要重写loadclass和findclass方法。

1)堆栈配置相关

java -xmx3550m -xms3550m -xmn2g -xss128k 
-xx:maxpermsize=16m -xx:newratio=4 -xx:survivorratio=4 -xx:maxtenuringthreshold=0

-xmx3550m: 最大堆大小为3550m。

-xms3550m: 设置初始堆大小为3550m。

-xmn2g: 设置年轻代大小为2g。

-xss128k: 设置线程堆栈大小为128k。

-xx:maxpermsize: 设置持久代大小为16m

-xx:newratio=4: 设置年轻代(包括eden和两个survivor区)与老年代的比值(除去持久代)。

-xx:survivorratio=4: 设置年轻代中eden区与survivor区的大小比值。设置为4,则两个survivor区与一个eden区的比值为2:4,一个survivor区占整个年轻代的1/6

-xx:maxtenuringthreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过survivor区,直接进入老年代。

2)垃圾收集器相关

-xx: useparallelgc
-xx:parallelgcthreads=20
-xx: useconcmarksweepgc 
-xx:cmsfullgcsbeforecompaction=5
-xx: usecmscompactatfullcollection:

-xx: useparallelgc: 选择垃圾收集器为并行收集器。

-xx:parallelgcthreads=20: 配置并行收集器的线程数

-xx: useconcmarksweepgc: 设置老年代为并发收集。

-xx:cmsfullgcsbeforecompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次gc以后对内存空间进行压缩、整理。
-xx: usecmscompactatfullcollection: 打开对老年代的压缩。可能会影响性能,但是可以消除碎片

3)辅助信息相关

-xx: printgc
-xx: printgcdetails

-xx: printgc 输出形式:

[gc 118250k->113543k(130112k), 0.0094143 secs] [full gc 121376k->10414k(130112k), 0.0650971 secs]

-xx: printgcdetails 输出形式:

[gc [defnew: 8614k->781k(9088k), 0.0123035 secs] 118250k->113543k(130112k), 0.0124633 secs] [gc [defnew: 8614k->8614k(9088k), 0.0000665 secs][tenured: 112761k->10414k(121024k), 0.0433488 secs] 121376k->10414k(130112k), 0.0436268 secs

1.将堆的最大、最小设置为相同的值,目的是防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间。
-xmx3550m: 最大堆大小为3550m。
-xms3550m: 设置初始堆大小为3550m。

2.在配置较好的机器上(比如多核、大内存),可以为老年代选择并行收集算法: -xx: useparalleloldgc 。

3.年轻代和老年代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率来调整二者之间的大小,也可以针对回收代。

比如年轻代,通过 -xx:newsize -xx:maxnewsize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-xx:newsize -xx:maxnewsize设置为同样大小。

4.年轻代和老年代设置多大才算合理

1)更大的年轻代必然导致更小的老年代,大的年轻代会延长普通gc的周期,但会增加每次gc的时间;小的老年代会导致更频繁的full gc

2)更小的年轻代必然导致更大老年代,小的年轻代会导致普通gc很频繁,但每次的gc时间会更短;大的老年代会减少full gc的频率

如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大。但很多应用都没有这样明显的特性。

在抉择时应该根 据以下两点:
(1)本着full gc尽量少的原则,让老年代尽量缓存常用对象,jvm的默认比例1:2也是这个道理 。
(2)通过观察应用一段时间,看其他在峰值时老年代会占多少内存,在不影响full gc的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给老年代至少预留1/3的增长空间。

加载>>验证>>准备>>解析>>初始化

(1)加载:在硬盘上查找并通过i0读入字节码文件,在堆内存中生成一个对象,作为方法区数据的访问入口
(2)验证: 校验字节码文件的正确性
(3)准备:给类的静态变量分配内存,并赋予默认值
(4)解析(动态链接): 将静态方法的符号引用替换为直接引用(有对应的内存地址信息)
(5)初始化: 给类的静态变量初始化为指定的值,执行静态代码块

1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所占的内存不是在虚拟机内部,⽽是本地 内存空间,这么做的原因是,不管是永久代还是元空间,他们都是⽅法区的具体实现,之所以元空间所占 的内存改成本地内存,官⽅的说法是为了和jrockit统⼀,不过额外还有⼀些原因,⽐如⽅法区所存储的类 信息通常是⽐较难确定的,所以对于⽅法区的⼤⼩是⽐较难指定的,太⼩了容易出现⽅法区溢出,太⼤了 ⼜会占⽤了太多虚拟机的内存空间,⽽转移到本地内存后则不会影响虚拟机所占⽤的内存。

分两种情况
①对于还在正常运⾏的系统:

  1. 可以使⽤jmap来查看jvm中各个区域的使⽤情况;
  2. 可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁
  3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏调优了 ;
  4. 通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析 ;
  5. ⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表示 fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对 象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较大,导致年轻代放不 下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明修改有效;
  6. 同时,还可以找到占⽤cpu最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免某些 对象的创建,从⽽节省内存 。

② 对于已经发⽣了oom的系统:
1.⼀般⽣产系统中都会设置当系统发⽣了oom时,⽣成当时的dump⽂件(- xx: heapdumponoutofmemoryerror -xx:heapdumppath=/usr/local/base)
2. 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
3. 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤cpu⾼),定位到具体的代码
4. 然后再进⾏详细的分析和调试

深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本數据类型, 一种是实例对象的引用。

1.浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象, 也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象

2深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象

首先,gc root是根对象,jvm在进⾏垃圾回收时,需要找到“垃圾”对象,也就是没有被引⽤的对象,但是直接找“垃圾”对象是⽐较耗时的,所以反过来,先找“⾮垃圾”对象,也就是正常对象,那么就需要从某些“根”开始去找,根据这些“根”的引⽤路径找到正常对象,⽽这些“根”有⼀个特征,就是它只会引⽤其他对象,⽽不会被其他对象引⽤,例如:栈中的本地变量、⽅法区中的静态变量、本地⽅法栈中的变量、正在运⾏的线程等可以作为gc root。

阿里的arthas
jdk自带jvm诊断工具:java visualvm

full gc把整个堆都回收
oom:内存溢出 out of memory error

stw:stop the word java进行gc时会停止用户线程,对用户体验很不好

反证法,如果没有stw,用户线程将一直执行。
gc要进行垃圾回收,就要找所有垃圾和非垃圾,先找gc root,然后找引用对象。因为程序会在gc的过程中一直在执行,然后结束了,结束之后内存空间都被释放了,栈帧弹出,gc root已经没了,之前找的非垃圾已经全变成垃圾了。

网站地图