一,多线程实现的四种方式
1. 实现Runnable接口
普通实现:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行中...");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
Lambda表达式:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("线程执行中..."));
thread.start();
}
}
2. 实现Callable接口
普通实现方式:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程执行中...";
}
}
public class Main {
public static void main(String[] args) {
Callable<String> callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
Lambda实现:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) {
Callable<String> callable = () -> "线程执行中...";
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
3. 继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行中...");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
}
}
4. 使用Executors工具类创建线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行中...");
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(new MyRunnable());
executorService.shutdown();
}
}
二,JVM内存结构模型
java虚拟机在jdk8改变了许多,网络上各种解释都有,在查阅了官方文档以及一下大佬的解释以后,我来粗浅的介绍一下我理解的java8的内存结构。
java8内存结构图
虚拟机内存与本地内存的区别
Java虚拟机在执行的时候会把管理的内存分配成不同的区域,这些区域被称为虚拟机内存,同时,对于虚拟机没有直接管理的物理内存,也有一定的利用,这些被利用却不在虚拟机内存数据区的内存,我们称它为本地内存,这两种内存有一定的区别:
- JVM内存受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报OOM
- 本地内存本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制虽然不受参数的限制,但是如果内存的占用超出物理内存的大小,同样也会报OOM
java运行时数据区域
java虚拟机在执行过程中会将所管理的内存划分为不同的区域,有的随着线程产生和消失,有的随着java进程产生和消失,根据《Java虚拟机规范》的规定,运行时数据区分为以下一个区域:
程序计数器(Program Counter Register)
程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它来实现跳转、循环、恢复线程等功能。
- 在任何时刻,一个处理器内核只能运行一个线程,多线程是通过线程轮流切换,分配时间来完成的,这就需要有一个标志来记住每个线程执行到了哪里,这里便需要到了程序计数器。
- 所以,程序计数器是线程私有的,每个线程都已自己的程序计数器。
虚拟机栈(JVM Stacks)
虚拟机栈是线程私有的,随线程生灭。虚拟机栈描述的是线程中的方法的内存模型:
- 每个方法被执行的时候,都会在虚拟机栈中同步创建一个栈帧(stack frame)
- 每个栈帧的包含如下的内容局部变量表局部变量表中存储着方法里的java基本数据类型(byte/boolean/char/int/long/double/float/short)以及对象的引用(注:这里的基本数据类型指的是方法内的局部变量)操作数栈动态连接方法返回地址
- 方法被执行时入栈,执行完后出栈
虚拟机栈可能会抛出两种异常:
- 如果线程请求的栈深度大于虚拟机所规定的栈深度,则会抛出StackOverFlowError即栈溢出
- 如果虚拟机的栈容量可以动态扩展,那么当虚拟机栈申请不到内存时会抛出OutOfMemoryError即OOM内存溢出
本地方法栈(Native Method Stacks)
本地方法栈与虚拟机栈的作用是相似的,都会抛出OutOfMemoryError和StackOverFlowError,都是线程私有的,主要的区别在于:
- 虚拟机栈执行的是java方法
- 本地方法栈执行的是native方法(什么是Native方法?)
Java堆(Java Heap)
java堆是JVM内存中最大的一块,由所有线程共享,是由垃圾收集器管理的内存区域,主要存放对象实例,当然由于java虚拟机的发展,堆中也多了许多东西,现在主要有:
- 对象实例类初始化生成的对象基本数据类型的数组也是对象实例
- 字符串常量池字符串常量池原本存放于方法区,jdk7开始放置于堆中。字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
- 静态变量静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
- 线程分配缓冲区(Thread Local Allocation Buffer)线程私有,但是不影响java堆的共性增加线程分配缓冲区是为了提升对象分配时的效率
java堆既可以是固定大小的,也可以是可扩展的(通过参数-Xmx和-Xms设定),如果堆无法扩展或者无法分配内存时也会报OOM
方法区(Method Area)
方法区绝对是网上所有关于java内存结构文章争论的焦点,因为方法区的实现在java8做了一次大革新,现在我们来讨论一下:
方法区是所有线程共享的内存,在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中,方法区与其他区域不同的地方在于,方法区在编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:
- 类元信息(Klass)类元信息在类编译期间放入方法区,里面放置了类的基本信息,包括类的版本、字段、方法、接口以及常量池表(Constant Pool Table)常量池表(Constant Pool Table)存储了类在编译期间生成的字面量、符号引用(什么是字面量?什么是符号引用?),这些信息在类加载完后会被解析到运行时常量池中
- 运行时常量池(Runtime Constant Pool)运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些运行时常量池具备动态性,可以添加数据,比较多的使用就是String类的intern()方法
直接内存
直接内存位于本地内存,不属于JVM内存,但是也会在物理内存耗尽的时候报OOM,所以也讲一下。
在jdk1.4中加入了NIO(New Input/Putput)类,引入了一种基于通道(channel)与缓冲区(buffer)的新IO方式,它可以使用native函数直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样可以在一些场景下大大提高IO性能,避免了在java堆和native堆来回复制数据。
常见问题
什么是Native方法?
由于java是一门高级语言,离硬件底层比较远,有时候无法操作底层的资源,于是,java添加了native关键字,被native关键字修饰的方法可以用其他语言重写,这样,我们就可以写一个本地方法,然后用C语言重写,这样来操作底层资源。当然,使用了native方法会导致系统的可移植性不高,这是需要注意的。
成员变量、局部变量、类变量分别存储在内存的什么地方?
- 类变量类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁在java8之前把静态变量存放于方法区,在java8时存放在堆中
- 成员变量成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中
- 局部变量局部变量是定义在类的方法中的变量在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中
由final修饰的常量存放在哪里?
final关键字并不影响在内存中的位置,具体位置请参考上一问题。
类常量池、运行时常量池、字符串常量池有什么关系?有什么区别?
类常量池与运行时常量池都存储在方法区,而字符串常量池在jdk7时就已经从方法区迁移到了java堆中。
在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符,在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池;对于文本字符来说,它们会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池;字符串常量池存储的是字符串对象的引用,而不是字符串本身。
什么是字面量?什么是符号引用?
- 字面量java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:int a=1;//这个1便是字面量 String b="iloveu";//iloveu便是字面量 12
- 符号引用由于在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,所以如果你在一个类中引用了另一个类,那么你完全无法知道他的内存地址,那怎么办,我们只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取他的内存地址。例子:我在com.demo.Solution类中引用了com.test.Quest,那么我会把com.test.Quest作为符号引用存到类常量池,等类加载完后,拿着这个引用去方法区找这个类的内存地址。
三,JVM两种垃圾回收方式
1. 判断对象是否为垃圾的两个算法:
1、引用计数算法
该算法通过维护一个对象的引用计数,当对象被引用时计数加一,当引用失效时计数减一。当引用计数为0时,对象被判断为垃圾对象。但是该算法存在循环引用的问题,即两个对象相互引用,导致引用计数永远不为0,无法被回收。
2、可达性分析算法
该算法通过从根对象(GC Root)(如虚拟机栈、本地方法栈、方法区中的静态变量等)出发,逐个遍历对象引用链,将所有能被访问到的对象标记为存活对象,未被访问到的对象则被判断为垃圾对象。最终将未标记的对象进行回收。该算法能够解决循环引用的问题,是目前主流的垃圾回收算法。
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
著作权归Guide所有 原文链接:https://javaguide.cn/java/jvm/memory-area.html#%E5%A0%86
Minor GC(年轻代GC)和Full GC(老年代GC)是Java虚拟机中的垃圾收集器执行的两种不同类型的垃圾回收操作。
2. Minor GC(年轻代GC):
它是指对年轻代(包括Eden区和Survivor区)进行垃圾回收的过程。在Minor GC中,只有年轻代区域会被扫描和回收,而老年代不会受到影响。Minor GC通常会伴随着短暂的停顿时间(性能影响小),用于回收年轻代的垃圾对象。Minor GC的频率比较高,一般在新生代空间不足时触发。
3. Full GC(老年代GC):
它是指对整个堆内存(包括年轻代和老年代)进行垃圾回收的过程。在Full GC中,会同时对年轻代和老年代进行扫描和回收。Full GC通常会伴随着较长的停顿时间(性能影响大),因为需要扫描整个堆内存。Full GC的频率相对较低,一般在老年代空间不足、永久代空间不足、系统空闲时或者调用`System.gc()`方法时触发。
4. 使用场景:
- - Minor GC:适用于应用程序中大量创建和销毁对象的情况,例如短期的请求处理、临时对象的创建等。Minor GC的目标是尽快回收年轻代的垃圾对象,以保证年轻代的可用空间。
- - Full GC:适用于应用程序中长时间运行的对象、大对象、永久代的垃圾回收等。Full GC的目标是回收整个堆内存的垃圾对象,以释放更多的可用空间。
需要注意的是,Full GC的执行会导致较长的停顿时间,可能会对应用程序的性能产生较大的影响,因此在设计和调优应用程序时需要避免Full GC的频繁触发。
四,垃圾收集算法
1,标记-清除算法
适用场景:都行
特点:
- 效率问题:标记和清除两个过程效率都不高。
- 空间问题:标记清除后会产生大量不连续的内存碎片。
2,复制算法
使用场景:新生代内存区收集车(存活对象数量少)
特点:
- 可用内存变小:可用内存缩小为原来的一半。
- 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。
3,标记-整理算法
适用场景:老年代内存区(存活对象数量多)
特点:由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。