JDK8在2020年使用占比84%, 2023年使用占比32%
JDK17在2022年使用占比0.37%, 2023年使用占比9.07%
JDK11+ 使用G1占比65%
从JDK8到JDK11, G1平均速度提升16%, 从JDK11到JDK17平均速度提升8.66%
JDK17相对于JDK8和JDK11, 所有垃圾回收器的性能都有很明显的提升
public static void main(String[] args) {
/**
* 文本块
*/
String s = "hello" +
" - " +
"world";
System.out.println(s);
String s1 = """
hello
-
wold
""";
System.out.println(s1);
}
比如在java8中, 如果要拼接是很长的字符串或者html格式的文本, 一般情况下, 为了方便阅读都要换行处理, 但是换行后, 每个字符串都要使用"+“拼接.
在jdk17中, 只需要”“” xx “”" 3个引号引起来, 就可以直接换行了, 简单方便了许多.
public static void main(String[] args) {
/**
* 空指针提示优化
*/
testNpe();
}
private static void testNpe() {
Object a = null;
var flag = a.equals("1");
System.out.println(flag);
}
同样的一个的NPE异常, 打印出不同的错误栈信息.
这是java8的错误提示信息
这是java17的错误提示信息
从了jdk17, 再也不用为找不到哪个空而发愁了.
1. jdk8
先来看下jdk8的pojo的使用方式
定义pojo对象
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
定义好成员变量, 同时添加好对应的getter和setter方法, 以及可能需要的toString, equals等方法.
也有的直接使用lombok的@Getter, @Setter或者@Data等注解实现.
使用最原始的方式, 不依赖三方的插件, 只会让我们的代码上看着比较臃肿, 并且都是模板代码.
如果要使用lombok的话, 就必须要依赖三方插件, 关键是你一个人用, 无所谓了, 但是如果是在企业开发, 那就需要所有的小朋友都要安装和使用lombok插件了.
2. jdk17
我们在来看下jdk17如何优化这种问题的(不代表它是最优解, 也有一定的缺陷)
public record User2(String name){
static final int a = 1;
}
public static void main(String[] args) {
/**
* pojo
*/
User user = new User();
user.setName("1");
System.out.println(user.toString());
}
从user2的这个定义上来看, 是不是超级简介
我们从它的字节码上看下它到底是什么逻辑
从上图的字节码中我们可以看到:
从这些点我们也可以看到一个优势, 同时也有一些劣势, 它的场景也很明显, 大家自行斟酌了.
//老的方法
boolean assignableFrom = Record.class.isAssignableFrom(User2.class);
System.out.println(assignableFrom);
//新的方法
boolean record = User2.class.isRecord();
System.out.println(record);
public enum SwitchTest {
ONE,
TWO,
THREE;
}
1. jdk8写法
private static void switchTest(SwitchTest switchTest) {
switch (switchTest) {
case ONE:
System.out.println(1);
break;
case TWO:
System.out.println(2);
break;
default:
System.out.println("default");
}
}
想必大家对这个已经很熟悉了, 这里面我们要写很多的break或者直接return的定义.
2. jdk17写法
private static void switchTest2(SwitchTest switchTest) {
switch (switchTest) {
case ONE -> System.out.println(1);
case TWO, THREE -> System.out.println("2 & 3");
default -> System.out.println("default");
}
}
是不是看着简洁了一下, 这个和其他语言有相似之处, 比如scala, python.
每个case后面以;结尾.
1. jdk8的写法
private static void instanceof1(Object a) {
if (a instanceof String) {
String lowerCase = ((String) a).toLowerCase();
System.out.println(lowerCase);
}
}
就算a是String对象, 在使用的时候我们也必须要先强转为String对象, 才可以后续的使用
2. jdk17
private static void instanceof1(Object a) {
if (a instanceof Integer v) {
double b = v.doubleValue();
}
}
在使用instanceof的时候, 直接定义了变量v, 那么v在后面的运算中, 就可以直接使用了, 不用再强转为Integer类型的变量了.
在jdk8中我们已经熟练使用了接口中的default的使用了, 对于方法, 接口中也就只能定义default和普通的定义方法了, 但是如果我们使用了default, 需要很多的适配或者逻辑处理, 就只能再接口实现类中去实现, 如果是所有实现的功能逻辑, 可能我们还需要抽取一层service, 不是很方便, 但是在jdk17中就给我们提供了可以编写private方法的功能了, 可以便于我们在使用default方法的同时做一些公共的逻辑处理.
public interface UserService {
default String getName() {
return adapterName(1);
}
private String adapterName(int a) {
String name;
switch (a) {
case 1 -> name = "1";
case 2 -> name = "2";
default -> name = "3";
}
return name;
}
}
getName使我们定义的default方法, 如果这个方法对于所有实现类来说是公共的逻辑, 那么我们直接在接口中完成它即可, 不用再放置在抽象类或者每个实现类中了, 是不是很方便?
在Java8的时候,其实就已经支持了类型推断了.
在Java10的时候又引入了另外一个特性,叫局部变量类型推断这个特性。正如这个特性的名称,这个特性只能用于局部变量,且类型是确定的,无二义性的,下面的示例代码中给出了哪些地方不能用局部变量类型推断,也就是var关键词在哪些场景下不允许使用。
public class VarTest {
var name; //成员变量不可使用var
private int age;
public VarTest(var age) { //构造方法或者普通方法的入参不可使用var
this.age = age;
}
public var getAge() { //方法返回值不可使用var
return this.age;
}
public void setAge(var age) {
this.age = age;
}
public void test(int a) {
var b = a + 1; //方法内的变量是可使用var类型推断的
System.out.println(b);
}
public void tryc() { // catch中的参数变量不可使用var
try {
int i = 1 / 0;
} catch (var e) {
}
}
}
从上面列举的示例中我们可以看到, 只有方法内的局部变量才可以使用var类型推断, 并且是类型唯一的才可以.
终于它还是优化了,原来的JDK自带的Http客户端真的非常难用,这也就给了很多像okhttp、restTemplate等第三方库极大的发挥空间,几乎就没有人愿意去用原生的http客户端的。但现在不一样了,感觉像是新时代的API了。FluentAPI风格,处处充满了现代风格,用起来也非常地方便,再也不用去依赖第三方的包了,方便了很多, 这样我就不用再依赖三方api, 就可以实现远程调用了.
1. jdk8
HttpClient httpClient = HttpClient.New(new URL("https://www.baidu.com"));
httpClient.setConnectTimeout(1000);
MessageHeader messageHeader = new MessageHeader();
messageHeader.add("keep-alive", "true");
httpClient.writeRequests(messageHeader, null);
OutputStream outputStream = httpClient.getOutputStream();
byte[] b = new byte[1024];
outputStream.write(b);
System.out.println(new String(b));
估计大部分人看这个代码都不熟悉了, 好多人也没用过, 反正我是没用过.
2. jdk17
Map<String, Object> params = new HashMap<>();
params.put("event_type", "custom-reader");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://edith.xiaohongshu.com/api/sns/web/activity/report_event"))
.timeout(Duration.ofMinutes(1))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(JSON.toJSONString(params), StandardCharsets.UTF_8))
.build();
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(100))
.build();
try {
//这个是同步方法
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
//异步调用请使用client.sendAsync()
String body = response.body();
System.out.println(body);
} catch (Exception e) {
throw new RuntimeException(e);
}
看着是不是特别像restTemplate格式的api, FluentAPI风格看着就是很舒服.
定义request的时候, 可以直接设置url, timeout, header, 请求方式, 以及body参数等等.
3. HttpResponse.BodyHandlers
这里我们延伸一下, 当请求发出后, 一般情况下我们都要获取请求返回的结果, 在我们正常项目开发中, 为了方便操作数据, 我们一般都要转成javabean以供后续使用.
@FunctionalInterface
public interface BodyHandler<T> {
public BodySubscriber<T> apply(ResponseInfo responseInfo);
}
BodyHandler是一个泛型的函数式接口, 下图中返回的所有的内置的ofxx方法, 返回的都是BodyHandler函数.
public static BodySubscriber<String> ofString(Charset charset) {
Objects.requireNonNull(charset);
return new ResponseSubscribers.ByteArraySubscriber<>(
bytes -> new String(bytes, charset)
);
}
这是一个ofString的实现, 看这个是把返回的结果转换成了string对象.
4. 问题来了, 如果我想要直接转成javabean该如何实现呢?
public class MyBodyHandler<R> implements HttpResponse.BodyHandler<R>{
private final Class<R> responseType;
public MyBodyHandler(Class<R> responseType) {
this.responseType = responseType;
}
@Override
public HttpResponse.BodySubscriber<R> apply(HttpResponse.ResponseInfo responseInfo) {
return HttpResponse.BodySubscribers.mapping(
HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8),
json -> JSON.parseObject(json, responseType));
}
}
我们只需要实现HttpResponse.BodyHandler, 在apply里面实现结果集到javabean的转化即可.
MyBodyHandler myBodyHandler = new MyBodyHandler(Result.class);
HttpResponse<Result> response = client.send(request, myBodyHandler);
Result body = response.body();
当定义好自定义的BodyHandler后, 在调用的时候, 只需要把我们自定义的实例对象作为入参传到send方法中即可, 这样response.body()返回的直接就是我们定义好的javabean对象.
5. 并发访问
在正常的业务处理中, 考虑到性能问题, 可能要并发访问多个地址, 请看demo
HttpClient client = HttpClient.newHttpClient();
List<HttpRequest> requests = IntStream.range(1, 5)
.mapToObj(url -> {
Map<String, Object> params = new HashMap<>();
params.put("event_type", "custom-reader");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://edith.xiaohongshu.com/api/sns/web/activity/report_event"))
.timeout(Duration.ofMinutes(1))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(JSON.toJSONString(params), StandardCharsets.UTF_8))
.build();
return request;
})
.toList();
List<CompletableFuture<HttpResponse<String>>> futures = requests.stream()
.map(request -> client.sendAsync(request, HttpResponse.BodyHandlers.ofString()))
.toList();
futures.forEach(e -> e.whenComplete((resp, err) -> {
if (null == err) {
System.out.println(resp.body());
}
}));
CompletableFuture.allOf(futures.toArray(CompletableFuture<?>[]::new)).join();
总之, jdk17新提供的HttpClient, 已经满足了我们的需求了, 还是很好用的.
集合肯定就包含map, list, set
Set<Integer> integers = Set.of(1, 2, 3);
List<Integer> integers1 = List.of(1, 2, 3, 4);
Map<String, Integer> stringIntegerMap = Map.of("1", 2, "3", 4);
这个就是jdk17的实现方式, 如果使用过guava的朋友, 看起来肯定很熟悉, 是的, 你没看错, 它确实是差不多的, 类似于guava的Sets.newHashSet()等等
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(1);
在jdk8中, 我们想要添加值, 只能先创建set对象后, 然后调用add方法才可以使用.
String a = "";
System.out.println(a.isEmpty());//true
System.out.println(a.isBlank());//true
String b = " ";
System.out.println(b.isEmpty());//false
System.out.println(b.isBlank());//true
String c = "c";
System.out.println(c.repeat(3));//ccc
String d = " asdfals sdfjasdjlfwejdsf dfs ";
System.out.println(d.stripLeading());//"asdfals sdfjasdjlfwejdsf dfs "
System.out.println(d.stripTrailing());//" asdfals sdfjasdjlfwejdsf dfs"
System.out.println(d.strip());//asdfals sdfjasdjlfwejdsf dfs
String e = "a\nb\nc\nd\n";
Stream<String> lines = e.lines();
long count = lines.count();
System.out.println(count);//4
String f = "abc";
String indent = f.indent(3);
System.out.println(indent);//" abc"
IntStream intStream = IntStream.range(1, 10)
.takeWhile(i -> i % 2 == 1);
List<Integer> list = intStream.boxed().toList();
//结果是[1]
IntStream intStream2 = IntStream.range(1, 10)
.dropWhile(i -> i % 2 == 1);
List<Integer> list2 = intStream2.boxed().toList();
//结果是[2,3,4,5,6,7,8,9]
/**
* return t == null ? Stream.empty()
*/
Stream<Object> nullStream = Stream.ofNullable(null);
long count = nullStream.count();
//结果是0
String readString = Files.readString(Path.of(Test17.class.getClassLoader().getResource("test.txt").toURI()));
Path path = Path.of(File.createTempFile("temp", ".txt").toURI());
Files.writeString(path, "hello world", Charset.defaultCharset(), StandardOpenOption.WRITE);
Path path1 = Path.of(Test17.class.getClassLoader().getResource("test.txt").toURI());
Path path2 = Path.of(Test17.class.getClassLoader().getResource("test2.txt").toURI());
提供的新的api主要是为了开发者更方便的读写文件内容.
在jdk8中也有类似的方法, 比如Files.readlines(), Files.readAllLines()
还有apache的, FileUtils.readFromFile()等等.
JDK 17推出密封类的原因主要是为了增加对类继承关系的限制,以提供更好的封装性和安全性。
密封类的作用主要有以下几点:
限制类的继承:通过使用密封类,可以确保该类不能被其他类继承,从而提供更好的封装性和安全性。这对于那些不希望外部扩展功能的组件来说非常有用。
防止恶意代码的攻击:由于密封类的继承被限制,因此可以防止恶意代码通过继承来获取或修改类的内部实现,从而增加了代码的安全性。
public sealed class Shape permits Circle, Hexagon{
}
定义了一个Shape的基础类, 加了密封类的标识符sealed.
permits定义可以继承Shape的子类, 有Circle, Hexagon
这个类也可以是抽象类
public non-sealed class Circle extends Shape{
}
public final class Hexagon extends Shape{
}
- 定义了sealed的密封类必须有子类
- 子类可以是final修饰的类, 可以是no-sealed修饰的类(非密封类), 也可以是sealed密封类(必须有子类)
- permits指定的子类必须直接继承该父类
- 接口间的继承也是同样的道理
DateTimeFormatter提供了对时段的支持, 可以通过api查询指定时间是一天的上午还是下午.
这是DateTimeFormatter一些格式化注释说明.
LocalTime localTime = LocalTime.of(10, 0, 0);
//这里只会返回上午 下午
String date1 = DateTimeFormatter.ofPattern("a").format(localTime);
//会返回上午 中午 下午 晚上
String date2 = DateTimeFormatter.ofPattern("B").format(localTime );
//返回一天的小时
String date3 = DateTimeFormatter.ofPattern("k").format(localTime );
之前运行java文件必须要javac编译一下, 才可以运行, jdk17直接使用java xx.java就可以直接运行了, 省去了单文件编译的麻烦.
关于模块的官方说明请戳这里
模块化在包之上提供更高层次的聚合。关键的新语言元素是模块,它是一组专属命名、可重用的相关包、资源(例如图像和 XML 文件)和一个模块描述符,用于指定:
JDK推出的模块系统(Java Platform Module System,JPMS)的作用是将Java平台分解为互相依赖的模块,并提供了一种更加灵活和可控的方式来管理和组织代码。这个模块系统是为了解决Java平台长期以来存在的一些问题,包括:
看下jdk8的rt.jar
看下jdk17的rt.jar
新的jdk已经对jar进行了模块化区分了.
现在我们就要看下如果要模块化管理, 需要怎么做?
建模块也必须遵守下面的规则:
- 模块名称必须是唯一的。
- 模块描述符文件module-info.java 必须有。
- 包名称必须是唯一的。即使在不同的模块中,我们也不能有相同的包名。
- 每个模块将创建一个 jar 文件。对于多个 jar,我们需要创建单独的模块。
- 一个项目可以由多个模块组成。
module m1 {
exports org.example to m3, m2;
exports org.example2 to m2;
opens org.example;
provides org.example.M with org.example.M1;
}
requires 模块指令指定此模块依赖于另一个模块,此关系称为模块依赖关系。每个模块必须明确地指出其依赖项。当模块 A requires 模块 B 时,模块 A 称为读取模块 B,模块 B 则是给模块 A 读取。如需指定对其他模块的依赖性,请使用 requires指令.
module m3 {
requires m1;
requires m2;
uses org.example.M;
}
这里就表明m3模块需要依赖m1模块
requires static 指令,用于指示在编译时必需要有模块,在运行时则不是必须的。这称为可选相关项
requires transitive — 隐式可读性。指定对其他模块的依赖性并确保其他模块读取您的模块时也能读取这依赖性,所谓的隐式可读性,请使用 requires transitive
这个就有点像是maven依赖的传递性, 比如B依赖了A, C依赖了B, 那么C可以直接使用A的模块的API.
默认情况下,模块里下所有包都是私有的,即使被外部依赖也无法访问,一个模块之内的包还遵循之前的规则不受模块影响。
exports 及 exports…to。exports 模块指令指定模块的一个包,其 public 类型(及其嵌套的 public 和 protected 类型)应可供所有其他模块的代码访问。通过 exports…to 指令,您可以在逗号分隔的列表中指定哪个模块或模块的代码可以访问导出的包,这就是所谓的限定导出。
module m1 {
exports org.example to m3;
exports org.example2 to m2;
opens org.example;
provides org.example.M with org.example.M1;
}
比如这里的exports, 对于m3模块, 它只能使用org.example包下的api, 不能使用org.example2包下的api.
对于m2模块, 它只能使用org.example2包下的api, 不能使用org.example包下的api.
这两个是定向导出的, 当然我们也可以直接使用:
module m1 {
exports org.example;
}
那么只要是依赖了m1的模块, 都可以使用org.example包下的api.
uses 模块指令指定此模块所使用的服务,使此模块成为服务使用者。service 是类的对象,用于实施接口或扩展 uses 指令中指定的 abstract 类。
使用 uses 关键字,我们可以指定我们的模块需要或使用某些服务, 这个服务通常是一个接口或抽象类, 而不是一个具体的实现类。
uses只能从模块自己的包中或者requires、requires static以及requires transitive传递过来的接口或者抽象类。
module m3 {
requires m1;
requires m2;
uses org.example.M;
}
比如这里使用了org.example.M, M是m1模块中定义的接口, m3模块依赖了m1和m2, 只要这2个模块中有M的实现类, 那么我们就可以直接使用了.
ServiceLoader<M> load = ServiceLoader.load(M.class);
load.stream().forEach(p -> System.out.println(p.type()));
比如我们通过ServiceLoader加载所有M的实现类, 只要依赖的模块或者间接依赖的模块有提供M的实现, 就是下面的provides指令, 我们都可以加载到.
provides…with 模块指令指定模块提供服务实施,使模块成为服务提供者。指令的 provides 部分指定模块的 uses 指令指令中列出的接口或 abstract 类;指令的 with 部分指定 implements 接口或 extends abstract类的服务提供类的名称。
module m2 {
requires m1;
exports org.zk;
provides org.example.M with org.zk.M21;
}
对于M接口m2模块提供了它的实现是M21.
包 的 public 类型(及其嵌套的 public 和 protected 类型)只能在运行时可供其他模块中的代码访问。同样,指定包中的所有类型(以及所有类型的成员)都可通过 reflection 进行访问.
我们来看下jdk8反射的使用方式
User user = new User();
user.setName("1");
Class<? extends User> aClass = user.getClass();
Field name = null;
try {
name = aClass.getDeclaredField("name");
name.setAccessible(true);
Object object = name.get(user);
System.out.println(object);
} catch (Exception e) {
throw new RuntimeException(e);
}
这样直接使用的化, 我们就可以取到name的值.
同样的方式, 如果在jdk17模块系统中, 就会有如下提示:
因为我们定义的User类是在m1模块的org.example包下, 所以这时候就必须要用opens指令开放反射权限.
module m1 {
exports org.example to m3, m2;
exports org.example2 to m2;
opens org.example;
}
添加了opens指令后, 我们就可以正常使用反射api的方式了.
注意:
未命名模块
添加到类路径中的 jar 和类。当我们将 jar 或类添加到类路径时,所有这些类都会添加到未命名的模块中只导出到其他未命名的模块和自动模块。这意味着,应用程序模块无法访问这些类。它可以访问所有模块的类。
G1 (Garbage First) 是一款面向服务器的垃圾收集器,主要针对配置多核处理器以及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征,JDK 9开始默认使用G1 垃圾收集器.
G1的主要特点可以分为以下四点:
垃圾收集算法方法论是基于分代收集的思想,垃圾收集器又是垃圾收集算法方法论的具体实现,而其他的垃圾收集器也都是这么实现的,把堆划分成年轻代、老年代,但是G1在物理方面却已经脱离了分代的概念,虽然底层逻辑还是借用了分代的思想,既然脱离了分代的概念, 可以参考下图.
G1将一整块堆划分成多个大小相等的独立区域Region,JVM最多可以有2048个Region默认也是2048个。
一般Region大小等于堆大小除以2048,比如堆大小是4096M,则Region的大小是2M,当然可以通过JVM命令 -XX:G1HeapRegionSize 调整Region大小,但是推荐默认的大小调整。
年轻代对堆内存的占比是5%,如果堆大小是4096M,那么年轻代占据约200MB左右的内存,对应的Region区域个数就是100个。
年轻代中的Eden区和Survivor区对应的region也跟之前一样,默认8:1:1,假设年轻代现在有100个Region,那么Eden区就是80个,survivor0就是10个,survivor1就是10个。
一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化,所以说在G1的世界里,它只认识Region,但是逻辑上还是有新生代和老年代。
大对象处理方式
G1垃圾收集器对于对象什么时候会移动到老年代和之前讲的套路一样,唯一不同的就是对于大对象的处理,G1有专门分配大对象的Humongous区,而不是让大对象进入老年代的Region,在G1中,如果一个对象超过了Region区的50%大小,那么就被判定成大对象,比如上面说的,如果Region区是2M的大小,如果一个对象超过了1M,那么就放入Region区,而且一个大对象如果太大,那么会横跨多个Region来存放。
Humongous区专门用来存放大对象,不用直接进入老年代,可以节约老年代的空间,Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
下图是G1运行示意图:
CMS回收阶段是跟用户线程一起并发执行的,但是G1因为内部实现太过复杂,所以暂时没有实现并发回收,到了ZGC,Shenandoah就实现了并发收集Shenandoah可以看作是G1的升级版
总体上来说,G1用的算法有点类似于标记-整理,因为产生很少的内存碎片,和标记-整理达到的效果是一样的,但是它底层还是用的标记-复制算法。
Shenandoah一词来自于印第安语,十九世纪四十年代有一首著名的航海歌曲在水手中广为流传,讲述一位年轻富商爱上印第安酋长Shenandoah的女儿的故事。 后来美国有一条位于Virginia州西部的小河以此命名,所以Shenandoah的中文译名为“情人渡”。
Shenandoah在Open JDK12中推出,是由Red Hat开发,主要为了解决之前各种垃圾回收器处理大堆时停顿较长的问题。
Shenandoah的设计目标是将停顿压缩到10ms级别(G1将低停顿做到了百毫秒级别),且与堆大小无关。它的很多设计点在权衡上更倾向于低停顿,而不是高吞吐。
Shenandoah是OpenJDK中的垃圾处理器,ZGC是Oracle JDK的垃圾处理器,Shenandoah很多方面与G1非常相似,甚至共用了一部分代码。
Shenandoah和G1有三点主要区别:
1.G1的回收是需要STW的,而且这部分停顿占整体停顿时间的80%以上,Shenandoah则实现了并发回收。
2.Shenandoah不再区分年轻代和年老代。
3.Shenandoah使用连接矩阵替代G1中的卡表。
- 卡表
G1堆中的每一个Region都有一份Rememberd Set,也叫RSet,它的作用就是为每一个Region记录哪些Region对其含有引用。
由于对象引用变更非常频繁,如果同步写卡表消耗非常大,所以通常会把更新信息存入队列中再异步更新RSet。耗费计算资源还占据了非常大的内存空间。- 连接矩阵
连接矩阵可以简单理解为一个二维表格,如果Region A中有对象指向Region B中的对象,那么就在表格的第A行第B列打上标记。
连接矩阵的颗粒度更粗,直接指向了整个Region,这是通过选择更低资源消耗的连接矩阵而对吞吐进行妥协的一项决策。
下图是shenandoah的运行示意图:
ZGC是Oracle在JDK11中引入,并于JDK15中作为生产就绪使用,其设计之初定义了三大目标:
1.支持TB级内存
2.停顿控制在10ms以内,且不随堆大小增加而增加
3.对程序吞吐量影响小于15%
随着JDK的迭代,目前JDK16及以上版本,ZGC已经可以实现不超过1毫秒的停顿,适用于堆大小在8MB到16TB之间。
ZGC和G1一样也采用了分区域的堆内存布局,ZGC的Region(官方称为Page,概念同G1 Region)可以动态创建和销毁,容量也可以动态调整。
ZGC的Region分为三种:
1.小型Region容量固定为2MB,用于存放小于256KB的对象。
2.中型Region容量固定为32MB,用于存放大于等于256KB但不足4MB的对象。
3.大型Region容量为2MB的整数倍,存放4MB及以上大小的对象,而且每个大型Region中只存放一个大对象。由于大对象移动代价过大,所以该对象不会被重分配。
G1中的回收集用来存放所有需要G1扫描的Region,而ZGC为了省去卡表的维护,标记过程会扫描所有Region,如果判定某个Region中的存活对象需要被重分配,那么就将该Region放入重分配集中。
通俗的说,如果将GC分为标记和回收两个主要阶段,那么回收集是用来判定标记哪些Region,重分配集用来判定回收哪些Region。
和Shenandoah相同,ZGC也实现了并发回收,不同的是前者是使用转发指针来实现的,后者则是采用染色指针的技术来实现。
三色标记本质上与对象无关,仅仅与引用有关:通过引用关系判定对像存活与否。HotSpot虚拟机中不同垃圾回收器有着不同的处理方式,有些是标记在对象头中,有些是标记在单独的数据结构中,而ZGC则是直接标记在指针上。
64位机器指针是64位,Linux下64位中高18位不能用来寻址,剩下46位中,ZGC选择其中4位用来辅助GC工作,另外42位能够支持最大内存为4T,通常来说,4T的内存完全够用。