目录
5.JVM线程模型你知道吗?谈一谈(我是懵逼,线程模型是啥?)
1. 服务器CPU占用过高怎么办?
1.问题要点
该问题包含如下两个要点:
如何观察Linux服务器CPU占比;
如何定位到产生问题的Java代码所在线程,判断出当前问题线程到底在执行什么方法。
1 通过top命令找到CPU消耗过高的进程id (即pid)
2.top -Hp pid 显示所有的线程(取消耗过高的线程id)
3.jstack 线程id >aa.txt 输出线程信息到
jstack命令能够打印出当前所有java栈中的线程信息,其中必然包括出问题的线程。剩下我们要做的就是根据线程的id,找到这个线程正在执行的方法即可。这里71289是十进制整数,而jstack日志中的线程id是十六进制,因此需要做以下转换。
然后搜索aa.txt
2.如何排查死锁?使用jstack
死锁产生原因
通过以上示例,我们可以得出结论,要产生死锁需要满足以下 4 个条件:
-
互斥条件:一个资源同一时间能且只能被一个线程访问;
-
不可掠夺:当资源被一个线程占用时,其他线程不可抢夺该资源;
-
请求与等待:当资源被一个线程占用时,其他线程只能等待资源的释放再拥有;
-
循环等待:指的是若干线程形成头尾相接的情况,将所有资源都占用导致的整体死锁或局部死锁。
只有以上 4 个条件同时满足,才会造成死锁问题。
首先通过 jps 得到运行程序的进程 ID,使用方法如下:
“jps -l”可以查询本机所有的 Java 程序,jps(Java Virtual Machine Process Status Tool)是 Java 提供的一个显示当前所有 Java 进程 pid 的命令,适合在 linux/unix/windows 平台上简单察看当前 Java 进程的一些简单情况,“-l”用于输出进程 pid 和运行程序完整路径名(包名和类名)。
有了进程 ID(PID)之后,我们就可以使用“jstack -l PID”来发现死锁问题了,如下图所示:
jstack 用于生成 Java 虚拟机当前时刻的线程快照,“-l”表示长列表(long),打印关于锁的附加信息。
PS:可以使用 jstack -help 查看更多命令使用说明
3.说一下Java 创建对象,你知道那些方式??
创建对象的 6 种方式
创建个女朋友类:
@Data
@NoArgsConstructor
@AllArgsConstructor
class GirlFriend {
private String name;
}
注解使用的是 Lombok 框架注解,方便快速开发,不熟悉的阅读这篇文章:
1.new 一个对象
没对象就 new 一个吧,没错,使用 new 关键字,这也是 Java 创建对象最简单直接的方式了。
示例代码:
/**
* new一个对象
* @author: 栈长
* @from: 公众号Java技术栈
*/
@Test
public void girlFriend1() {
GirlFriend girlFriend = new GirlFriend("new一个对象");
System.out.println(girlFriend);
}
输出结果:
GirlFriend(name=new一个对象)
2.克隆一个对象
朋友有女朋友,你没有,如果可以,把别人的女朋友克隆一个吧?
让女朋友类先实现 Cloneable 接口,并且实现其 clone() 方法:
/**
* 女朋友类
* @author: 栈长
* @from: 公众号Java技术栈
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
class GirlFriend implements Cloneable {
private String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
注意:这里演示默认使用的是浅拷贝,即只克隆基本类型的字段,引用类型的需要再重写 clone() 方法手动赋下引用字段的值。
现在克隆一个对象,示例代码:
@Test
public void girlFriend2() throws CloneNotSupportedException {
GirlFriend girlFriend1 = new GirlFriend("克隆一个对象");
GirlFriend girlFriend2 = (GirlFriend) girlFriend1.clone();
System.out.println(girlFriend2);
}
输出结果:
GirlFriend(name=克隆一个对象)
使用克隆的好处就是可以快速创建一个和原对象值一样的对象,对象的字段值一样,但是两个不同的引用。
3.类派发一个对象(反射)
直接使用女朋友类派发一个吧:
/**
* 类派发一个对象
* @author: 栈长
* @from: 公众号Java技术栈
*/
@Test
public void girlFriend3() throws InstantiationException, IllegalAccessException {
GirlFriend girlFriend = GirlFriend.class.newInstance();
girlFriend.setName("类派发一个对象");
System.out.println(girlFriend);
}
输出结果:
GirlFriend(name=类派发一个对象)
另外,最新最全的 Java 面试题整理好了,微信搜索Java面试库小程序在线刷题。
4.动态加载一个对象(反射)
知道女朋友类在哪里(类全路径),但却没有被加载,那就反射一个对象吧:
/**
* 反射一个对象
* @author: 栈长
* @from: 公众号Java技术栈
*/
@Test
public void girlFriend4() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
GirlFriend girlFriend = (GirlFriend) Class.forName("cn.javastack.test.jdk.core.GirlFriend").newInstance();
girlFriend.setName("反射一个对象");
System.out.println(girlFriend);
}
输出结果:
GirlFriend(name=反射一个对象)
方法5:构造一个对象(反射)
知道女朋友类的构造,就可以调用构造器构造一个对象:
/**
* 构造一个对象
* @author: 栈长
* @from: 公众号Java技术栈
*/
@Test
public void girlFriend5() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
GirlFriend girlFriend = GirlFriend.class.getConstructor().newInstance();
girlFriend.setName("构造一个对象");
System.out.println(girlFriend);
}
输出结果:
GirlFriend(name=构造一个对象)
这里也可以同时结合类全路径构造一个对象。
方法6:反序列化一个对象
这个和克隆的作用类似,假如以前序列化(保存)了一个女朋友在磁盘上,现在就可以反序列化出来。
Java 序列化基础就不介绍了,栈长之前分享不少,我也都整理好了,可以在公众号Java技术栈菜单中阅读。
首先让女朋友可序列化,实现 Serializable 接口:
/**
* 女朋友类
* @author: 栈长
* @from: 公众号Java技术栈
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
class GirlFriend implements Cloneable, Serializable {
private static final long serialVersionUID = 1L;
private String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
序列化/反序列化对象示例代码:
/**
* 反序列化一个对象
* @author: 栈长
* @from: 公众号Java技术栈
*/
@Test
public void girlFriend6() throws IOException, ClassNotFoundException {
GirlFriend girlFriend1 = new GirlFriend("反序列化一个对象");
// 序列化一个女朋友
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("gf.obj"));
objectOutputStream.writeObject(girlFriend1);
objectOutputStream.close();
// 反序列化出来
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("gf.obj"));
GirlFriend girlFriend2 = (GirlFriend) objectInputStream.readObject();
objectInputStream.close();
System.out.println(girlFriend2);
}
输出结果:
GirlFriend(name=反序列化一个对象)
4.聊一下JVM的内存模型
一个图介绍的明明白白,理解着看。
注意:
-
堆和方法区是 线程共享 的
-
虚拟机栈、本地方法栈、程序计数器3个区域是随线程而生,随线程而灭。
4.1方法区(JVM1.8为元数据区)
方法区的作用为:存放虚拟机加载的:类型信息,域(Field)信息,方法(Method)信息,常量,静态变量,即时编译器编译后的代码缓存
值得注意的是,无法申请到内存时,将抛出 OutOfMemoryError
方法区中存在运行时常量池,字面量、符号引用等存放入其中。
在Hotspot的演变过程中:
Java6及之前:方法区存在永久代,保存有静态变量
Java7:进行去永久代工作,虽然还保留着,但静态常量池,如字符串常量池,已经移动到堆中
Java8:移除永久代,类型信息、域(Field)信息、方法(Method)信息存放在元数据区;
字符串常量池、静态变量存放在堆区
4.2 虚拟机栈
虚拟机栈中保存了 每一次 方法调用 的信息。
每个Java线程创建时,都会创建对应的 虚拟机栈 ,每一次方法调用,都会往栈中压入一个 栈帧。如下图:
而栈帧中,包含:
-
局部变量表:保存函数 (即方法) 的局部变量 操作数栈:保存计算过程中的结果,即临时变量 动态链接:指向方法区的运行时常量池。字节码中的 方法调用指令 以常量池中指向方法的 符号引用 为参数。 方法的返回地址
4.3 本地方法栈
和虚拟机栈功能上类似,它管理了native方法的一些执行细节,而虚拟机栈管理的是Java方法的执行细节。
4.4 程序计数器
程序计数器记录线程执行的字节码行号,如果当前线程正在运行native方法则为空。也有称之为 PC寄存器
字节码解释器在工作时,通过改变计数器的值来选取下一跳需要执行的字节码指令,分支 、 循环 、跳转 、 异常处理 、线程恢复 等基本功能都需要依赖计数器来完成。
Java虚拟机的多线程实现方式:通过 轮流切换并分配处理器执行时间 实现
所以,在任意确定的时间点,一个处理器只会处理一个线程中的指令。为了正确地处理 线程切换后的任务恢复 ,每一个线程都具有自身的程序计数器
4.5 堆
堆提供了类实例和数组的内存,可以按如下方式划分:
如下图所示:
划分和对象创建与GC有关
-
新生成的对象在Eden区
-
触发 Minor GC后,还 "幸存" 的对象移动到S0
-
再次触发Minor GC后,S0和Eden 中存活的对象被移动到S1中,S0清空
-
每次移动时,自动递增计数器,超过默认值时 (印象中是16),移动到老年代,如果Eden中没有足够内存分配,也将直接在老年代中分配内存
-
老年代中依靠Major GC
5.JVM线程模型你知道吗?谈一谈(我是懵逼,线程模型是啥?)
5.1 Java线程的实现方式可以有三种:
使用内核线程实现
使用用户线程实现
使用用户线程加轻量级进程混合实现
印象中JVM没有规定线程实现的规范,具体研究需要结合具体的JVM实现,下面我们简单探索一下
5.2 内核线程模型
内核线程模型: 完全依赖操作系统内核提供的内核线程(Kernel-Level Thread ,KLT)来实现多线程。这种方式下:线程的切换调度 由 系统内核 完成。
一般而言,程序不会直接使用内核线程,而是使用一种 高级接口 即 轻量级进程(Light Weight Process,LWP)。
用户进程中,通过 LWP 使用系统的 内核线程 。由于其一对一的关系,又称为 一对一模型
由于 用户线程 与 LWP 一一对应,LWP 是独立的调度单元,因此某个LWP在 用户进程调用过程中 发生阻塞,以及在 系统调用中 发生了阻塞,都不会影响整个进程的执行。
但是LWP依托内核线程,所以 线程操作 需要 依赖系统调用 ,代价是较高的,需要在 用户态(User Mode) 和 内核态(Kernel Mode) 中来回切换;而且每个 LWP 都需要一个 内核线程 进行支持,因此 LWP 要消耗一定的内核资源,因此一个系统仅可支持 少量有限 的 LWP。
5.3 用户线程模型
排除掉 内核线程 ,JVM平台也可以实现 用户线程 User Thread 下文简称 UT ,完全自行实现创建、调度、销毁。
区别于内核线程模型,此时线程的调度不再依赖内核,极少占据内核资源,基本限定在用户态内,所以可以突破量的限制,并且减少线程切换时的损耗。
这样看起来似乎很美好,但难以利用多核CPU的优势,并且一旦产生系统调用发生中断,其他线程也将被中断。
这种 多对一模型 的实用性较低。
5.4 混合模型
又称 多对多模型 ,这种方式充分利用了上面两种方式的优点。
这种模型中,既存在UT,也存在LWP。
创建、切换线程(UT)依旧是廉价的,并且可以拥有大量的线程;同时利用 LWP作为UT到KLT(内核线程)的桥梁, 享受了系统内核的线程调度、CPU映射,免去了自行实现系统调用的部分,进行系统调用时,阻塞整个进程的概率也低于 用户线程模型 。