java常问面试题5道,offer来碗里(四)干货满满

目录

1. 服务器CPU占用过高怎么办?

2.如何排查死锁?使用jstack

死锁产生原因

3.说一下Java 创建对象,你知道那些方式??

1.new 一个对象

2.克隆一个对象

3.类派发一个对象(反射)

4.动态加载一个对象(反射)

方法5:构造一个对象(反射)

方法6:反序列化一个对象

4.聊一下JVM的内存模型

4.1方法区(JVM1.8为元数据区)

4.2 虚拟机栈

4.3 本地方法栈

4.4 程序计数器

4.5 堆

 5.JVM线程模型你知道吗?谈一谈(我是懵逼,线程模型是啥?)

5.1 Java线程的实现方式可以有三种:

5.2 内核线程模型

5.3 用户线程模型

5.4 混合模型


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 个条件

  1. 互斥条件:一个资源同一时间能且只能被一个线程访问;

  2. 不可掠夺:当资源被一个线程占用时,其他线程不可抢夺该资源;

  3. 请求与等待:当资源被一个线程占用时,其他线程只能等待资源的释放再拥有;

  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映射,免去了自行实现系统调用的部分,进行系统调用时,阻塞整个进程的概率也低于 用户线程模型 。

郭霖

之前总结的,常问干货:

java常问面试题10道,offer来碗里(一)

java常问面试题10道,offer来碗里(二)

java常问面试题10道,offer来碗里(三)

郭霖

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

only-qi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值