最近研究时区问题的时候发现Linux上的Java时区设置简单中带着一些不简单,翻了翻jvm的源码把这部分的逻辑理清楚了

JVM如何获取获取当前时区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
  
public static TimeZone getDefault() {
return (TimeZone) getDefaultRef().clone();
}

/**
* Returns the reference to the default TimeZone object. This
* method doesn't create a clone.
*/
static TimeZone getDefaultRef() {
TimeZone defaultZone = defaultTimeZone;
if (defaultZone == null) {
// Need to initialize the default time zone.
defaultZone = setDefaultZone();
assert defaultZone != null;
}
// Don't clone here.
return defaultZone;
}
private static synchronized TimeZone setDefaultZone() {
TimeZone tz;
// get the time zone ID from the system properties
Properties props = GetPropertyAction.privilegedGetProperties();
String zoneID = props.getProperty("user.timezone");

// if the time zone ID is not set (yet), perform the
// platform to Java time zone ID mapping.
if (zoneID == null || zoneID.isEmpty()) {
String javaHome = StaticProperty.javaHome();
try {
zoneID = getSystemTimeZoneID(javaHome);
if (zoneID == null) {
zoneID = GMT_ID;
}
} catch (NullPointerException e) {
zoneID = GMT_ID;
}
}

// Get the time zone for zoneID. But not fall back to
// "GMT" here.
tz = getTimeZone(zoneID, false);

if (tz == null) {
// If the given zone ID is unknown in Java, try to
// get the GMT-offset-based time zone ID,
// a.k.a. custom time zone ID (e.g., "GMT-08:00").
String gmtOffsetID = getSystemGMTOffsetID();
if (gmtOffsetID != null) {
zoneID = gmtOffsetID;
}
tz = getTimeZone(zoneID, true);
}
assert tz != null;

final String id = zoneID;
props.setProperty("user.timezone", id);

defaultTimeZone = tz;
return tz;
}
/**
* Gets the platform defined TimeZone ID.
**/
private static native String getSystemTimeZoneID(String javaHome);

可以看到获取默认时区的主要代码逻辑在setDefaultZone这个方法中:

  1. 先从user.timezone这个系统参数中获取时区设置
1
2
3
4
TimeZone tz;
// get the time zone ID from the system properties
Properties props = GetPropertyAction.privilegedGetProperties();
String zoneID = props.getProperty("user.timezone");
  1. 如果user.timezone没有设置,则获取系统的默认时区设置,如果没有获取到默认时区设置则使用GMT
1
2
3
4
5
6
7
8
9
10
11
12
13
// if the time zone ID is not set (yet), perform the
// platform to Java time zone ID mapping.
if (zoneID == null || zoneID.isEmpty()) {
String javaHome = StaticProperty.javaHome();
try {
zoneID = getSystemTimeZoneID(javaHome);
if (zoneID == null) {
zoneID = GMT_ID;
}
} catch (NullPointerException e) {
zoneID = GMT_ID;
}
}
  1. 如果获取到了系统的默认时区设置,但是无法转换成java的ZoneID,则fallback到GMT
1
2
3
4
5
6
7
8
9
10
if (tz == null) {
// If the given zone ID is unknown in Java, try to
// get the GMT-offset-based time zone ID,
// a.k.a. custom time zone ID (e.g., "GMT-08:00").
String gmtOffsetID = getSystemGMTOffsetID();
if (gmtOffsetID != null) {
zoneID = gmtOffsetID;
}
tz = getTimeZone(zoneID, true);
}
  1. 最后将时区赋值到user.timezone系统参数里

getSystemTimeZoneID

user.timezone这个系统参数用的很少,一般对于linux的服务会使用TZ变量来对Java服务进行时区的配置。

getSystemTimeZoneID是一个native函数,可以在Github上找到这个函数的源码,这个方法调用了一个findJavaTZ_md函数,这个函数在TimeZone_md.h中被声明,对于不同的操作系统jvm有不同的实现,Linux操作系统上的实现的.h和.c文件地址分别为:

