链接一下目录方便查找
菜鸡的2021春招实习之旅(面经+自己总结的笔记)
背烂了
简单来说就是内存汇中已经不再使用到的空间就是垃圾
使用标记算法:
标记算法有两种,一种引用计数器算法,另一种是可达性分析算法
引用计数器算法就是如果A对象被引用,则给计数器加1,如果引用接触则减去1,下次垃圾回收的时候如果计数器为0则表示为垃圾,但是存在严重的问题,就是如果存在循环引用,会引发内存泄露
可达性分析,就是使用GCRoot作为起点,用引用链能够找到的对象就是可达的,如果不可达就是不可用
使用可达性分析,通过GCRoot为起点,如果一个对象到GC Root没有任何引用链相连,说明此对象不可用
1.eden区满了,触发young gc,触发复制算法,将存活的对象放到s1区;如果eden区再次满了,再次触发young gc,将s1和eden区存活对象放到s2区;
如果超过15次阈值还是长期存活就直接转移到老年代;或者s1区过小,eden区转移不到s区,直接放入老年代 ;或者当前文件过大,直接就转移到老年代
2.老年代垃圾回收
对老年代而言,垃圾没那么多,以前是使用标记-清理算法,但是会有内存碎片,没有连续可用的空间;后续使用标记-整理算法
https://blog.csdn.net/qq_41522089/article/details/107801105
在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(无法保证马上触发GC)。
垃圾回收是自动进行的,无序手动
内存溢出和内存泄露
内存溢出就是内存在GC完毕的情况下或者有大文件GC知道自己无法进行清理,就会存在内存溢出
内存泄露就是本来不应该可达的对象,竟然可达了,就是还有引用在上面.说明存在泄露,比如ThreadLocal里面的变量就是一个非常典型的例子
栈满会抛出该错误。无限递归就会导致StackOverflowError,是java.lang.Throwable
→java.lang.Error
→java.lang.VirtualMachineError
下的错误。
堆满会抛出该错误。
这个错误是指:GC的时候会有“Stop the World",STW越小越好,正常情况是GC只会占到很少一部分时间。但是如果用超过98%的时间来做GC,而且收效甚微,就会被JVM叫停。下例中,执行了多次Full GC
,但是内存回收很少,最后抛出了OOM:GC overhead limit exceeded
错误。详见GCOverheadDemo。
[GC (Allocation Failure) [PSYoungGen: 2048K->496K(2560K)] 2048K->960K(9728K), 0.0036555 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2544K->489K(2560K)] 3008K->2689K(9728K), 0.0060306 secs] [Times: user=0.08 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 2537K->512K(2560K)] 4737K->4565K(9728K), 0.0050620 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2560K->496K(2560K)] 6613K->6638K(9728K), 0.0064025 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 2048K->860K(2560K)] [ParOldGen: 6264K->7008K(7168K)] 8312K->7869K(9728K), [Metaspace: 3223K->3223K(1056768K)], 0.1674947 secs] [Times: user=0.63 sys=0.00, real=0.17 secs]
[Full GC (Ergonomics) [PSYoungGen: 2048K->2006K(2560K)] [ParOldGen: 7008K->7008K(7168K)] 9056K->9015K(9728K), [Metaspace: 3224K->3224K(1056768K)], 0.1048666 secs] [Times: user=0.45 sys=0.00, real=0.10 secs]
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7082K->7082K(7168K)] 9130K->9130K(9728K), [Metaspace: 3313K->3313K(1056768K)], 0.0742516 secs] [Times: user=0.28 sys=0.00, real=0.07 secs]
·······
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7084K->7084K(7168K)] 9132K->9132K(9728K), [Metaspace: 3313K->3313K(1056768K)], 0.0738461 secs] [Times: user=0.36 sys=0.02, real=0.07 secs]
Exception in thread "main" [Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7119K->647K(7168K)] 9167K->647K(9728K), [Metaspace: 3360K->3360K(1056768K)], 0.0129597 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:401)
at java.lang.String.valueOf(String.java:3099)
at jvm.GCOverheadDemo.main(GCOverheadDemo.java:12)
在写NIO
程序的时候,会用到ByteBuffer
来读取和存入数据。与Java堆的数据不一样,ByteBuffer
使用native
方法,直接在堆外分配内存。当堆外内存(也即本地物理内存)不够时,就会抛出这个异常。
在高并发应用场景时,如果创建超过了系统默认的最大线程数,就会抛出该异常。Linux单个进程默认不能超过1024个线程。解决方法要么降低程序线程数,要么修改系统最大线程数vim /etc/security/limits.d/90-nproc.conf
。
使用静态类,将元空间装满了,元空间满了就会抛出这个异常。
Stop-the-World,简称STW,指的是GC事件发生过程中,会产生应用程序(对应进程)的停顿。
可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿.
1.STW事件和采用哪款GC无关,所有的GC都有这个事件。
2.哪怕是G1也不能完全避免Stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
3.STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
4.开发中尽量不要采用System.gc();会导致Stop-the-world的发生。
强引用:只要有引用,就不回收,一般是new
软引用:在内存足够的情况下不回收,如果内存不够就进行回收,如果内存还不够就OOM
弱引用:只要垃圾回收器一运作就回收
虚引用:形同虚设,get方法为null,一般机制被引用队列锁使用
public class PhantomReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference phantomReference = new PhantomReference(o1, referenceQueue);
System.out.println(o1);
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());
System.out.println("===========");
o1 = null;
System.gc();
Thread.sleep(500);
System.out.println(o1);
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());//GC后将对象放到引用队列中了
}
}
详解可以见我的博客[https://blog.csdn.net/qq_41522089/article/details/107850681](
详解可见https://blog.csdn.net/qq_41522089/article/details/107871181
Java 8可以将垃圾收集器分为四类。
为单线程环境设计且只使用一个线程进行GC,会暂停所有用户线程,不适用于服务器。就像去餐厅吃饭,只有一个清洁工在打扫。
使用多个线程并行地进行GC,会暂停所有用户线程,适用于科学计算、大数据后台,交互性不敏感的场合。多个清洁工同时在打扫。
用户线程和GC线程同时执行(不一定是并行,交替执行),GC时不需要停顿用户线程,互联网公司多用,适用对响应时间有要求的场合。清洁工打扫的时候,也可以就餐。
对内存的划分与前面3种很大不同,将堆内存分割成不同的区域,然后并发地进行垃圾回收。
CMS与serial old GC是备选,就是如果无法用原有搭配就使用备选搭配
红线在JDK8以后弃用,9直接remove
绿线在jdk14弃用
CMS在JDK14删除
年代最久远,是Client VM
模式下的默认新生代收集器,使用复制算法。优点:单个线程收集,没有线程切换开销,拥有最高的单线程GC效率。缺点:收集的时候会暂停用户线程。
使用-XX:+UseSerialGC
可以显式开启,开启后默认使用Serial
+SerialOld
的组合。
也就是Serial
的多线程版本,GC的时候不再是一个线程,而是多个,是Server VM
模式下的默认新生代收集器,采用复制算法。
使用-XX:+UseParNewGC
可以显式开启,开启后默认使用ParNew
+SerialOld
的组合。但是由于SerialOld
已经过时,所以建议配合CMS
使用。
ParNew
收集器仅在新生代使用多线程收集,老年代默认是SerialOld
,所以是单线程收集。而Parallel Scavenge
在新、老两代都采用多线程收集。Parallel Scavenge
还有一个特点就是吞吐量优先收集器,可以通过自适应调节,保证最大吞吐量。采用复制算法。
使用-XX:+UseParallelGC
可以开启, 同时也会使用ParallelOld
收集老年代。其它参数,比如-XX:ParallelGCThreads=N
可以选择N个线程进行GC,-XX:+UseAdaptiveSizePolicy
使用自适应调节策略。
Serial
的老年代版本,采用标整算法。JDK1.5之前跟Parallel Scavenge
配合使用,现在已经不了,作为CMS
的后备收集器。
Parallel
的老年代版本,JDK1.6之前,新生代用Parallel
而老年代用SerialOld
,只能保证新生代的吞吐量。JDK1.8后,老年代改用ParallelOld
。
使用-XX:+UseParallelOldGC
可以开启, 同时也会使用Parallel
收集新生代。
并发标记清除收集器,是一种以获得最短GC停顿为目标的收集器。适用在互联网或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望停顿时间最短。是G1
收集器出来之前的首选收集器。使用标清算法。在GC的时候,会与用户线程并发执行,不会停顿用户线程。但是在标记的时候,仍然会STW。
使用-XX:+UseConcMarkSweepGC
开启。开启过后,新生代默认使用ParNew
,同时老年代使用SerialOld
作为备用。
优点:停顿时间少,响应速度快,用户体验好。
缺点:
组合的选择
G1
收集器与之前垃圾收集器的一个显著区别就是——之前收集器都有三个区域,新、老两代和元空间。而G1收集器只有G1区和元空间。而G1区,不像之前的收集器,分为新、老两代,而是一个一个Region,每个Region既可能包含新生代,也可能包含老年代。
G1
收集器既可以提高吞吐量,又可以减少GC时间。最重要的是STW可控,增加了预测机制,让用户指定停顿时间。
使用-XX:+UseG1GC
开启,还有-XX:G1HeapRegionSize=n
、-XX:MaxGCPauseMillis=n
等参数可调。
G1
整体上看是标整算法,在局部看又是复制算法,不会产生内存碎片。G1
收集器会尽量满足。与CMS
类似。
标配参数
比如-version
、-help
、-showversion
等,几乎不会改变。
X参数
用得不多,比如-Xint
,解释执行模式;-Xcomp
,编译模式;-Xmixed
,开启混合模式(默认)。
XX参数
重要,用于JVM调优。
查询
jps -l 查询正在运行的Java程序
jinfo -flag 属性名 进程号
布尔类型
公式:-XX:+某个属性
、-XX:-某个属性
,开启或关闭某个功能。比如-XX:+PrintGCDetails
,开启GC详细信息。
KV键值类型
公式:-XX:属性key=值value
。比如-XX:Metaspace=128m
、-XX:MaxTenuringThreshold=15
。
-xms/-xmx参数
-Xms
和-Xmx
十分常见,用于设置初始堆大小和最大堆大小。第一眼看上去,既不像X参数,也不像XX参数。实际上-Xms
等价于-XX:InitialHeapSize
,-Xmx
等价于-XX:MaxHeapSize
。所以-Xms
和-Xmx
属于XX参数。
查看默认初始值:java -XX:+PrintFlagsInitial
查看修改后参数
-XX:+PrintFlagsFinal
= 是默认参数,:=是修改后的参数
可以用-XX:+PrintCommandLineFlags
查看常用参数。
-Xmx/-Xms
最大和初始堆大小。最大默认为物理内存的1/4,初始默认为物理内存的1/64。
-Xss
等价于-XX:ThresholdStackSize
。用于设置单个栈的大小,系统默认值是0,不代表栈大小为0。而是使用默认值,根据操作系统的不同,有不同的值。比如64位的Linux系统是1024K,而Windows系统依赖于虚拟内存。
-Xmn
新生代大小,一般不调。
-XX:MetaspaceSize
设置元空间大小。
-XX:+PrintGCDetails
输出GC收集信息,包含GC
和Full GC
信息。
-XX:SurvivorRatio
新生代中,Eden
区和两个Survivor
区的比例,默认是8:1:1
。通过-XX:SurvivorRatio=4
改成4:1:1
-XX:NewRatio
老生代和新年代的比列,默认是2,即老年代占2,新生代占1。如果改成-XX:NewRatio=4
,则老年代占4,新生代占1。
-XX:MaxTenuringThreshold
新生代设置进入老年代的时间,默认是新生代逃过15次GC后,进入老年代。如果改成0,那么对象不会在新生代分配,直接进入老年代。
https://blog.csdn.net/qq_41522089/article/details/107850681)
-XX: +PrintGC 输出Gc日志。类似: -verbose: gc
只会显示总的GC堆的变化
-XX: +PrintGCDetails 输出GC的详细日志
详细显示GC使用情况,收集多少垃圾,内存变化,花费时间等
-XX: +PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
显示启动以来花费的时间
-XX: +PrintGCDateStamps输出GC的时间戳(以日期的形式 )
以yyyy-MM-dd HH:mm:ss的形式输出启动以来的时间
-XX: +PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:. . /logs/gc. log日志文件的输出路径
实际工作中,如何结合spr ingboot进行调优?
JVMGC -->>调优–>> spr ingboot微服务的生产部
署和调参优化
1 IDEA开发完微服务工程
2 maven进行clean package
3要求微服务启动的时候,同时配置我们的JVM/GC的调优参数
3.1内
3.2外===>重点
4公式
java -server jvm的各种参数 -jar 第一步上面的jar/war包名字
https://blog.csdn.net/qq_41522089/article/details/107596640
一、堆栈空间分配区别:
1、栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
2、堆(操作系统): 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
二、堆栈缓存方式区别:
1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
三、堆栈数据结构区别:
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。
线程上下文加载器的重要性:
SPI (Service Provider Interface)
父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的classloader加载的类。
这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的CLassLoader加载的类的情况,即改变了
双亲委托模型。
线程上下文加载器就是当前线程的Current ClassLoader
在双亲委托模型下,类加载器由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是java
核心库所提供的,而java核心库是由启动类加载器来加载的,而这些接口的实现来自于不同的jar包(厂商提供),java
的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求,而通过给当前线程设置上下文加载器
就可以设置上下文类加载器来实现对于接口实现类的加载。
如果没有通过与setContextClassLoader(ClassLoader classloader)进行设置的话,线程将继承其父线程的上下文类加载器。
Java应用运行时的初始线程的上下文加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源
Java new一个对象有以下几步:
1.首先查看常量池,看看有没有该类的符号引用,如果没有符号引用的话就会触发创建对象,总体又分为这个类加载过程和对象内存分配的过程
2.首先是触发类加载机制,加载链接初始化;将字节码文件使用类加载器进行加载,然后链接分为验证,准备和解析,验证就是验证字节码文件的安全性,准备就是将对应的类成员变量进行赋默认值,常量直接赋值,解析就是将符号引用转换成直接引用,经过初始化过程,就是调用方法进行初始化,为静态变量赋值,执行静态代码块
3.之后就是进行对象内存分配;如果已经有过符号引用可以省去第二步类加载过程.首先在堆中分配一块内存,如果说是连续空间的话,就是用指针碰撞在空闲的位置开辟一块空间;如果是空闲列表,就是找一块足够大的空间存储内存.然后设置对象头;在之后是调用对象的初始化方法;最后在栈中引用新对象
如果经过逃逸分析以后,对象不发生逃逸,那么可以分配到栈上,如果不可以,尝试使用TLAB进行分配,再不行就进行堆内存,指针碰撞和空闲列表的方式,如果eden内存不够则分给老年代