JVM内存是怎样划分的?
- 方法区:又叫做永久代,主要用来存放jvm加载的类信息,静态变量、常量、JIT编译后的源码存放的区域,线程的共享区域。
- 堆内存:线程的共享区域,垃圾回收的主要场所,主要用来给对象分配内存空间
- jvm栈(栈内存):基本数据类型变量、局部变量,对象的引用边浪存放的区域
- 本地方法栈:为jvm提供本地方法
- 程序计数器:当前线程执行的字节码的位置指示器
可以说一下对象创建过程中的内存分配吗?(重点)
一般情况下我们通过new指令来创建对象,当虚拟机遇到一条new指令的时候,会去检查这个指令的参数是否能在常量池中定位到某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,如果没有,那么会执行类加载过程,就会在堆内存上为对象分配一块内存空间。
对象的内存分配有两种方式,即指针碰撞和空闲列表方式。
-
指针碰撞方式:堆内存是绝对规整的,用过的内存在一边,未使用的内存在另一边,中间有一个指示指针,内存分配就是把那个指针向空闲空间那边挪动一个对象大小。
-
空闲列表方式:堆内存中不是规整的,已使用和未使用的内存相互交错,那么虚拟机就必须维护一个列表用来记录哪块内存是可用的,分配的时候找到一块足够大的空间分配对象实例,并且需要更新列表上的记录。
那么内存分配如何保证线程安全呢?
- 对内存分配的动作进行同步处理,通过“CAS + 失败重试”的方式保证更新指针操作的原子性
内存分配与垃圾回收:(重点掌握)
jvm内存分为堆内存和非堆内存,堆内存又分为年轻代、老年代,年轻代又分为1个eden区、2个Survivor。
JVM堆内存的分配
创建的对象会优先在Eden区分配,如果是大对象(很长的字符串数组)可以直接进入老年代。
- -XX:PretenureSizeThreshold=4m:设置大于这个参数值的对象直接在老年代中分配
- 长期存活在年轻代中的对象即将进入老年代时,每一次minorGC,对象的年龄就大一岁,默认15岁晋升到老年代。
- -XX:MaxTenuringThreshold=3:设置晋升年龄
- -Xms128m:jvm设置堆内存初始大小,默认为物理内存
1/64
- -Xmx512m:jvm设置堆内存最大上限,默认为物理内存的
1/4
- 剩余堆内存不足40%jvm就会增大到-Xmx的最大值,剩余堆内存超出70%jvm就会缩小到-Xms的最小值,所以一般我们设置-Xmx和-Xms相等,避免在GC的时候调整堆大小。
- -Xmn2G:设置年轻代大小为2G
- -XX:SurvivorRatio=3:设置年轻代中eden区与survivor比值,注意survivor有2个,eden:survivor=3:2,eden占整个年轻代是3/5,一个survivor占年轻代1/5
- -XX:NewRatio=3:设置年轻代和老年代的比值,默认是1:2
JVM非堆内存的分配
- -XX:PermSize=128m:jvm设置非堆内存初始化大小,默认为物理内存的
1/64
- -XX:MaxPermSize=512m:jvm设置非堆内存最大上限,默认为物理内存的
1/4
minorGC和fullGC
- Minor GC(年轻代GC):创建的对象优先在eden区分配,当eden区没有足够的空间时,jvm发生一次minorGC,MinorGC非常频繁,而且速度也很快。
- Full GC(老年代GC):当老年代没有足够的空间将发生fullGC。
JVM如何判定一个对象是否应该被回收?(重点掌握)
- 引用计数法:对象有引用,计数+1;对象删除引用,计数-1,垃圾回收时,只需要收集计数为0的对象,该算法缺点就是无法解决循环引用的问题。
- root根搜索法:选择可以作为root根对象的节点作为起始节点,从该节点出发从上向下搜索,从root根节点的到该节点如果没有任何引用链,该对象可以被回收;以下对象会被认为是root对象:
- 栈内存中引用的对象
- 方法区中静态引用和常量引用指向的对象
- 被启动类(bootstrap加载器)加载的类和创建的对象
- Native方法中JNI引用的对象。
JVM垃圾回收算法有哪些?(重点掌握)
hotspot虚拟机采用root搜索法判断对象是否可以被回收,常见的垃圾收集算法有标记清除算法、复制算法、标记整理算法
。
- 标记-清除算法(Mark-Sweep):分为两个阶段,第一阶段从根结点开始标记所有被引用的对象,第二阶段把未标记的对象全部清除,此算法需要暂停整个应用,会产生内存碎片。
- 复制算法:将内存分为大小相等的2片区域,每次只使用其中一个区域,垃圾回收时,遍历使用的区域,把正在使用的对象复制到另外一个区域中,然后把原来的区域一次性清除掉。此算法不会出现内存碎片的问题,但是当使用的对象比较多时,复制操作效率会降低,且只能使用一半内存。
- 标记整理算法:分为2个阶段,第一阶段从根开始标记所有被引用的对象,第二阶段,遍历堆,清除哪些没有标记的对象,并且将正在使用的对象压缩到堆的一侧,按顺序排放,此算法不会产生内存碎片也不会产生,也不会产生空间问题。
JVM中的垃圾收集器有了解吗?(重点掌握CMS收集器)
hotspot虚拟机垃圾收集器有7种:
serial、serial old、parnew、parallel scanvenge、parallel old、cms、g1
- Serial收集器:年轻代版本的单线程收集器,
- serial old收集器:老年代版本的单线程收集器
- parnew收集器:年轻代版本的多线程收集器,常和cms收集器一起使用,需要用户手动开启该收集器,可以使用命令
-XX:+UseParNewGC
使用该收集器。可以使用命令-XX:ParallelGCThreads=3
指定线程数量 - parallel scavenge收集器:年轻代版本的多线程收集器,通过命令
-XX:MaxGCPauseMillis=100
设置最大垃圾收集停顿时间(年轻代每一次垃圾回收的最长时间),可以通过命令-XX:+UseAdaptiveSizePolicy
打开垃圾收集器的自适应调节策略,jvm会动态调整堆中各个区域的大小等(eden、2个survivor)。自适应调节策略是parallel scavenge和parnew的主要区别。 - parallel old收集器:老年代版本的多线程收集器
- g1收集器:
- cms(concurrent mark sweep)收集器:老年代版本的多线程收集器,
以获取最短垃圾收集停顿时间为目标的
,常和parnew收集器一起使用,使用参数-XX:+UseConcMartSweepGC
开启cms垃圾收集器。
cms垃圾收集器的过程
初始标记:jvm会停顿正在执行的任务(STW,stop the world),扫描和root根对象直接关联的对象,速度很快。
并发标记:在初始标记的基础上继续扫描,应用程序线程和标记线程并发执行,所以用户感觉不到停顿。
重新标记:这个阶段jvm会暂停(stw,stop the world),修正并发标记过程中因为应用程序线程继续运行导致的标记产生变动的那一部分对象的标记记录
并发清除:清除垃圾对象,应用程序线程和垃圾收集器线程并发执行。
CMS缺点
- CMS采用的垃圾收集算法是mark-sweep,所以会产生内存碎片,可以使用参数
-XX:+UseCMSCompactAtFullCollection
开启内存整理功能,配合参数-XX:CMSFullGCsBeforeCompaction=10
一起使用,设置执行了多少次不压缩的fullgc后进行一次带压缩的内存整理。 - 需要更多的cpu资源,因为并发标记阶段,标记线程和应用程序线程并发执行,这就需要更多的cpu资源,单纯依赖线程的切换是不靠谱的。
- cms收集的过程中会产生浮动垃圾,它不会在老年代满的时候开始垃圾收集,相反,它会提前收集,通过参数
-XX:CMSInitiatingOccupancyFraction=70
控制内存使用百分比,该值不能设置太高否则运行期间预留的空间不够程序使用会出现concurrent mode failure
,之后会临时使用serial old作为老年代垃圾收集器,会产生更长时间的停顿。
浮动垃圾:应用运行的同时也在进行垃圾回收,所以就存在有些垃圾是在垃圾回收结束时候产生的,这部分垃圾就是浮动垃圾,这些垃圾需要在下次垃圾收集周期被收集,所以cms一般需要预留20%的空间存放浮动垃圾。
STW(stop the world):
JVM启动参数如下:
JAVA_OPTS="-Xms4096m –Xmx4096m -XX:NewRatio=2 -XX:SurvivorRatio=8 -Xloggc:/home/work/log/serviceName/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=10 "
JVM常用内存调优命令(重点掌握)
- top命令:实时监测进程的概要信息,如:多少进程处于运行、休眠、停止状态等;cpu使用率;内存使用率等。
- jstat命令:实时监测jvm的状态包括:内存状态和垃圾回收状态等。
jstat -gcutil 11666 1000 3 #每隔1s打印一次该进程的gc信息,打印3次(输出的是百分比)
jstat -gc 11666 1000 3 #每隔1s打印一次该进程的gc信息,打印3次(输出是实际的数)
jstack命令:查看当前jvm的线程快照,可以定位线程长时间出现卡顿的原因,如:死锁、死循环等
jstack -l 11666 #查看线程锁信息
- jps命令:查看进程的状态信息,如:jvm的启动参数等
jps -q:只输出进程号
jps -m:输出main method的参数
jps -l:输出主类名
jps -v:输出jvm参数
- jinfo命令
jinfo -sysprops 11666 #查看系统配置
jinfo -flag PrintGC 11666 #打印GC日志参数
jinfo -flag +PrintGC 11666 #打开GC日志参数
jinfo -flag -PrintGC 11666 #关闭GC日志参数
- jmap命令
jmap -heap 11666 #查看堆内存信息,gc使用的算法
JDK8中在内存管理上的变化
jdk8中使用元空间代替永久代,它们功能类似,都是对方法区的实现,区别在于元空间不在jvm上,而是在本地内存中,默认受本地内存大小的限制,可以使用参数-XX:MetaSpaceSize=128m指定元空间大小。
为什么使用元空间代替永久代
永久代主要用来存放jvm加载的类信息,而类和方法大小比较难确定,导致永久代大小比较难确定,如果给永久代分配的内存太小就会导致内存溢出问题,因此使用元空间的本地内存代替永久代。
jvm的类加载机制有了解吗?(重点掌握)
jvm把描述为类的信息加载到内存中,最终形成可以被使用的java类型。整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。
- 加载:将字节码文件加载到内存中存放到方法区中,然后在堆内存创建一个对象,用来封装方法区中的数据。
- 验证:验证被加载类是否正确,包括文件格式验证等。
- 准备:为类的静态变量分配内存,并进行初始化,如:
public static int val = 0
- 解析:将类中的符号引用转变为直接引用
- 初始化:为类的静态变量赋予正确的初始值
类加载器的分类
- 启动类加载器(bootstrap classloader):用来加载jdk/jre/lib目录中的类
- 扩展类加载器(extclassloader):用来加载jdk/jre/lib/ext目录中的类
- 应用类加载器(appclassloader):用来加载用户类路径(ClassPath)所指定的类
类加载器的职责
- 全盘负责:当一个类被类加载器加载,类加载器会加载该类所依赖和引用的相关类。
- 父类委托:当一个类被类加载器加载,它会让该类加载器的父类加载器先加载,只有父类加载器无法加载才会使用该加载器加载,可以防止内存中出现多份相同的字节码文件,保证程序的稳定性。
- 缓存机制:类加载器会将加载的类缓存起来以备下次使用