JVM
阿里二面试题:JVM 方法区和元空间什么关系?6大角度带你了解方法区
方法区和永久代以及元空间有什么关系?
方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。
并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现便成为元空间。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
注意:
Java的堆中 1.8及以后有new出来的对象实例,静态变量、字符串常量池
而1.6及以前静态变量和字符串常量池都是在方法去中(永久带中)。
JVM内存结构、内存模型、对象模型的区别
- JVM内存结构和Java虚拟机运行时区域有关。
- Java内存模型和Java并发编程有关。
- Java对象模型和Java对象在虚拟机中表现形式有关。
JMM定义了一套在多线程读写共享数据时(成员变量,数组)时,对数据的可见性、有序性、和原子性的规则和保障
Jvm知识
里面有GC、调优等等
GC
一道关于GC的题目
jdk创建对象的速度远大于cglib,这是由于cglib创建对象时需要操作字节码。cglib执行速度略大于jdk,所以比较适合单例模式。另外由于CGLIB的大部分类是直接对Java字节码进行操作,这样生成的类会在Java的永久堆中。如果动态代理操作过多,容易造成永久堆满,触发OutOfMemory异常。spring默认使用jdk动态代理,如果类没有接口,则使用cglib。
选C
CGLIB是一个强大的、高性能的代码生成库。
CGLIB代理主要通过对字节码的操作,为对象引入间接级别,以控制对象的访问。我们知道Java中有一个动态代理也是做这个事情的,那我们为什么不直接使用Java动态代理,而要使用CGLIB呢?答案是CGLIB相比于JDK动态代理更加强大,JDK动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。
CGLIB和Java动态代理的区别
- Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类;
- Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效
GC种类,GC的STW

【大厂突击】六、Minor GC、Young GC、Full GC、Old GC、Major GC、Mixed GC 一文搞懂