findJavaTZ_md这个函数首先会从TZ环境变量拿系统的默认时区,没有获取到有效信息则会调用getPlatformTimeZoneID函数去/etc/timezone/etc/localtime文件获取系统时区信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
char * findJavaTZ_md(const char *java_home_dir)
{
char *tz;
char *javatz = NULL;
char *freetz = NULL;

tz = getenv("TZ");

if (tz == NULL || *tz == '\0') {
tz = getPlatformTimeZoneID();
freetz = tz;
}

if (tz != NULL) {
/* Ignore preceding ':' */
if (*tz == ':') {
tz++;
}
#if defined(__linux__)
/* Ignore "posix/" prefix on Linux. */
if (strncmp(tz, "posix/", 6) == 0) {
tz += 6;
}
#endif

#if defined(_AIX)
/* On AIX do the platform to Java mapping. */
javatz = mapPlatformToJavaTimezone(java_home_dir, tz);
if (freetz != NULL) {
free((void *) freetz);
}
#else
if (freetz == NULL) {
/* strdup if we are still working on getenv result. */
javatz = strdup(tz);
} else if (freetz != tz) {
/* strdup and free the old buffer, if we moved the pointer. */
javatz = strdup(tz);
free((void *) freetz);
} else {
/* we are good if we already work on a freshly allocated buffer. */
javatz = tz;
}
#endif
}

return javatz;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#if defined(__linux__) || defined(_ALLBSD_SOURCE)
static const char *ETC_TIMEZONE_FILE = "/etc/timezone";
static const char *ZONEINFO_DIR = "/usr/share/zoneinfo";
static const char *DEFAULT_ZONEINFO_FILE = "/etc/localtime";
#else
static const char *SYS_INIT_FILE = "/etc/default/init";
static const char *ZONEINFO_DIR = "/usr/share/lib/zoneinfo";
static const char *DEFAULT_ZONEINFO_FILE = "/usr/share/lib/zoneinfo/localtime";
#endif /* defined(__linux__) || defined(_ALLBSD_SOURCE) */

/*
* Performs Linux specific mapping and returns a zone ID
* if found. Otherwise, NULL is returned.
*/
static char * getPlatformTimeZoneID()
{
struct stat64 statbuf;
char *tz = NULL;
FILE *fp;
int fd;
char *buf;
size_t size;
int res;

#if defined(__linux__)
/*
* Try reading the /etc/timezone file for Debian distros. There's
* no spec of the file format available. This parsing assumes that
* there's one line of an Olson tzid followed by a '\n', no
* leading or trailing spaces, no comments./etc/timezone
*/
if ((fp = fopen(ETC_TIMEZONE_FILE, "r")) != NULL) {
char line[256];

if (fgets(line, sizeof(line), fp) != NULL) {
char *p = strchr(line, '\n');
if (p != NULL) {
*p = '\0';
}
if (strlen(line) > 0) {
tz = strdup(line);
}
}
(void) fclose(fp);
if (tz != NULL) {
return tz;
}
}
#endif /* defined(__linux__) */

/*
* Next, try /etc/localtime to find the zone ID.
*/
RESTARTABLE(lstat64(DEFAULT_ZONEINFO_FILE, &statbuf), res);
if (res == -1) {
return NULL;
}

/*
* If it's a symlink, get the link name and its zone ID part. (The
* older versions of timeconfig created a symlink as described in
* the Red Hat man page. It was changed in 1999 to create a copy
* of a zoneinfo file. It's no longer possible to get the zone ID
* from /etc/localtime.)
*/
if (S_ISLNK(statbuf.st_mode)) {
char linkbuf[PATH_MAX+1];
int len;

if ((len = readlink(DEFAULT_ZONEINFO_FILE, linkbuf, sizeof(linkbuf)-1)) == -1) {
jio_fprintf(stderr, (const char *) "can't get a symlink of %s\n",
DEFAULT_ZONEINFO_FILE);
return NULL;
}
linkbuf[len] = '\0';
removeDuplicateSlashes(linkbuf);
collapse(linkbuf);
tz = getZoneName(linkbuf);
if (tz != NULL) {
tz = strdup(tz);
return tz;
}
}

/*
* If it's a regular file, we need to find out the same zoneinfo file
* that has been copied as /etc/localtime.
* If initial symbolic link resolution failed, we should treat target
* file as a regular file.
*/
RESTARTABLE(open(DEFAULT_ZONEINFO_FILE, O_RDONLY), fd);
if (fd == -1) {
return NULL;
}

RESTARTABLE(fstat64(fd, &statbuf), res);
if (res == -1) {
(void) close(fd);
return NULL;
}
size = (size_t) statbuf.st_size;
buf = (char *) malloc(size);
if (buf == NULL) {
(void) close(fd);
return NULL;
}

RESTARTABLE(read(fd, buf, size), res);
if (res != (ssize_t) size) {
(void) close(fd);
free((void *) buf);
return NULL;
}
(void) close(fd);

tz = findZoneinfoFile(buf, size, ZONEINFO_DIR);
free((void *) buf);
return tz;
}

/etc/timezone

glibc默认的时区配置文件为/etc/localtime,但是Debian系的发行版中还有一些软件在使用/etc/timezone,可以在Debian的软件仓库中找到:

https://codesearch.debian.net/search?q=%2Fetc%2Ftimezone

总结

所以对于Java服务来说,先读取user.timezone获取时区信息,如果没有获取到user.timezone则读取TZ 环境变量,如果TZ环境变量如果没有获取到则读取/etc/timezone/etc/localtime,如果还是没有读取到则fallback到GMT,最后将获取的的时区信息设置到user.timezone,下次直接从user.timezone获取时区信息。

https://cloud.tencent.com/developer/article/1691540

https://github.com/openjdk/jdk/blob/47b86690b6672301aa46d4a7b9ced58d17047cc7/src/java.base/share/native/libjava/TimeZone.c#L40

https://github.com/openjdk/jdk/blob/master/src/java.base/unix/native/libjava/TimeZone_md.h

https://github.com/openjdk/jdk/blob/master/src/java.base/unix/native/libjava/TimeZone_md.c

https://codesearch.debian.net/search?q=%2Fetc%2Ftimezone