面试官:并发标记时如何标记垃圾 & 垃圾回收之三色标记法详解
并发标记,适用于CMS和G1,并发标记的意思就是 **
可以在不暂停用户线程的情况下对其进行标记
**,那么实现这种并发标记的算法就是三色标记法,三色标记法最大的特点就是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个GC。
空间碎片问题
由于CMS使用的是标记-清理算法,因此会导致产生大量的内存碎片,如果不整理的话,将会有大量的不连续的内存空间存在,因此就无法存放大对象,导致频繁GC。
解决方案:
CMS存在一个默认的参数 “-XX:+UseCMSCompactAtFullCollection”,意思是在Full GC之后再次STW,停止工作线程,整理内存空间,将存活的对象移到一边。
还有一个参数是“-XX:+CMSFullGCsBeforeCompaction”,表示在进行多少次Full GC之后进行内存碎片整理,默认为0,即每次Full GC之后都进行内存碎片整理。
D,execute可以抛出异常,
初始化
时机
注意:访问该类的静态变量或者静态方法的时候会初始化的
构造器也是 static 方法,尽管没有显式写出 static 关键字。因此可以更准确地说,类是在其任何 static 成员被访问时被加载。
初始化顺序(在类加载阶段,第二行不一定对)
因为子类初始化的话,父类如果没有初始化,也会被初始化。
所以,首先①父类static -> ②子类static -> ③父类非static -> ④子类非static (这个不一定正确)
但是子类访问父类的静态变量,只会触发父类的初始化。
下面这个一定正确
静态代码块和静态属性直接初始化的执行顺序?静态方法什么时候执行?_阿里面试题——Java对象初始化…
核心理念:
- 静态属性和静态代码块都是在类加载的时候初始化和执行,两者的优先级别是一致的,且高于非静态成员,执行按照编码顺序。
- 非静态属性和匿名构造器(就是变量后面加个括号的)一定在所有的构造方法之前执行,两者的优先级别一致,执行按照编码顺序。
- 以上执行完毕后执行构造方法中的代码。
1 | public class InitializeDemo { |
注意,
非常深刻,若1中顺序下来的时候,静态属性创建了对象实例需要调用构造方法进行构造的;由于核心理念2的作用,所有的非静态属性和匿名构造器会在前面先顺序执行一遍,(此时前面的静态变量和块,没有被运行到的就先都是默认值了(final不知道))。
有关i++的问题
首先如果不是静态变量的话,i++直接再slot中执行
所以只有++i + a
1 | iinc 1,1 |
++i的话正好相反。
而static 的i++的话,是四句话
1 | getstatic i |
注意,在下面的句子中,i最后均为0(不论是否static);注意,如果都是++i的话,那就是1了。
1 | i = 0; |
两个线程分别对int i=0进行i++一百次
注意上面的这是考虑主存和工作内存的原因,2~200
如果一个线程++,另一个–,那么就-100~100之间呗。当然我看的JVM课程是从字节码角度分析的。
Runnable 与 Callable的不同点
Runnable没有返回值;Callable可以返回执行结果
Callable接口的call()允许抛出异常;Runnable的run()不能抛出
1 |
|
同步容器、并发容器、免锁容器
同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调 用同步容器的方法,它们将会串行执行。比如 Vector, Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码, 可以看到这些容器实现线程安全的方式就 是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。
并发容器:使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁(当然jDK1.8之后使用的是Node数组+链表+红黑树,而且对元素的头+synchnorize或者该数组中没有的时候用CAS写入),在 这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发 地修改 map,所以它可以在并发环境下实现更高的吞吐量。
免锁容器:CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时, 不会抛出 ConcurrentModificationException。在 CopyOnWriteArrayList 中,写入将导致 创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可 以安全地执行。
免锁容器缺点
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可 能导致 young gc 或者 full gc;
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后, 读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足 实时性要求;
CopyOnWriteArrayList 透露的思想
1、读写分离,读和写分开
2、最终一致性
3、使用另外开辟空间的思路,来解决并发冲突
Java:ReentrantLock可重入锁的深入学习
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
一些垃圾回收器
对象都是在堆上分配的吗?
不一定,满足特定条件时,它们可以在(虚拟机)栈上分配内存。
JVM 通过逃逸分析,分析出新对象的使用范围,就可能将对象在栈上进行分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再将对象分配到堆空间,可以有效地减少 JVM 垃圾回收的压力。
java中的基本数据类型一定存储在栈中吗?
基本类型中只有局部变量在栈上,其余在堆上。
局部变量表
(也被称为局部变量数组
或者本地变量表
):定义一个数组
,主要用于存储方法参数
和定义在方法体内的局部变量
,
存放的数据类型
这些数据类型包含各种
基本数据类型
对象引用类型
返回地址(Return Address)类型
线程安全问题
由于局部变量表是建立在线程上的
,是线程私有
的数据,因此不存在数据安全问题
。
注意一个问题(三角)
之前在JVM学习过程中,讨论了在一个局部变量中int a = i++ + ++i;
这类问题,这类问题不是原子性问题的讨论,因为这里面我的i士局部变量,哪里来的原子性问题呢?这里面要分析的只是iinc和iload两个哪个先哪个后的问题。
而如果i是个全局变量,不论静态非静态(三角),他们的++都是三步,①放入、②拿出常量1、③增加、④拿出
然后如果这里面自增自检后该行还有运算的话,是还需要一个放入的,然后++在前还是在后,就是这个放入在上面的①前面,还是在放入的④后面。
CAS存在的三大问题
ABA问题
原因:原子操作只能对结果进行判断,不能对过程进行判断,比如一个值为A,想更新成C,在更新的过程中经历了A->B->A的过程,这时因为最终的结果还是A因此还是执行了A->C的操作
解决:基本上对结果不太影响,如果需要解决可以加版本号,对版本号也做判断
循环时间长开销大
原因:由于是乐观锁,如果CAS操作时有别的操作对目标进行操作,会等待然后重试,如果有个操作长时间没完成,会不断自旋导致cpu资源不断被占用,会给CPU带来非常大的执行开销。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,
这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
内存模型
Java内存模型,Java Memory Module(JMM)的意思。它定义了一套多线程读写共享数据(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。
happens-before
happens-before规定了哪些写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结。
实现死锁的两种方式以及ReentrantLock的额外功能(未完待续)
深度好文:JVM调优
对于调优,首先还是要进行架构调优、代码调优
1 | lsof -i tcp:8082 # 查看端口的进程 |
1 黑马程序员,栈内存溢出(java.lang.StackOverflowError)
栈:线程运行需要的内存空间。
栈桢:每个方法运行需要的内存。
栈不需要垃圾回收的,因为栈是一次次方法调用产生栈桢,用完弹出
栈内存肯定也不是越大越好,栈内存越大,(物理内存一定,然后每一个线程有一个栈,栈越大,那么同时运行的线程数目越少,递归可以更深一点)
1 | 栈桢过多导致栈内存溢出 |
比如第三方,一个类中引用另外一个类,另一个类中引用该类,那么json解析的时候,也会出现无限递归,可以给其中一个类的那个属性设置成@JsonIgnore
2 黑马程序员,堆内存溢出(java.OutOfMemoryError: Java heap space)
1 | // 黑马举的例子, |
黑马程序员、堆内存诊断
1 | 比如上面的代码跑出outofmemory,那么使用下面的一些调试方法 |
案例
1 | 这个进程在垃圾回收之后,内存占用还是很高 |
3 黑马程序员、方法区内存溢出(java.OutOfMemoryError: Metaspace/PermGen space)
1.8以前导致永久代内存溢出
1.8之后导致元空间内存溢出
-XX:MaxMetaspaceSize-8m
1.8以前,永久代。
jdk创建对象的速度远大于cglib,这是由于cglib创建对象时需要操作字节码。cglib执行速度略大于jdk,所以比较适合单例模式。另外由于CGLIB的大部分类是直接对Java字节码进行操作,这样生成的类会在Java的永久堆中。如果动态代理操作过多,容易造成永久堆满,触发OutOfMemory异常。spring默认使用jdk动态代理,如果类没有接口,则使用cglib。
元空间
类加载器加载类的二进制字节码的时候,存在方法区中,1.8以后是在操作系统内存中去了
所以默认没有上限,它的案例中用-XX:MaxMetaspaceSize-8m变小,然后就出现了。
defineClass也只能触发类的加载,不会有链接和初始化。
永久代
还有1.8以前的(1.6)也演示一下
实际场景中
spring、mybatis用到一些字节码技术,比如都会用到cglib这些技术。
spring用它生成一些代理类,这些代理类是Spring中AOP的核心。
mybatis中可以用cglib产生mapper接口的实现类。
运行期间动态生成类的二进制字节码,完成动态的类加载。代理技术中广泛的运用了字节码的动态生成技术。
所以在运行期间,会生成大量的在运行期间生成的类,所以1.8以前很容易导致永久代的溢出。
4 针对线程的诊断
案例1: CPU占用居高不下
1 top命令可以定位到哪一个线程,cpu占用很高
2 ps H -eo pid,tid,%cpu | grep pid (可以查看当前进程内的线程对CPU的占用情况,H线程,macOS下不用H,但是tid出不来)
3 jsack pid 就可以看到某个线程tid的一些日志,这里面的tid是16进制的,需要转换,然后里面可以定位到行
可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号
案例2: 运行很长时间,迟迟没有得到结果,可能死锁
1 top
2 ps H
3 jstack pid
最后会有一些输出,Found one Java-level deadlock,会定位到行号,当然也可以直接用jvisualvm查看死锁的。
感觉上面的案例中,除了要定位到某个线程的行号,jvisualvm用不了,其他的都能用它解决。
GC
G1是JDK9默认,取代了之前的CMS。
调优领域
1 内存
2 锁竞争
3 cpu占用
4 io
低延迟还是高吞吐量,选择合适的回收器(科学运算追求高吞吐量,互联网项目追求响应时间)
CMS,G1,ZZGC
ParallelGC
Zing(一个虚拟机),几乎没有STW的时间
-Xmn // 新生代,越小,minorGC越频繁,很多到了老年代,老年代GC也频繁;越大,老年代的空间紧张,那么再出发GC的时候就可能是FullGC,时间更长。所以新生代一般堆的1/4以上,1/2以下。
老年代调优的时候,先尝试不要调优。即使发生FullGC,也要先看看新生代的调优。
以CMS为例,CMS老年代的内存越大越好,并发的,其他线程也能 并发,但是初始标记和重新标记的时候,会STW。
垃圾回收的同时,其他线程也在运行,浮动垃圾产生了,浮动垃圾产生了又导致内存不足,那么就会造成CMS并发失败,那么就会退化成串行的serialold回收器。
-XX: CMSInitiatingOccupancyFraction=precent,就是说老年代使用了多少就FullGC,一般留下20%左右给浮动垃圾。
案例1:FullGC和MinorGC频繁
新生代小,minorGC频繁,导致晋升对象的晋升阈值降低,老年代存了大量短时间的,所以FullGC频繁。
增大新生代。
案例2:请求高峰时期发生FullGC,单次暂停时间也别长(CMS)
要求低延时
分析:查看GC日志,看看哪个阶段时间比较长。GC日志会有每一阶段耗费的时间,
CMS的初始标记和并发标记时间比较快,重新标记比较慢,因为重新标记扫描整个堆内存,不光老年代而且新生代,新生代对象个数比较多,那么扫描时间就会更大,那么是否可以先进行一次新生代的GC,就是在重新标记之前,再进行一次minorGC,使新生代需要标记的更少。
-XX:+CMSScavengeBeforeRemark
案例3: 老年代充裕的情况下,发生FullGC(CMS jdk1.7)
1.7的时候方法区是永久代,在堆中,永久代如果设置小了,内存不足,就会导致FullGC。
jvisualvm
1 GC日志
GC日志的话,还是需要添加虚拟机参数进行打印的。然后jvisualvm的话,就可以用插件visualGC观看变化(老年代、新生代(E、S1、S0)、方法区(元空间))。
如果为idea,通过如下方式配置打印GC日志:
1 | -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log |
1 | // 查看当前虚拟机下面的GC运行参数 |
2 堆dump
堆dump,用jmap可以
1 | 通过设置如下的JVM参数,可以在发生OutOfMemoryError后获取到一份HPROF二进制Heap Dump文件: |
不过直接用jvisualvm里面选择堆,然后点击堆dump就能查看GCroots个数等,也能查看当前的对象大小排列。也就是查看了当前的GC里面的数据了。
但是查看老年代、新生代、元空间的大小,除了上面的GC日志和VisualGC外,需要使用命令
1 | jmap -heap <pid> # 现实当前的年轻代、老年代等 |
3 死锁
jstack能看
jconsole也能看,jvisualvm也能看,参照下面thread dump最后会有。
4 查看线程
做法1: jps查找进程,ps H -eo pid,tid,%cpu | grep pid,jstack pid 。
做法2: jvisualvm,右键进程,有一个thread dump(里面的线程tab的下面也有),就可以看到里面有关进程的结果,跟jstack pid一样的效果。
一些参数
1 | -Xms | 堆的初始Size | 物理内存的1/64|-Xms128m |
Author: Jcwang
Permalink: http://example.com/2022/07/17/%E5%85%AB%E8%82%A1JVM/